3.8 KiB
3.8 KiB
Scene Lifecycle And Serialization
为什么场景系统通常这样分层
商业级游戏引擎很少把“场景”“对象”“组件”揉成一个类。更常见的拆法是:
- 场景负责对象集合、根节点入口和存档边界。
- 对象负责层级关系与生命周期转发。
- 组件负责真正的可组合行为。
XCEngine 当前的 Scene / GameObject / Component 关系基本就是这个思路,所以它看上去会比较像 Unity 风格的设计,而不是传统单一 Entity 容器。
Scene 真正负责什么
当前 Scene 最核心的职责有三件事:
- 持有
GameObject的唯一所有权。 - 从根对象入口驱动
Update/FixedUpdate/LateUpdate。 - 作为保存和加载的边界,把一整棵对象树序列化出去或重新建回来。
这样做的好处是,场景可以被理解成“一个可整体保存、整体切换的运行时世界片段”。
为什么要有根对象列表
场景里既有对象树,又单独保存 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 系统。