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

172 lines
6.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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)