docs: sync api and planning docs

This commit is contained in:
2026-04-08 16:07:03 +08:00
parent 08c3278e10
commit 31756847ab
1826 changed files with 44502 additions and 29645 deletions

View File

@@ -4,80 +4,164 @@
商业级游戏引擎很少把“场景”“对象”“组件”揉成一个类。更常见的拆法是:
- 场景负责对象集合、根节点入口和存档边界
- 对象负责层级关系与生命周期转发。
- 组件负责真正的可组合行为
- 场景负责对象集合、根入口、更新驱动与存档边界
- 对象负责层级关系、激活传播与生命周期分发
- 组件负责可组合行为本身
`XCEngine` 当前的 `Scene` / `GameObject` / `Component` 关系基本就是这个思路,所以它看上去会比较像 Unity 风格的设计,而不是传统单一 Entity 容器。
`XCEngine` 当前的 `Scene` / `GameObject` / `Component` 关系就是这个思路,所以它在心智模型上明显更像 Unity 风格对象系统,而不是单一 Entity 容器。
## `Scene` 真正负责什么
当前 `Scene` 最核心的职责有件事:
当前 `Scene` 最核心的职责有件事:
1. 持有 `GameObject` 的唯一所有权。
2. 根对象入口驱动 `Update` / `FixedUpdate` / `LateUpdate`
3. 作为保存和加载的边界,把一整棵对象树序列化出去或重新建回来
2. 维护根对象入口与按 ID 查找能力
3. 从根入口驱动 `Update` / `FixedUpdate` / `LateUpdate`
4. 作为完整对象树的保存与加载边界。
这样做的好处是,场景可以被理解成“一个可整体保存、整体切换的运行时世界片段”。
这样做的好处是,场景可以被理解成“一个可整体切换、整体保存、整体重新加载的运行时世界片段”。
## 为什么要有根对象列表
## 为什么要同时保存对象表和根列表
场景里既有对象树,又单独保存 `m_rootGameObjects`,这不是重复设计,而是为了让两件事都变简单
场景里既有 `m_gameObjects`,又单独保存 `m_rootGameObjects`,这不是重复设计,而是为了把两类操作都做得更直接
- 查“从哪里开始遍历场景”时,不用扫描全体对象找没有父节点的项。
- 对象挂接和脱离父节点时,只要同步更新根列表即可。
- 按 ID 找对象时,不需要遍历层级树
- 从场景入口开始递归更新、递归序列化时,不需要扫描所有对象去找“谁没有父节点”
这是很多商引擎场景树都会采用的做法,因为“根入口”和“局部子树”是两不同维度的查询
这是很多商引擎场景树都会采用的结构,因为“对象所有权表”和“层级入口列表”是两不同维度的数据
## 当前 active 状态有两层,但还没有完全打通
## 当前 active 状态有三层语义
理解这很重要:
理解这很重要:
- `SceneManager::GetActiveScene()` 是“哪一个场景被管理器认为是当前活动场景”
- `Scene::IsActive()` `Scene` 自己保存的一个布尔标志。
- `GameObject::IsActiveInHierarchy()` 才是对象更新真正参与的门槛
- `SceneManager::GetActiveScene()` 更接近“当前被管理器选中的场景”
- `Scene::IsActive()`场景自己的一个布尔状态位
- `GameObject::IsActiveInHierarchy()` 才是对象更新真正参与的门槛
在当前实现里,这三者并没有完全联动:
在当前实现里,它们没有完全联动:
- `Scene::SetActive(false)` 不会阻止 `Scene::Update()`
- `SceneManager::SetActiveScene()` 也不会自动 `Scene::IsActive()`
- `Scene::SetActive(false)` 不会阻止 `Scene::Update()`
- `SceneManager::SetActiveScene()` 也不会自动同步 `Scene::IsActive()`
这意味着现在更稳妥的理解方式是:
所以更稳妥的理解方式是:
- `SceneManager` 的 active scene 更接近“当前选中的场景引用”。
- `Scene::m_active` 更接近“一个可保存的场景状态位”。
- `SceneManager` 的 active scene 偏向管理器层选择
- `Scene::m_active` 偏向一个可保存状态位
- 运行时真正的对象是否活跃,还是看对象自己的层级有效激活态
## 序列化为什么用自定义文本格式
## 创建路径和加载路径为什么要分开
当前 `Scene::SerializeToString()` 没有走 JSON而是用了非常直接的文本块格式。这样做的现实好处是
### 创建路径
- 实现简单。
- 组件可以继续用自己的 `Serialize(std::ostream&)` 输出。
- 调试时直接打开文本就能看见对象、父子关系和组件条目。
`Scene::CreateGameObject()` 当前会:
但代价也很明确:
- 创建对象
- 注册到场景和全局 registry
- 接入根列表或父对象
- 立即调用 `Awake()`
- 触发对象创建事件
- 格式是引擎私有的,没有版本协商。
- 解析鲁棒性有限。
- 组件恢复依赖 `ComponentFactoryRegistry`,组件名或注册表变化都会影响加载结果。
### 反序列化路径
当前这份文本格式已经覆盖 `GameObject``tag` / `layer` 字段;场景 round-trip 会把它们和层级、Transform、普通组件一起恢复。
`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 做深度优先查询,但仍是线性扫描,不是带索引的完整 tag 系统
- `LoadSceneAsync()` 现在不是异步,更多只是一个名字上预留好的 API
- `LoadScene()` 后的管理器 key 来自文件名,而不是场景内部保存的 `scene=` 名称
- 场景析构不会自动重放完整销毁流程,所以不要把 `~Scene()` 当成完整清理器。
- `FindGameObjectWithTag()` 现在是真实 tag 查询,但仍是线性扫描。
- `Scene::m_active` 当前不会阻止 `Scene::Update()`
- 从文本反序列化恢复后,不会自动补发 `Awake()` / `Start()`
- `Save()` / `Load()` 的错误处理目前比较轻,不提供事务式恢复。
## 实际使用建议
如果你现在基于这套系统开发,比较稳妥的做法通常是:
- 需要 `OnDestroy` 或场景事件时,显式调用对象销毁入口,不要只依赖场景析构
- 需要稳定查场景时,优先用 `CreateScene()` 的名称或加载文件的文件名 stem不要假设一定等于场景内部名字
- 不要把 `LoadSceneAsync()` 当成真正的后台加载接口
- 可以把 `FindGameObjectWithTag()`真实 tag 查询,但不要把它成带项目配置和索引优化的完整 TagManager。
- 运行时对象创建优先用 `CreateGameObject()`,不要手工拼接半初始化对象
- 需要完整保存和恢复时,用 `SerializeToString()` / `DeserializeFromString()`,不要把单对象序列化当场景格式
- 需要生命周期完整对称时,明确区分“新建对象”与“从存档恢复对象”两条流程
- 可以放心`FindGameObjectWithTag()`真实 tag 查询,但不要把它想象成带项目配置和索引优化的完整 TagManager。
## 相关 API