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

6.1 KiB
Raw Blame History

Scene Lifecycle And Serialization

为什么场景系统通常这样分层

商业级游戏引擎很少把“场景”“对象”“组件”揉成一个类。更常见的拆法是:

  • 场景负责对象集合、根入口、更新驱动与存档边界
  • 对象负责层级关系、激活传播与生命周期分发
  • 组件负责可组合行为本身

XCEngine 当前的 Scene / GameObject / Component 关系就是这个思路,所以它在心智模型上明显更像 Unity 风格对象系统,而不是单一 Entity 容器。

Scene 真正负责什么

当前 Scene 最核心的职责有四件事:

  1. 持有 GameObject 的唯一所有权。
  2. 维护根对象入口与按 ID 查找能力。
  3. 从根入口驱动 Update / FixedUpdate / LateUpdate
  4. 作为完整对象树的保存与加载边界。

这样做的好处是,场景可以被理解成“一个可整体切换、整体保存、整体重新加载的运行时世界片段”。

为什么要同时保存对象表和根列表

场景里既有 m_gameObjects,又单独保存 m_rootGameObjects,这不是重复设计,而是为了把两类操作都做得更直接:

  • 按 ID 找对象时,不需要遍历层级树
  • 从场景入口开始递归更新、递归序列化时,不需要扫描所有对象去找“谁没有父节点”

这是很多商业引擎场景树都会采用的结构,因为“对象所有权表”和“层级入口列表”是两个不同维度的数据。

当前 active 状态有三层语义

理解这点很重要:

  • SceneManager::GetActiveScene() 更接近“当前被管理器选中的场景”
  • Scene::IsActive() 是场景自己的一个布尔状态位
  • GameObject::IsActiveInHierarchy() 才是对象更新真正参与的门槛

在当前实现里,它们没有完全联动:

  • Scene::SetActive(false) 不会阻止 Scene::Update()
  • SceneManager::SetActiveScene() 也不会自动同步 Scene::IsActive()

所以更稳妥的理解方式是:

  • SceneManager 的 active scene 偏向管理器层选择
  • Scene::m_active 偏向一个可保存状态位
  • 运行时真正的对象是否活跃,还是看对象自己的层级有效激活态

创建路径和加载路径为什么要分开

创建路径

Scene::CreateGameObject() 当前会:

  • 创建对象
  • 注册到场景和全局 registry
  • 接入根列表或父对象
  • 立即调用 Awake()
  • 触发对象创建事件

反序列化路径

Scene::DeserializeFromString() 当前会:

  • 先清空旧场景容器
  • 再按文本内容两阶段重建对象、组件和父子关系
  • 重新注册到场景和全局 registry

但它不会:

  • 经过 CreateGameObject()
  • 触发对象创建事件
  • 自动补发 Awake() / Start()

这不是遗漏文档,而是当前实现的真实边界。它意味着“新建对象”和“从存档恢复对象”在初始化链上并不完全对称。

为什么序列化使用自定义文本格式

当前 Scene::SerializeToString() 没有走 JSON而是使用引擎自己的文本块格式。这样做的现实收益是

  • 实现直接
  • 调试友好
  • 组件仍然可以复用各自的 Serialize(std::ostream&)
  • 打开文件就能看见对象树、父子关系、tag/layer 与组件条目

代价也很明确:

  • 这是引擎私有格式,不是通用交换协议
  • 当前没有显式 schema version
  • 组件恢复依赖 ComponentFactoryRegistry
  • 坏数据的异常恢复能力有限

当前场景文本里到底会保存什么

场景文本当前已经覆盖:

  • 场景名
  • 场景 active 位
  • 对象 id / uuid
  • name
  • tag
  • active
  • layer
  • parent
  • transform
  • 普通组件类型与 payload

这意味着当前 round-trip 不只是“把树结构存下来”,而是会把对象分类元数据和普通组件数据一起恢复回来。

Tag 查询为什么现在已经是真实查询

旧文档最容易误导人的一点就是这里。

当前 Scene::FindGameObjectWithTag() 已经不再比较对象名字,而是:

  • 从根对象开始
  • 对每个对象调用 CompareTag(tag)
  • 按深度优先顺序返回第一个匹配项

同时,场景保存和加载也会保留 tag / layer,所以这套对象元数据已经是场景系统的真实组成部分。

但仍要注意:它现在还是轻量实现,不是完整 TagManager

  • 没有项目级 tag 定义表
  • 没有查询索引
  • 仍然是线性扫描

为什么脚本改 tag / layer 后场景查询会立刻变

当前 Mono 运行时把以下接口直接桥接到同一份 native GameObject 字段:

  • GameObject.tag
  • GameObject.layer
  • Component.tag
  • Component.layer
  • CompareTag()

所以:

  • 脚本看到的不是副本
  • 脚本写回后,原生对象会立刻更新
  • 场景级 FindGameObjectWithTag() 会立即观察到新值

这是一种很典型的商业引擎托管桥接设计:脚本 API 不维护一份平行数据,而是直接操作原生运行时对象。

当前最需要小心的几个事实

  • 场景析构不会自动重放完整销毁流程,所以不要把 ~Scene() 当成完整清理器。
  • FindGameObjectWithTag() 现在是真实 tag 查询,但仍是线性扫描。
  • Scene::m_active 当前不会阻止 Scene::Update()
  • 从文本反序列化恢复后,不会自动补发 Awake() / Start()
  • Save() / Load() 的错误处理目前比较轻,不提供事务式恢复。

实际使用建议

如果你现在基于这套系统开发,比较稳妥的做法通常是:

  • 运行时对象创建优先用 CreateGameObject(),不要手工拼接半初始化对象。
  • 需要完整保存和恢复时,用 SerializeToString() / DeserializeFromString(),不要把单对象序列化当场景格式。
  • 需要生命周期完整对称时,明确区分“新建对象”与“从存档恢复对象”两条流程。
  • 可以放心把 FindGameObjectWithTag() 当作真实 tag 查询,但不要把它想象成带项目配置和索引优化的完整 TagManager。

相关 API