docs: sync api and planning docs
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user