10 KiB
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 传入空字符串时也会规范化回
"Untagged" - CompareTag 直接比较
m_tag == tag
因此当前引擎里:
tag不是name的别名CompareTag("Player")比较的是真实 tag 字段- FindGameObjectsWithTag 和 Scene::FindGameObjectWithTag 都是真正按 tag 查询
Layer
- 底层字段是
m_layer - 默认值是
0 - 原生 SetLayer 会把上界限制到
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() - Start 只在
active in hierarchy时执行,且每个对象最多执行一次 - Update、FixedUpdate、LateUpdate 只对激活层级中的已启用组件递归分发
- OnDestroy 会把销毁消息发给普通组件
需要特别注意两条现实边界:
TransformComponent不参与普通组件生命周期遍历- 运行时
AddComponent<T>()只负责挂接,不会自动补发Awake()、Start()或OnEnable()
所以如果对象已经进入过 Start(),你之后再动态添加组件,新组件不会被引擎自动“追上进度”。
激活状态与层级传播
GameObject 维护两套不同层面的激活状态:
- IsActive 对应
m_activeSelf - IsActiveInHierarchy 要求对象自己激活,且所有父节点也激活
SetActive 与 SetParent 都会重新计算“层级有效激活态”。一旦有效激活态发生变化,当前实现会:
- 对已启用的普通组件发送
OnEnable()或OnDisable() - 把变化递归传播给子对象
这和商业引擎里把“对象自己是否开启”和“它是否真的在运行层级里生效”拆开的做法一致。
查找语义与全局 Registry
静态查找接口:
- Find 按对象名遍历全局 registry
- FindObjectsOfType 返回当前 registry 全部对象
- FindGameObjectsWithTag 通过
CompareTag()过滤 registry
当前 registry 主要由两条路径填充:
Scene::CreateGameObject()Scene::DeserializeFromString()
因此未加入场景的独立对象默认不会出现在这些查询结果中。另一方面,场景级 Scene::FindGameObjectWithTag 则是沿场景根对象做深度优先遍历,更适合表达“当前场景树里的第一个匹配对象”。
所有权与销毁
- 场景托管对象由
Scene以std::unique_ptr<GameObject>持有 - 普通组件由
GameObject以std::unique_ptr<Component>持有 TransformComponent由m_transform单独持有并在析构时删除
Destroy 当前有两种路径:
- 如果对象属于某个场景,则委托给
Scene::DestroyGameObject(this) - 如果对象不属于场景,只调用
OnDestroy(),不会释放对象自身内存
因此“独立对象调用 Destroy()”不等于“对象被 delete”。真正释放内存的仍然是拥有它的那一层所有者。
序列化边界
Serialize 只负责单对象基础状态:
nametagactivelayeriduuidtransform
它不会保存:
- 普通组件列表
- 父子层级
- 所属场景
完整对象树持久化由 Scene::SerializeToString 负责;对应地,Deserialize 也只恢复单对象基础字段,不会重建组件、父子关系或 Scene 归属。
托管脚本桥接
Mono 运行时当前已经把这些元数据直接暴露给 C#:
GameObject.Tag/GameObject.tagGameObject.Layer/GameObject.layerComponent.Tag/Component.tagComponent.Layer/Component.layerCompareTag(...)
这些接口不是副本,而是直接读写同一份原生字段。tests/scripting/test_mono_script_runtime.cpp 中的 GameObjectTagAndLayerApiExposeUnityStylePropertiesAndCompareTag 明确验证了:
- 托管脚本能读到原生已有的 tag / layer
- 托管脚本写回
"Player"和31后,原生对象会立即更新 - 场景级
FindGameObjectWithTag("Player")会立刻看到更新结果
当前实现限制
- tag / layer 仍是轻量字段,没有项目级定义表、mask 工具或索引查询
- 全局静态查找只对已注册对象成立
- 运行时新增组件不会自动补发初始化生命周期
Destroy()对独立对象不会释放自身内存Transform不参与普通组件生命周期遍历Scene::DeserializeFromString()恢复出的对象不会自动补发Awake()
推荐使用方式
- 运行时对象优先通过
Scene::CreateGameObject()创建。 - 需要对象分类时使用真实的 SetTag / CompareTag,不要再把 tag 当作名字别名。
- 需要层过滤或脚本暴露时使用 SetLayer,并按
0..31的轻量层模型组织约定。 - 需要完整持久化对象树时使用 Scene::SerializeToString / Scene::DeserializeFromString。
- 运行时动态挂组件时,不要假设引擎会自动补发
Awake()/Start()。
相关方法
- Constructor
- GetName
- SetName
- GetTag
- SetTag
- CompareTag
- GetLayer
- SetLayer
- AddComponent
- GetComponent
- GetComponents
- GetComponentInChildren
- GetComponentInParent
- GetComponentsInChildren
- GetTransform
- SetParent
- DetachFromParent
- DetachChildren
- SetActive
- IsActive
- IsActiveInHierarchy
- Find
- FindObjectsOfType
- FindGameObjectsWithTag
- Destroy
- Serialize
- Deserialize