6.1 KiB
Scene Lifecycle And Serialization
为什么场景系统通常这样分层
商业级游戏引擎很少把“场景”“对象”“组件”揉成一个类。更常见的拆法是:
- 场景负责对象集合、根入口、更新驱动与存档边界
- 对象负责层级关系、激活传播与生命周期分发
- 组件负责可组合行为本身
XCEngine 当前的 Scene / GameObject / Component 关系就是这个思路,所以它在心智模型上明显更像 Unity 风格对象系统,而不是单一 Entity 容器。
Scene 真正负责什么
当前 Scene 最核心的职责有四件事:
- 持有
GameObject的唯一所有权。 - 维护根对象入口与按 ID 查找能力。
- 从根入口驱动
Update/FixedUpdate/LateUpdate。 - 作为完整对象树的保存与加载边界。
这样做的好处是,场景可以被理解成“一个可整体切换、整体保存、整体重新加载的运行时世界片段”。
为什么要同时保存对象表和根列表
场景里既有 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 nametagactivelayerparenttransform- 普通组件类型与 payload
这意味着当前 round-trip 不只是“把树结构存下来”,而是会把对象分类元数据和普通组件数据一起恢复回来。
Tag 查询为什么现在已经是真实查询
旧文档最容易误导人的一点就是这里。
当前 Scene::FindGameObjectWithTag() 已经不再比较对象名字,而是:
- 从根对象开始
- 对每个对象调用
CompareTag(tag) - 按深度优先顺序返回第一个匹配项
同时,场景保存和加载也会保留 tag / layer,所以这套对象元数据已经是场景系统的真实组成部分。
但仍要注意:它现在还是轻量实现,不是完整 TagManager:
- 没有项目级 tag 定义表
- 没有查询索引
- 仍然是线性扫描
为什么脚本改 tag / layer 后场景查询会立刻变
当前 Mono 运行时把以下接口直接桥接到同一份 native GameObject 字段:
GameObject.tagGameObject.layerComponent.tagComponent.layerCompareTag()
所以:
- 脚本看到的不是副本
- 脚本写回后,原生对象会立刻更新
- 场景级
FindGameObjectWithTag()会立即观察到新值
这是一种很典型的商业引擎托管桥接设计:脚本 API 不维护一份平行数据,而是直接操作原生运行时对象。
当前最需要小心的几个事实
- 场景析构不会自动重放完整销毁流程,所以不要把
~Scene()当成完整清理器。 FindGameObjectWithTag()现在是真实 tag 查询,但仍是线性扫描。Scene::m_active当前不会阻止Scene::Update()。- 从文本反序列化恢复后,不会自动补发
Awake()/Start()。 Save()/Load()的错误处理目前比较轻,不提供事务式恢复。
实际使用建议
如果你现在基于这套系统开发,比较稳妥的做法通常是:
- 运行时对象创建优先用
CreateGameObject(),不要手工拼接半初始化对象。 - 需要完整保存和恢复时,用
SerializeToString()/DeserializeFromString(),不要把单对象序列化当场景格式。 - 需要生命周期完整对称时,明确区分“新建对象”与“从存档恢复对象”两条流程。
- 可以放心把
FindGameObjectWithTag()当作真实 tag 查询,但不要把它想象成带项目配置和索引优化的完整 TagManager。