86 lines
3.8 KiB
Markdown
86 lines
3.8 KiB
Markdown
|
|
# Scene Lifecycle And Serialization
|
|||
|
|
|
|||
|
|
## 为什么场景系统通常这样分层
|
|||
|
|
|
|||
|
|
商业级游戏引擎很少把“场景”“对象”“组件”揉成一个类。更常见的拆法是:
|
|||
|
|
|
|||
|
|
- 场景负责对象集合、根节点入口和存档边界。
|
|||
|
|
- 对象负责层级关系与生命周期转发。
|
|||
|
|
- 组件负责真正的可组合行为。
|
|||
|
|
|
|||
|
|
`XCEngine` 当前的 `Scene` / `GameObject` / `Component` 关系基本就是这个思路,所以它看上去会比较像 Unity 风格的设计,而不是传统单一 Entity 容器。
|
|||
|
|
|
|||
|
|
## `Scene` 真正负责什么
|
|||
|
|
|
|||
|
|
当前 `Scene` 最核心的职责有三件事:
|
|||
|
|
|
|||
|
|
1. 持有 `GameObject` 的唯一所有权。
|
|||
|
|
2. 从根对象入口驱动 `Update` / `FixedUpdate` / `LateUpdate`。
|
|||
|
|
3. 作为保存和加载的边界,把一整棵对象树序列化出去或重新建回来。
|
|||
|
|
|
|||
|
|
这样做的好处是,场景可以被理解成“一个可整体保存、整体切换的运行时世界片段”。
|
|||
|
|
|
|||
|
|
## 为什么要有根对象列表
|
|||
|
|
|
|||
|
|
场景里既有对象树,又单独保存 `m_rootGameObjects`,这不是重复设计,而是为了让两件事都变简单:
|
|||
|
|
|
|||
|
|
- 查“从哪里开始遍历场景”时,不用扫描全体对象找没有父节点的项。
|
|||
|
|
- 对象挂接和脱离父节点时,只要同步更新根列表即可。
|
|||
|
|
|
|||
|
|
这是很多商用引擎场景树都会采用的做法,因为“根入口”和“局部子树”是两种不同维度的查询。
|
|||
|
|
|
|||
|
|
## 当前 active 状态有两层,但还没有完全打通
|
|||
|
|
|
|||
|
|
理解这块很重要:
|
|||
|
|
|
|||
|
|
- `SceneManager::GetActiveScene()` 是“哪一个场景被管理器认为是当前活动场景”。
|
|||
|
|
- `Scene::IsActive()` 是 `Scene` 自己保存的一个布尔标志。
|
|||
|
|
- `GameObject::IsActiveInHierarchy()` 才是对象更新真正参与的门槛。
|
|||
|
|
|
|||
|
|
在当前实现里,这三者并没有完全联动:
|
|||
|
|
|
|||
|
|
- `Scene::SetActive(false)` 不会阻止 `Scene::Update()`。
|
|||
|
|
- `SceneManager::SetActiveScene()` 也不会自动改 `Scene::IsActive()`。
|
|||
|
|
|
|||
|
|
这意味着现在更稳妥的理解方式是:
|
|||
|
|
|
|||
|
|
- `SceneManager` 的 active scene 更接近“当前选中的场景引用”。
|
|||
|
|
- `Scene::m_active` 更接近“一个可保存的场景状态位”。
|
|||
|
|
|
|||
|
|
## 序列化为什么用自定义文本格式
|
|||
|
|
|
|||
|
|
当前 `Scene::SerializeToString()` 没有走 JSON,而是用了非常直接的文本块格式。这样做的现实好处是:
|
|||
|
|
|
|||
|
|
- 实现简单。
|
|||
|
|
- 组件可以继续用自己的 `Serialize(std::ostream&)` 输出。
|
|||
|
|
- 调试时直接打开文本就能看见对象、父子关系和组件条目。
|
|||
|
|
|
|||
|
|
但代价也很明确:
|
|||
|
|
|
|||
|
|
- 格式是引擎私有的,没有版本协商。
|
|||
|
|
- 解析鲁棒性有限。
|
|||
|
|
- 组件恢复依赖 `ComponentFactoryRegistry`,组件名或注册表变化都会影响加载结果。
|
|||
|
|
|
|||
|
|
## 当前最需要小心的几个事实
|
|||
|
|
|
|||
|
|
- 场景析构不会自动为所有对象走显式销毁流程,所以不要把 `~Scene()` 当成完整生命周期清理器。
|
|||
|
|
- `FindGameObjectWithTag()` 现在其实是在按名字查。
|
|||
|
|
- `LoadSceneAsync()` 现在不是异步,更多只是一个名字上预留好的 API。
|
|||
|
|
- `LoadScene()` 后的管理器 key 来自文件名,而不是场景内部保存的 `scene=` 名称。
|
|||
|
|
|
|||
|
|
## 实际使用建议
|
|||
|
|
|
|||
|
|
如果你现在基于这套系统开发,比较稳妥的做法通常是:
|
|||
|
|
|
|||
|
|
- 需要 `OnDestroy` 或场景事件时,显式调用对象销毁入口,不要只依赖场景析构。
|
|||
|
|
- 需要稳定查场景时,优先用 `CreateScene()` 的名称或加载文件的文件名 stem,不要假设一定等于场景内部名字。
|
|||
|
|
- 不要把 `LoadSceneAsync()` 当成真正的后台加载接口。
|
|||
|
|
- 不要把 `FindGameObjectWithTag()` 当成完整 tag 系统。
|
|||
|
|
|
|||
|
|
## 相关 API
|
|||
|
|
|
|||
|
|
- [Scene Module](../../XCEngine/Scene/Scene.md)
|
|||
|
|
- [Scene](../../XCEngine/Scene/Scene/Scene.md)
|
|||
|
|
- [SceneManager](../../XCEngine/Scene/SceneManager/SceneManager.md)
|
|||
|
|
- [GameObject](../../XCEngine/Components/GameObject/GameObject.md)
|