Files
XCEngine/docs/api/_guides/Scene/Scene-Lifecycle-And-Serialization.md

3.8 KiB
Raw Blame History

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