# 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 - [Scene Module](../../XCEngine/Scene/Scene.md) - [Scene](../../XCEngine/Scene/Scene/Scene.md) - [SceneManager](../../XCEngine/Scene/SceneManager/SceneManager.md) - [GameObject](../../XCEngine/Components/GameObject/GameObject.md)