255 lines
10 KiB
Markdown
255 lines
10 KiB
Markdown
# GameObject
|
||
|
||
**命名空间**: `XCEngine::Components`
|
||
|
||
**类型**: `class`
|
||
|
||
**头文件**: `XCEngine/Components/GameObject.h`
|
||
|
||
**描述**: 当前对象树模型的核心节点,负责身份、层级、激活状态、轻量 `tag/layer` 元数据、组件容器与生命周期分发。
|
||
|
||
## 设计定位
|
||
|
||
`GameObject` 是当前引擎对象模型的中心类型。它的设计明显借鉴了 Unity 的 `GameObject + Transform + Component` 体系,但刻意保持为更轻的实现:
|
||
|
||
- 用对象树而不是 ECS 组织运行时对象,便于编辑器层级、脚本调用和场景保存。
|
||
- 把 `Transform` 作为基础设施内建到每个对象里,避免“对象没有空间变换”的特例分支。
|
||
- 提供熟悉的 `tag` / `layer` 心智模型,但暂时不引入完整的项目级 TagManager、LayerMask 配置器或查询索引。
|
||
|
||
这种设计的好处是学习成本低、编辑器友好、序列化边界清晰;代价则是很多查询仍是线性扫描,生命周期也更依赖 `Scene -> GameObject -> Component` 的调用链。
|
||
|
||
## 当前真实数据模型
|
||
|
||
`GameObject` 当前同时持有以下几类状态:
|
||
|
||
- 身份字段:`m_id`、`m_uuid`、`m_name`
|
||
- 元数据字段:`m_tag`、`m_layer`
|
||
- 层级字段:`m_parent`、`m_children`
|
||
- 激活字段:`m_activeSelf`、`m_started`
|
||
- 所有权字段:`m_scene`、`m_transform`、`m_components`
|
||
|
||
其中最容易和旧文档混淆的是 `tag` / `layer`:它们现在已经是独立字段,不再是名字别名或“预留接口”。
|
||
|
||
## Tag 与 Layer 语义
|
||
|
||
### Tag
|
||
|
||
- 底层字段是 `m_tag`
|
||
- 默认值是 `"Untagged"`
|
||
- [SetTag](SetTag.md) 传入空字符串时也会规范化回 `"Untagged"`
|
||
- [CompareTag](CompareTag.md) 直接比较 `m_tag == tag`
|
||
|
||
因此当前引擎里:
|
||
|
||
- `tag` 不是 `name` 的别名
|
||
- `CompareTag("Player")` 比较的是真实 tag 字段
|
||
- [FindGameObjectsWithTag](FindGameObjectsWithTag.md) 和 [Scene::FindGameObjectWithTag](../../Scene/Scene/FindGameObjectWithTag.md) 都是真正按 tag 查询
|
||
|
||
### Layer
|
||
|
||
- 底层字段是 `m_layer`
|
||
- 默认值是 `0`
|
||
- 原生 [SetLayer](SetLayer.md) 会把上界限制到 `31`
|
||
- 托管 `GameObject.layer` / `Component.layer` 先把 `int` 显式限制到 `[0, 31]`,再回落到 native setter
|
||
|
||
这意味着在“引擎正常产生的数据路径”里,layer 会稳定落在 `0..31`。文档里常说的“clamp 到 `[0, 31]`”,对托管调用链是精确描述;对原生 C++ API 来说,更精确的说法是“参数本身是 `uint8_t`,setter 再额外做上界限制”。
|
||
|
||
## 为什么 Tag / Layer 做成轻量字段
|
||
|
||
当前实现没有引入完整 TagManager 的原因并不是“尚未支持 tag”,而是有意把它们先收敛为轻量对象元数据:
|
||
|
||
- 对编辑器和脚本层来说,独立字段已经足够支撑常见对象分类与查询。
|
||
- 对场景序列化来说,独立字段更容易稳定 round-trip。
|
||
- 对运行时复杂度来说,不必提前引入项目级表、重命名迁移、索引维护和配置资产。
|
||
|
||
因此它更接近“商业引擎里可持续扩展的第一阶段实现”,而不是临时占位符。
|
||
|
||
## 内建 Transform 与组件模型
|
||
|
||
`GameObject` 构造时会立即创建一个 `TransformComponent` 并绑定到 `m_transform`。这带来三个明确语义:
|
||
|
||
- 每个对象天然拥有 `Transform`
|
||
- `AddComponent<TransformComponent>()` 返回已有实例,而不是再造一个
|
||
- `RemoveComponent()` 不允许移除 `Transform`
|
||
|
||
普通组件则保存在 `m_components` 里,所有权由 `std::unique_ptr<Component>` 持有。也正因为 `Transform` 不在 `m_components` 中,很多生命周期与序列化逻辑都只覆盖“普通组件”,不会自动覆盖内建 `Transform`。
|
||
|
||
## 创建路径与对象注册
|
||
|
||
### 直接构造
|
||
|
||
`GameObject go;` 或 `GameObject go("Name");` 会得到一个可用对象,但它:
|
||
|
||
- 拥有内建 `Transform`
|
||
- `m_scene == nullptr`
|
||
- 不会自动注册到全局 registry
|
||
- 不会自动触发 `Awake()`
|
||
|
||
这条路径适合测试、临时对象或完全手工控制生命周期的代码。
|
||
|
||
### 由 `Scene::CreateGameObject()` 创建
|
||
|
||
这是当前推荐的运行时路径。`Scene` 会额外完成:
|
||
|
||
- 接管对象所有权
|
||
- 把对象放入 `GameObject::GetGlobalRegistry()`
|
||
- 设置所属 `Scene`
|
||
- 接入根节点或父子层级
|
||
- 创建后立即调用 `Awake()`
|
||
|
||
### 由 `Scene::DeserializeFromString()` 恢复
|
||
|
||
反序列化也会重建对象并注册到场景和全局 registry,但它不会经过 `CreateGameObject()`,因此不会自动触发创建事件,也不会补发 `Awake()` / `Start()`。
|
||
|
||
## 生命周期语义
|
||
|
||
`GameObject` 自己不是组件,但它是普通组件生命周期的分发者。
|
||
|
||
- [Awake](Awake.md) 遍历普通组件并调用 `Awake()`
|
||
- [Start](Start.md) 只在 `active in hierarchy` 时执行,且每个对象最多执行一次
|
||
- [Update](Update.md)、[FixedUpdate](FixedUpdate.md)、[LateUpdate](LateUpdate.md) 只对激活层级中的已启用组件递归分发
|
||
- [OnDestroy](OnDestroy.md) 会把销毁消息发给普通组件
|
||
|
||
需要特别注意两条现实边界:
|
||
|
||
- `TransformComponent` 不参与普通组件生命周期遍历
|
||
- 运行时 `AddComponent<T>()` 只负责挂接,不会自动补发 `Awake()`、`Start()` 或 `OnEnable()`
|
||
|
||
所以如果对象已经进入过 `Start()`,你之后再动态添加组件,新组件不会被引擎自动“追上进度”。
|
||
|
||
## 激活状态与层级传播
|
||
|
||
`GameObject` 维护两套不同层面的激活状态:
|
||
|
||
- [IsActive](IsActive.md) 对应 `m_activeSelf`
|
||
- [IsActiveInHierarchy](IsActiveInHierarchy.md) 要求对象自己激活,且所有父节点也激活
|
||
|
||
[SetActive](SetActive.md) 与 [SetParent](SetParent.md) 都会重新计算“层级有效激活态”。一旦有效激活态发生变化,当前实现会:
|
||
|
||
- 对已启用的普通组件发送 `OnEnable()` 或 `OnDisable()`
|
||
- 把变化递归传播给子对象
|
||
|
||
这和商业引擎里把“对象自己是否开启”和“它是否真的在运行层级里生效”拆开的做法一致。
|
||
|
||
## 查找语义与全局 Registry
|
||
|
||
静态查找接口:
|
||
|
||
- [Find](Find.md) 按对象名遍历全局 registry
|
||
- [FindObjectsOfType](FindObjectsOfType.md) 返回当前 registry 全部对象
|
||
- [FindGameObjectsWithTag](FindGameObjectsWithTag.md) 通过 `CompareTag()` 过滤 registry
|
||
|
||
当前 registry 主要由两条路径填充:
|
||
|
||
- `Scene::CreateGameObject()`
|
||
- `Scene::DeserializeFromString()`
|
||
|
||
因此未加入场景的独立对象默认不会出现在这些查询结果中。另一方面,场景级 [Scene::FindGameObjectWithTag](../../Scene/Scene/FindGameObjectWithTag.md) 则是沿场景根对象做深度优先遍历,更适合表达“当前场景树里的第一个匹配对象”。
|
||
|
||
## 所有权与销毁
|
||
|
||
- 场景托管对象由 `Scene` 以 `std::unique_ptr<GameObject>` 持有
|
||
- 普通组件由 `GameObject` 以 `std::unique_ptr<Component>` 持有
|
||
- `TransformComponent` 由 `m_transform` 单独持有并在析构时删除
|
||
|
||
[Destroy](Destroy.md) 当前有两种路径:
|
||
|
||
- 如果对象属于某个场景,则委托给 `Scene::DestroyGameObject(this)`
|
||
- 如果对象不属于场景,只调用 `OnDestroy()`,不会释放对象自身内存
|
||
|
||
因此“独立对象调用 `Destroy()`”不等于“对象被 delete”。真正释放内存的仍然是拥有它的那一层所有者。
|
||
|
||
## 序列化边界
|
||
|
||
[Serialize](Serialize.md) 只负责单对象基础状态:
|
||
|
||
- `name`
|
||
- `tag`
|
||
- `active`
|
||
- `layer`
|
||
- `id`
|
||
- `uuid`
|
||
- `transform`
|
||
|
||
它不会保存:
|
||
|
||
- 普通组件列表
|
||
- 父子层级
|
||
- 所属场景
|
||
|
||
完整对象树持久化由 [Scene::SerializeToString](../../Scene/Scene/SerializeToString.md) 负责;对应地,[Deserialize](Deserialize.md) 也只恢复单对象基础字段,不会重建组件、父子关系或 `Scene` 归属。
|
||
|
||
## 托管脚本桥接
|
||
|
||
Mono 运行时当前已经把这些元数据直接暴露给 C#:
|
||
|
||
- `GameObject.Tag` / `GameObject.tag`
|
||
- `GameObject.Layer` / `GameObject.layer`
|
||
- `Component.Tag` / `Component.tag`
|
||
- `Component.Layer` / `Component.layer`
|
||
- `CompareTag(...)`
|
||
|
||
这些接口不是副本,而是直接读写同一份原生字段。`tests/scripting/test_mono_script_runtime.cpp` 中的 `GameObjectTagAndLayerApiExposeUnityStylePropertiesAndCompareTag` 明确验证了:
|
||
|
||
- 托管脚本能读到原生已有的 tag / layer
|
||
- 托管脚本写回 `"Player"` 和 `31` 后,原生对象会立即更新
|
||
- 场景级 `FindGameObjectWithTag("Player")` 会立刻看到更新结果
|
||
|
||
## 当前实现限制
|
||
|
||
- tag / layer 仍是轻量字段,没有项目级定义表、mask 工具或索引查询
|
||
- 全局静态查找只对已注册对象成立
|
||
- 运行时新增组件不会自动补发初始化生命周期
|
||
- `Destroy()` 对独立对象不会释放自身内存
|
||
- `Transform` 不参与普通组件生命周期遍历
|
||
- `Scene::DeserializeFromString()` 恢复出的对象不会自动补发 `Awake()`
|
||
|
||
## 推荐使用方式
|
||
|
||
1. 运行时对象优先通过 `Scene::CreateGameObject()` 创建。
|
||
2. 需要对象分类时使用真实的 [SetTag](SetTag.md) / [CompareTag](CompareTag.md),不要再把 tag 当作名字别名。
|
||
3. 需要层过滤或脚本暴露时使用 [SetLayer](SetLayer.md),并按 `0..31` 的轻量层模型组织约定。
|
||
4. 需要完整持久化对象树时使用 [Scene::SerializeToString](../../Scene/Scene/SerializeToString.md) / [Scene::DeserializeFromString](../../Scene/Scene/DeserializeFromString.md)。
|
||
5. 运行时动态挂组件时,不要假设引擎会自动补发 `Awake()` / `Start()`。
|
||
|
||
## 相关方法
|
||
|
||
- [Constructor](Constructor.md)
|
||
- [GetName](GetName.md)
|
||
- [SetName](SetName.md)
|
||
- [GetTag](GetTag.md)
|
||
- [SetTag](SetTag.md)
|
||
- [CompareTag](CompareTag.md)
|
||
- [GetLayer](GetLayer.md)
|
||
- [SetLayer](SetLayer.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)
|
||
|
||
## 相关文档
|
||
|
||
- [当前模块](../Components.md)
|
||
- [Component](../Component/Component.md)
|
||
- [TransformComponent](../TransformComponent/TransformComponent.md)
|
||
- [Scene](../../Scene/Scene/Scene.md)
|