2026-03-26 19:34:24 +08:00
|
|
|
|
# Scene Lifecycle And Serialization
|
|
|
|
|
|
|
|
|
|
|
|
## 为什么场景系统通常这样分层
|
|
|
|
|
|
|
|
|
|
|
|
商业级游戏引擎很少把“场景”“对象”“组件”揉成一个类。更常见的拆法是:
|
|
|
|
|
|
|
2026-04-08 16:07:03 +08:00
|
|
|
|
- 场景负责对象集合、根入口、更新驱动与存档边界
|
|
|
|
|
|
- 对象负责层级关系、激活传播与生命周期分发
|
|
|
|
|
|
- 组件负责可组合行为本身
|
2026-03-26 19:34:24 +08:00
|
|
|
|
|
2026-04-08 16:07:03 +08:00
|
|
|
|
`XCEngine` 当前的 `Scene` / `GameObject` / `Component` 关系就是这个思路,所以它在心智模型上明显更像 Unity 风格对象系统,而不是单一 Entity 容器。
|
2026-03-26 19:34:24 +08:00
|
|
|
|
|
|
|
|
|
|
## `Scene` 真正负责什么
|
|
|
|
|
|
|
2026-04-08 16:07:03 +08:00
|
|
|
|
当前 `Scene` 最核心的职责有四件事:
|
2026-03-26 19:34:24 +08:00
|
|
|
|
|
|
|
|
|
|
1. 持有 `GameObject` 的唯一所有权。
|
2026-04-08 16:07:03 +08:00
|
|
|
|
2. 维护根对象入口与按 ID 查找能力。
|
|
|
|
|
|
3. 从根入口驱动 `Update` / `FixedUpdate` / `LateUpdate`。
|
|
|
|
|
|
4. 作为完整对象树的保存与加载边界。
|
2026-03-26 19:34:24 +08:00
|
|
|
|
|
2026-04-08 16:07:03 +08:00
|
|
|
|
这样做的好处是,场景可以被理解成“一个可整体切换、整体保存、整体重新加载的运行时世界片段”。
|
2026-03-26 19:34:24 +08:00
|
|
|
|
|
2026-04-08 16:07:03 +08:00
|
|
|
|
## 为什么要同时保存对象表和根列表
|
2026-03-26 19:34:24 +08:00
|
|
|
|
|
2026-04-08 16:07:03 +08:00
|
|
|
|
场景里既有 `m_gameObjects`,又单独保存 `m_rootGameObjects`,这不是重复设计,而是为了把两类操作都做得更直接:
|
2026-03-26 19:34:24 +08:00
|
|
|
|
|
2026-04-08 16:07:03 +08:00
|
|
|
|
- 按 ID 找对象时,不需要遍历层级树
|
|
|
|
|
|
- 从场景入口开始递归更新、递归序列化时,不需要扫描所有对象去找“谁没有父节点”
|
2026-03-26 19:34:24 +08:00
|
|
|
|
|
2026-04-08 16:07:03 +08:00
|
|
|
|
这是很多商业引擎场景树都会采用的结构,因为“对象所有权表”和“层级入口列表”是两个不同维度的数据。
|
2026-03-26 19:34:24 +08:00
|
|
|
|
|
2026-04-08 16:07:03 +08:00
|
|
|
|
## 当前 active 状态有三层语义
|
2026-03-26 19:34:24 +08:00
|
|
|
|
|
2026-04-08 16:07:03 +08:00
|
|
|
|
理解这点很重要:
|
2026-03-26 19:34:24 +08:00
|
|
|
|
|
2026-04-08 16:07:03 +08:00
|
|
|
|
- `SceneManager::GetActiveScene()` 更接近“当前被管理器选中的场景”
|
|
|
|
|
|
- `Scene::IsActive()` 是场景自己的一个布尔状态位
|
|
|
|
|
|
- `GameObject::IsActiveInHierarchy()` 才是对象更新真正参与的门槛
|
2026-03-26 19:34:24 +08:00
|
|
|
|
|
2026-04-08 16:07:03 +08:00
|
|
|
|
在当前实现里,它们没有完全联动:
|
2026-03-26 19:34:24 +08:00
|
|
|
|
|
2026-04-08 16:07:03 +08:00
|
|
|
|
- `Scene::SetActive(false)` 不会阻止 `Scene::Update()`
|
|
|
|
|
|
- `SceneManager::SetActiveScene()` 也不会自动同步 `Scene::IsActive()`
|
2026-03-26 19:34:24 +08:00
|
|
|
|
|
2026-04-08 16:07:03 +08:00
|
|
|
|
所以更稳妥的理解方式是:
|
2026-03-26 19:34:24 +08:00
|
|
|
|
|
2026-04-08 16:07:03 +08:00
|
|
|
|
- `SceneManager` 的 active scene 偏向管理器层选择
|
|
|
|
|
|
- `Scene::m_active` 偏向一个可保存状态位
|
|
|
|
|
|
- 运行时真正的对象是否活跃,还是看对象自己的层级有效激活态
|
2026-03-26 19:34:24 +08:00
|
|
|
|
|
2026-04-08 16:07:03 +08:00
|
|
|
|
## 创建路径和加载路径为什么要分开
|
2026-03-26 19:34:24 +08:00
|
|
|
|
|
2026-04-08 16:07:03 +08:00
|
|
|
|
### 创建路径
|
2026-03-26 19:34:24 +08:00
|
|
|
|
|
2026-04-08 16:07:03 +08:00
|
|
|
|
`Scene::CreateGameObject()` 当前会:
|
2026-03-26 19:34:24 +08:00
|
|
|
|
|
2026-04-08 16:07:03 +08:00
|
|
|
|
- 创建对象
|
|
|
|
|
|
- 注册到场景和全局 registry
|
|
|
|
|
|
- 接入根列表或父对象
|
|
|
|
|
|
- 立即调用 `Awake()`
|
|
|
|
|
|
- 触发对象创建事件
|
2026-03-26 19:34:24 +08:00
|
|
|
|
|
2026-04-08 16:07:03 +08:00
|
|
|
|
### 反序列化路径
|
2026-03-26 19:34:24 +08:00
|
|
|
|
|
2026-04-08 16:07:03 +08:00
|
|
|
|
`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 不维护一份平行数据,而是直接操作原生运行时对象。
|
2026-04-03 15:48:09 +08:00
|
|
|
|
|
2026-03-26 19:34:24 +08:00
|
|
|
|
## 当前最需要小心的几个事实
|
|
|
|
|
|
|
2026-04-08 16:07:03 +08:00
|
|
|
|
- 场景析构不会自动重放完整销毁流程,所以不要把 `~Scene()` 当成完整清理器。
|
|
|
|
|
|
- `FindGameObjectWithTag()` 现在是真实 tag 查询,但仍是线性扫描。
|
|
|
|
|
|
- `Scene::m_active` 当前不会阻止 `Scene::Update()`。
|
|
|
|
|
|
- 从文本反序列化恢复后,不会自动补发 `Awake()` / `Start()`。
|
|
|
|
|
|
- `Save()` / `Load()` 的错误处理目前比较轻,不提供事务式恢复。
|
2026-03-26 19:34:24 +08:00
|
|
|
|
|
|
|
|
|
|
## 实际使用建议
|
|
|
|
|
|
|
|
|
|
|
|
如果你现在基于这套系统开发,比较稳妥的做法通常是:
|
|
|
|
|
|
|
2026-04-08 16:07:03 +08:00
|
|
|
|
- 运行时对象创建优先用 `CreateGameObject()`,不要手工拼接半初始化对象。
|
|
|
|
|
|
- 需要完整保存和恢复时,用 `SerializeToString()` / `DeserializeFromString()`,不要把单对象序列化当场景格式。
|
|
|
|
|
|
- 需要生命周期完整对称时,明确区分“新建对象”与“从存档恢复对象”两条流程。
|
|
|
|
|
|
- 可以放心把 `FindGameObjectWithTag()` 当作真实 tag 查询,但不要把它想象成带项目配置和索引优化的完整 TagManager。
|
2026-03-26 19:34:24 +08:00
|
|
|
|
|
|
|
|
|
|
## 相关 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)
|