Files
XCEngine/docs/api/XCEngine/Components/GameObject/GameObject.md

202 lines
8.7 KiB
Markdown
Raw Normal View History

2026-03-26 16:45:24 +08:00
# GameObject
**命名空间**: `XCEngine::Components`
**类型**: `class`
**头文件**: `XCEngine/Components/GameObject.h`
2026-03-27 16:15:00 +08:00
**描述**: 场景对象树的基础节点,负责对象身份、父子层级、激活状态、组件容器和生命周期分发。
## 角色概述
`GameObject` 是当前引擎对象模型的中心类型。你可以把它理解成一个“可挂组件的层级节点”:
- 它自身保存 `name``id``uuid`、active 状态、父子关系和所属 `Scene`
- 它始终自带一个 `TransformComponent`
- 它通过模板 `AddComponent<T>()`/`GetComponent<T>()` 组织附加组件。
- 它负责把生命周期回调分发给已挂接组件。
这个设计明显接近 Unity 的 `GameObject` 模式,优点是对象树和组件树可以直接映射到编辑器层级与脚本使用习惯;代价是很多行为依赖“对象是否由 `Scene` 托管”,而不是只看对象本身。
## 内建 Transform 语义
`GameObject` 构造函数会直接 `new TransformComponent()`,并把该组件绑定为内建 `m_transform`。这带来几个重要结果:
- 每个 `GameObject` 天生就有 `TransformComponent`,不需要也不应该自己创建。
- `AddComponent<TransformComponent>()` 不会生成第二个 `Transform`,而是直接返回内建的那个实例。
- `RemoveComponent()` 明确禁止移除 `Transform`
如果你把这个模块当成 Unity 风格对象树来理解,这一行为是合理的;但它与“所有组件都等价可增删”的纯组合系统不同,文档中必须明确区分。
## 场景托管与独立对象
`GameObject` 有两种常见存在方式:
### 1. 独立构造
直接调用 `GameObject go;``GameObject go("Name");`
- 会创建对象和内建 `Transform`
- 不会自动注册到全局查找表。
- `m_scene` 为空。
- 不会自动调用 `Awake()`
这类对象适合单元测试或临时对象操作,但不要把它和“已经加入场景”的对象等同。
### 2. 由 `Scene::CreateGameObject()` 创建
这是当前运行时的推荐路径。
- `Scene` 会把对象放进场景拥有的 `unique_ptr` 容器。
- 对象会注册到全局 registry。
- `m_scene` 被设置为所属场景。
- 如果指定父对象,会接入场景层级。
- 创建结束后会立即调用 `Awake()`
因此,静态查找接口和完整生命周期语义,本质上都更偏向“场景托管对象”。
## 生命周期
`GameObject` 自己不派生自 `Component`,但它是组件生命周期的分发器。
- `Awake()` 会遍历普通组件并调用对应钩子。
- `Start()` 只在对象处于 `active in hierarchy` 时执行;首次执行后会把 `m_started` 置为 true之后不再重复调用。
- `Update()``FixedUpdate()``LateUpdate()` 只在对象处于激活层级时向已启用组件分发,并递归处理子对象。
- `OnDestroy()` 会把销毁消息发给普通组件。
注意两个当前实现特征:
- `TransformComponent` 不在 `m_components` 容器中,因此这些遍历不会覆盖 `Transform`
- `AddComponent<T>()` 只是创建并挂接组件,不会自动调用 `Awake()``Start()``OnEnable()`
- 如果 `GameObject` 已经进入过 `Start()` 阶段,后续再动态添加组件时,新的组件也不会在未来被自动补发 `Start()`,因为 `m_started` 已经锁定为 true。
这意味着运行时动态加组件时,当前行为比 Unity 更原始,使用者需要自己控制初始化时机。
## 激活状态与层级传播
`GameObject` 同时维护:
- `m_activeSelf`:对象自身开关,对应 `IsActive()`
- 层级有效激活态:父链都激活时才为 true对应 `IsActiveInHierarchy()`
`SetActive()``SetParent()` 都会计算层级激活态是否发生变化;如果发生变化,会:
- 给当前对象上已启用组件发送 `OnEnable()``OnDisable()`
- 递归向子对象传播
这一行为已经由 `tests/Components/test_game_object.cpp``tests/Components/test_component.cpp` 覆盖。
## 查找与全局注册表
静态接口:
- `Find(const std::string&)`
- `FindObjectsOfType()`
- `FindGameObjectsWithTag(const std::string&)`
它们都依赖 `GameObject::GetGlobalRegistry()`。按当前实现,这个 registry 主要在以下路径填充:
- `Scene::CreateGameObject()`
- `Scene::DeserializeFromString()`
普通构造出来但未加入场景的对象,不会自动出现在这些查找结果里。
另外,`FindGameObjectsWithTag()` 当前实际上比较的是对象名称,而不是独立的 tag 字段。这是一个重要的实现限制,文档和上层调用都不应假定已经存在真正的 tag 系统。
## 所有权与销毁
### 对象所有权
- 场景托管对象由 `Scene` 通过 `std::unique_ptr<GameObject>` 持有。
- 普通组件由 `GameObject` 通过 `std::unique_ptr<Component>` 持有。
- `TransformComponent` 单独由裸指针 `m_transform` 持有,并在析构中 `delete`
### `Destroy()` 的当前行为
- 如果对象属于某个 `Scene``Destroy()` 会委托给 `Scene::DestroyGameObject(this)`
- 如果对象不属于场景,`Destroy()` 只会调用 `OnDestroy()`,不会 `delete this`
这和很多用户的直觉并不完全一致。对独立对象来说,`Destroy()` 更像“发送销毁事件”,不是“释放对象内存”。
另外,析构函数本身不会主动调用 `OnDestroy()`;它只会删除内建 `Transform` 并清空组件容器。所以不要依赖析构语义去替代显式销毁回调。
## 序列化边界
`GameObject::Serialize()` 当前只写入:
- `name`
- `active`
- `id`
- `uuid`
- `transform`
它不会负责写出普通组件列表,也不会负责写出父子关系。完整场景序列化是在 `Scene::SerializeToString()` 中递归完成的,那里才会:
- 输出 `parent`
- 遍历普通组件并写入 `component=<type>;payload`
- 递归所有子对象
因此,单独调用 `GameObject::Serialize()` 更适合基础对象状态转储,而不是完整 prefab/scene 快照。
还要注意一个当前版本的生命周期差异:`Scene::DeserializeFromString()` 在重建对象和组件后,并不会自动调用 `Awake()`。也就是说,场景加载恢复出的对象和通过 `Scene::CreateGameObject()` 新建出来的对象,在初始化阶段语义上并不完全一致。
## 线程语义
- 当前实现没有内部加锁。
- 父子层级、组件容器和全局 registry 都不是线程安全容器。
- 默认应按主线程场景更新路径使用。
## 当前实现限制
- 没有真正的 tag 系统,`FindGameObjectsWithTag()` 只是按名称匹配。
- `AddComponent<T>()` 不会自动驱动新组件进入完整生命周期。
- 已经 `Start()` 过的对象在运行时新增组件后,新组件不会被自动 `Start()`
- 静态查找接口只对已注册对象生效,独立构造对象默认不可见。
- `Destroy()` 对独立对象不会释放自身内存。
- `Transform` 不会被普通组件遍历逻辑自动包含在生命周期分发中。
- 通过 `Scene::DeserializeFromString()` 恢复的对象当前不会自动补发 `Awake()`
## 推荐使用方式
1. 运行时对象优先通过 `Scene::CreateGameObject()` 创建,而不是直接构造裸对象。
2. 操作父子关系优先使用 `GameObject::SetParent()`,而不是只改 `TransformComponent::SetParent()`
3. 需要保存完整对象树时,优先走 `Scene::SerializeToString()` / `Scene::DeserializeFromString()`
4. 运行时动态挂组件时,不要假定引擎已经自动补发 `Awake`/`Start`
5. 场景反序列化后如果某些组件依赖 `Awake()` 完成初始化,需要显式检查当前加载路径是否已补齐这一步。
## 相关方法
- [Constructor](Constructor.md)
- [AddComponent](AddComponent.md)
- [GetComponent](GetComponent.md)
- [GetComponents](GetComponents.md)
- [GetComponentInChildren](GetComponentInChildren.md)
- [GetComponentInParent](GetComponentInParent.md)
- [GetComponentsInChildren](GetComponentsInChildren.md)
- [GetTransform](GetTransform.md)
- [SetParent](SetParent.md)
- [DetachFromParent](DetachFromParent.md)
- [DetachChildren](DetachChildren.md)
- [SetActive](SetActive.md)
- [IsActive](IsActive.md)
- [IsActiveInHierarchy](IsActiveInHierarchy.md)
- [Find](Find.md)
- [FindObjectsOfType](FindObjectsOfType.md)
- [FindGameObjectsWithTag](FindGameObjectsWithTag.md)
- [Destroy](Destroy.md)
- [Serialize](Serialize.md)
- [Deserialize](Deserialize.md)
## 相关指南
- [GameObject-Component Lifecycle And Serialization](../../../_guides/Components/GameObject-Component-Lifecycle-And-Serialization.md)
2026-03-26 16:45:24 +08:00
## 相关文档
2026-03-27 16:15:00 +08:00
- [当前模块](../Components.md)
- [Component](../Component/Component.md)
- [TransformComponent](../TransformComponent/TransformComponent.md)
- [Scene](../../Scene/Scene/Scene.md)
- [API 总索引](../../../main.md)