docs: refactor Components API content
This commit is contained in:
@@ -0,0 +1,266 @@
|
||||
# GameObject-Component Lifecycle And Serialization
|
||||
|
||||
## 这篇指南解决什么问题
|
||||
|
||||
只看 `Components` 模块下的单个 API 页,很容易知道“有哪些函数”,但不容易知道“应该怎么组织对象”“生命周期到底是谁驱动的”“为什么有些对象能被查到、有些不能”“为什么某些组件保存后会丢状态”。
|
||||
|
||||
这篇指南专门回答这些问题。它不是第二套 API 参考,而是帮助你建立当前 `XCEngine::Components` 模块的整体心智模型。
|
||||
|
||||
## 先建立正确心智模型
|
||||
|
||||
当前引擎采用的是 **GameObject + Component + Scene 树** 模型,而不是 ECS。
|
||||
|
||||
可以把它粗略类比成 Unity 的对象体系:
|
||||
|
||||
- `Scene` 像场景容器,拥有对象并驱动更新与序列化。
|
||||
- `GameObject` 像层级树节点,负责身份、父子关系、激活状态和组件容器。
|
||||
- `TransformComponent` 像内建 `Transform`,每个对象必有一个。
|
||||
- 其他组件按职责挂接到对象上,例如 `CameraComponent`、`LightComponent`、`MeshRendererComponent`、`AudioSourceComponent`。
|
||||
|
||||
这种设计的优势是:
|
||||
|
||||
- 对编辑器层级、脚本调用和运行时调试都比较直观。
|
||||
- 对多数游戏开发者来说学习成本低。
|
||||
- 场景保存/加载可以直接围绕对象树展开。
|
||||
|
||||
代价是:
|
||||
|
||||
- 生命周期不再只是“组件自己的事”,而是依赖 `Scene -> GameObject -> Component` 这条链路。
|
||||
- 某些看起来像全局查询的接口,其实只对场景托管对象成立。
|
||||
- 运行时动态加组件时,如果没有自动补发初始化阶段,就会出现“对象在,但逻辑没进入完整生命周期”的情况。
|
||||
|
||||
## 为什么 `TransformComponent` 是内建组件
|
||||
|
||||
商业级游戏引擎里,`Transform` 往往不是普通业务组件,而是对象树的基础设施。当前实现也是这个思路:
|
||||
|
||||
- `GameObject` 构造时立即创建 `TransformComponent`
|
||||
- `AddComponent<TransformComponent>()` 返回已有实例
|
||||
- `RemoveComponent()` 不能移除它
|
||||
|
||||
这样设计的好处是:
|
||||
|
||||
- 所有对象都天然具有空间位置和层级关系
|
||||
- 相机、灯光、网格、音频这些组件都可以稳定依赖 `transform()`
|
||||
- 编辑器和场景序列化不需要处理“这个对象没有变换”的特殊分支
|
||||
|
||||
代价是 `TransformComponent` 不再是完全对称的组件类型。你在上层逻辑里必须把它当成“基础设施组件”,不是“普通可选功能块”。
|
||||
|
||||
## 场景托管对象 vs 独立对象
|
||||
|
||||
这是当前模块最容易被忽略的一条边界。
|
||||
|
||||
### 独立对象
|
||||
|
||||
```cpp
|
||||
XCEngine::Components::GameObject player("Player");
|
||||
```
|
||||
|
||||
这会得到一个可用对象,但它:
|
||||
|
||||
- 只有内建 `Transform`
|
||||
- 默认不在任何 `Scene` 中
|
||||
- 不会自动注册到全局查找表
|
||||
- 不会自动触发 `Awake`
|
||||
|
||||
它适合测试、临时对象或手工控制生命周期的场景。
|
||||
|
||||
### 场景托管对象
|
||||
|
||||
```cpp
|
||||
auto* player = scene.CreateGameObject("Player");
|
||||
```
|
||||
|
||||
按当前实现,这条路径会额外完成:
|
||||
|
||||
- 设置 `m_scene`
|
||||
- 注册到全局 registry
|
||||
- 接入场景根节点或父节点
|
||||
- 调用 `Awake()`
|
||||
|
||||
这才是更接近“真实运行时对象”的创建方式。
|
||||
|
||||
### 为什么这很重要
|
||||
|
||||
因为下列能力都和“是否已被场景托管”直接相关:
|
||||
|
||||
- `GameObject::Find()`
|
||||
- `GameObject::FindObjectsOfType()`
|
||||
- `GameObject::FindGameObjectsWithTag()`
|
||||
- `Component::GetScene()`
|
||||
- 场景级保存/加载
|
||||
|
||||
如果对象只是裸构造出来的,你不能期待这些能力都成立。
|
||||
|
||||
## 当前真实生命周期
|
||||
|
||||
下面这张表描述的是 **当前代码已经实现的行为**,不是理想化设计图。
|
||||
|
||||
| 阶段 | 谁触发 | 当前行为 |
|
||||
|------|------|------|
|
||||
| 构造 `GameObject` | 用户或 `Scene` | 创建对象、分配 ID/UUID、创建内建 `Transform` |
|
||||
| `Scene::CreateGameObject()` | `Scene` | 注册全局表、设置场景、挂到父子层级、调用 `Awake()` |
|
||||
| `Scene::DeserializeFromString()` | `Scene` | 重建对象、Transform、普通组件和父子关系,但当前不会自动补发 `Awake()` |
|
||||
| `GameObject::Awake()` | `Scene` 或用户 | 遍历普通组件并调用 `Awake()` |
|
||||
| `Scene::Update()` | `Scene` | 对激活 root object 调用 `Start()`,之后 `Update()` |
|
||||
| `GameObject::Start()` | `Scene` 或用户 | 只调用一次;仅对激活层级中的已启用普通组件生效 |
|
||||
| `SetEnabled()` / `SetActive()` / `SetParent()` | 用户或内部逻辑 | 根据层级有效激活态触发 `OnEnable()` / `OnDisable()` |
|
||||
| `Destroy()` | 用户 | 场景托管对象走 `Scene::DestroyGameObject()`;独立对象只发 `OnDestroy()`,不自动 delete |
|
||||
|
||||
## 运行时 `AddComponent<T>()` 的现实语义
|
||||
|
||||
很多用户会下意识把它当成 Unity 那种“加上就自动进入生命周期”的接口,但当前实现不是。
|
||||
|
||||
```cpp
|
||||
auto* go = scene.CreateGameObject("Emitter");
|
||||
auto* source = go->AddComponent<XCEngine::Components::AudioSourceComponent>();
|
||||
```
|
||||
|
||||
这里会发生的事情只有:
|
||||
|
||||
- 创建组件实例
|
||||
- 回填 `component->m_gameObject = go`
|
||||
- 放进 `m_components`
|
||||
|
||||
这里 **不会自动发生**:
|
||||
|
||||
- `Awake()`
|
||||
- `Start()`
|
||||
- `OnEnable()`
|
||||
|
||||
为什么这很关键?因为很多组件初始化逻辑都习惯写在这些阶段里。如果你在运行中动态加组件,就需要自己明确何时让它进入可用状态。
|
||||
|
||||
还有一个更容易踩坑的点:如果这个 `GameObject` 之前已经进入过 `Start()`,后续再添加的新组件也不会在未来被自动 `Start()`,因为当前实现只用 `m_started` 记录“这个对象是否已经启动过一次”,而不是“每个组件是否已经启动过”。
|
||||
|
||||
## 激活状态为什么要区分两层
|
||||
|
||||
和 Unity 一样,这里也区分:
|
||||
|
||||
- `activeSelf`
|
||||
- `activeInHierarchy`
|
||||
|
||||
原因很现实:
|
||||
|
||||
- 一个对象自己打开,不代表它真的在运行;只要父对象被禁用,它依然应该整体失活。
|
||||
- 组件的 `OnEnable()` / `OnDisable()` 不能只看自己的 `enabled`,还要看宿主对象是否真的在活跃层级里。
|
||||
|
||||
当前实现里,`Component::SetEnabled()` 就按下面的“有效启用态”处理:
|
||||
|
||||
`component enabled && gameObject active in hierarchy`
|
||||
|
||||
这能避免父节点关闭时组件仍然误以为自己处于活动状态。
|
||||
|
||||
## 为什么序列化要分层
|
||||
|
||||
初看时可能会觉得:既然 `GameObject` 有组件容器,为什么不让 `GameObject::Serialize()` 一次把所有内容都写完?
|
||||
|
||||
当前实现没有这么做,而是分成两层:
|
||||
|
||||
### `GameObject::Serialize()`
|
||||
|
||||
只写对象基础状态:
|
||||
|
||||
- `name`
|
||||
- `active`
|
||||
- `id`
|
||||
- `uuid`
|
||||
- `transform`
|
||||
|
||||
### `Scene::SerializeToString()`
|
||||
|
||||
负责完整场景结构:
|
||||
|
||||
- 所有 root object 递归展开
|
||||
- `parent` 关系
|
||||
- 每个普通组件的类型名和 payload
|
||||
- 反序列化时通过 `ComponentFactoryRegistry` 重建组件
|
||||
|
||||
这种分层的好处是职责清晰:
|
||||
|
||||
- `GameObject` 负责“我是谁,我的基础状态是什么”
|
||||
- `Scene` 负责“我如何把整棵对象树持久化”
|
||||
|
||||
这和商业引擎里把对象局部状态和场景容器职责拆开的思路是一致的。
|
||||
|
||||
但当前加载路径还有一个必须知道的现实差异:`Scene::DeserializeFromString()` 在恢复对象和组件后,没有自动重放 `Awake()`。所以“新建场景对象”和“从场景文本加载出来的对象”在初始化阶段并不完全对称。
|
||||
|
||||
## `ComponentFactoryRegistry` 在这里的作用
|
||||
|
||||
反序列化场景时,文本里只有组件类型名,例如:
|
||||
|
||||
```text
|
||||
component=Camera;projection=0;fov=60;...
|
||||
```
|
||||
|
||||
引擎需要把 `"Camera"` 重新变成具体组件实例。当前实现就是通过 `ComponentFactoryRegistry` 完成这件事。
|
||||
|
||||
它当前预注册了:
|
||||
|
||||
- `Camera`
|
||||
- `Light`
|
||||
- `AudioSource`
|
||||
- `AudioListener`
|
||||
- `MeshFilter`
|
||||
- `MeshRenderer`
|
||||
- `ScriptComponent`
|
||||
|
||||
注意 `Transform` 不在里面,因为 `TransformComponent` 不是靠工厂反序列化创建的,它是 `GameObject` 的内建成员。
|
||||
|
||||
## 你现在必须知道的限制
|
||||
|
||||
### 1. `FindGameObjectsWithTag()` 不是 tag 系统
|
||||
|
||||
当前实现按对象名称匹配,不是独立 tag 字段。
|
||||
|
||||
### 2. `TransformComponent::Find()` 不是局部子树查找
|
||||
|
||||
它会从整个场景的 root objects 开始搜索第一个名字匹配项。
|
||||
|
||||
### 3. `TransformComponent` 的 sibling API 还不完整
|
||||
|
||||
`SetSiblingIndex()`、`SetAsFirstSibling()`、`SetAsLastSibling()` 目前只修改索引字段,没有真正重排父节点子列表。
|
||||
|
||||
### 4. 音频组件当前不参与完整序列化
|
||||
|
||||
`AudioSourceComponent` 和 `AudioListenerComponent` 没有自定义 `Serialize()` / `Deserialize()`,所以场景保存/加载不会持久化它们的运行时参数。
|
||||
|
||||
### 5. 独立对象的 `Destroy()` 不会删除对象本身
|
||||
|
||||
它只调用 `OnDestroy()`。如果对象不是 `Scene` 持有的,内存释放仍然要由真正的所有者负责。
|
||||
|
||||
### 6. 反序列化路径当前不会自动重放 `Awake()`
|
||||
|
||||
如果某些组件把关键初始化逻辑写在 `Awake()`,那么从场景文本恢复后的状态可能和新建对象不同。
|
||||
|
||||
## 推荐工作流
|
||||
|
||||
### 场景内对象
|
||||
|
||||
1. 用 `Scene::CreateGameObject()` 创建对象。
|
||||
2. 用 `GameObject::SetParent()` 组织层级。
|
||||
3. 用 `AddComponent<T>()` 挂功能组件。
|
||||
4. 用 `Scene::Update()` / `FixedUpdate()` / `LateUpdate()` 驱动生命周期。
|
||||
5. 用 `Scene::SerializeToString()` / `DeserializeFromString()` 保存和恢复。
|
||||
|
||||
### 非场景测试对象
|
||||
|
||||
1. 直接构造 `GameObject`。
|
||||
2. 只依赖本地组件访问和 `Transform` 操作。
|
||||
3. 不要假定静态查找、场景序列化或自动生命周期已经可用。
|
||||
|
||||
## 和 Unity 的关系应该怎么理解
|
||||
|
||||
最合理的理解方式是:
|
||||
|
||||
- 它在对象模型上明显借鉴了 Unity。
|
||||
- 它提供了足够熟悉的 `GameObject + Transform + Component` 心智模型。
|
||||
- 但当前实现仍然比 Unity 轻得多,特别是在 tag、运行时加组件生命周期、完整组件序列化和层级顺序控制上。
|
||||
|
||||
把它理解成“Unity 风格、但当前版本能力更收敛的对象系统”是比较准确的。
|
||||
|
||||
## 从这里继续读什么
|
||||
|
||||
- [Components](../../XCEngine/Components/Components.md)
|
||||
- [GameObject](../../XCEngine/Components/GameObject/GameObject.md)
|
||||
- [Component](../../XCEngine/Components/Component/Component.md)
|
||||
- [TransformComponent](../../XCEngine/Components/TransformComponent/TransformComponent.md)
|
||||
- [ComponentFactoryRegistry](../../XCEngine/Components/ComponentFactoryRegistry/ComponentFactoryRegistry.md)
|
||||
Reference in New Issue
Block a user