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

10 KiB
Raw Blame History

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_idm_uuidm_name
  • 元数据字段:m_tagm_layer
  • 层级字段:m_parentm_children
  • 激活字段:m_activeSelfm_started
  • 所有权字段:m_scenem_transformm_components

其中最容易和旧文档混淆的是 tag / layer:它们现在已经是独立字段,不再是名字别名或“预留接口”。

Tag 与 Layer 语义

Tag

  • 底层字段是 m_tag
  • 默认值是 "Untagged"
  • SetTag 传入空字符串时也会规范化回 "Untagged"
  • CompareTag 直接比较 m_tag == 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_tsetter 再额外做上界限制”。

为什么 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 时执行,且每个对象最多执行一次
  • UpdateFixedUpdateLateUpdate 只对激活层级中的已启用组件递归分发
  • OnDestroy 会把销毁消息发给普通组件

需要特别注意两条现实边界:

  • TransformComponent 不参与普通组件生命周期遍历
  • 运行时 AddComponent<T>() 只负责挂接,不会自动补发 Awake()Start()OnEnable()

所以如果对象已经进入过 Start(),你之后再动态添加组件,新组件不会被引擎自动“追上进度”。

激活状态与层级传播

GameObject 维护两套不同层面的激活状态:

SetActiveSetParent 都会重新计算“层级有效激活态”。一旦有效激活态发生变化,当前实现会:

  • 对已启用的普通组件发送 OnEnable()OnDisable()
  • 把变化递归传播给子对象

这和商业引擎里把“对象自己是否开启”和“它是否真的在运行层级里生效”拆开的做法一致。

查找语义与全局 Registry

静态查找接口:

当前 registry 主要由两条路径填充:

  • Scene::CreateGameObject()
  • Scene::DeserializeFromString()

因此未加入场景的独立对象默认不会出现在这些查询结果中。另一方面,场景级 Scene::FindGameObjectWithTag 则是沿场景根对象做深度优先遍历,更适合表达“当前场景树里的第一个匹配对象”。

所有权与销毁

  • 场景托管对象由 Scenestd::unique_ptr<GameObject> 持有
  • 普通组件由 GameObjectstd::unique_ptr<Component> 持有
  • TransformComponentm_transform 单独持有并在析构时删除

Destroy 当前有两种路径:

  • 如果对象属于某个场景,则委托给 Scene::DestroyGameObject(this)
  • 如果对象不属于场景,只调用 OnDestroy(),不会释放对象自身内存

因此“独立对象调用 Destroy()”不等于“对象被 delete”。真正释放内存的仍然是拥有它的那一层所有者。

序列化边界

Serialize 只负责单对象基础状态:

  • name
  • tag
  • active
  • layer
  • id
  • uuid
  • transform

它不会保存:

  • 普通组件列表
  • 父子层级
  • 所属场景

完整对象树持久化由 Scene::SerializeToString 负责;对应地,Deserialize 也只恢复单对象基础字段,不会重建组件、父子关系或 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 / CompareTag,不要再把 tag 当作名字别名。
  3. 需要层过滤或脚本暴露时使用 SetLayer,并按 0..31 的轻量层模型组织约定。
  4. 需要完整持久化对象树时使用 Scene::SerializeToString / Scene::DeserializeFromString
  5. 运行时动态挂组件时,不要假设引擎会自动补发 Awake() / Start()

相关方法

相关指南

相关文档