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

8.7 KiB
Raw Blame History

GameObject

命名空间: XCEngine::Components

类型: class

头文件: XCEngine/Components/GameObject.h

描述: 场景对象树的基础节点,负责对象身份、父子层级、激活状态、组件容器和生命周期分发。

角色概述

GameObject 是当前引擎对象模型的中心类型。你可以把它理解成一个“可挂组件的层级节点”:

  • 它自身保存 nameiduuid、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.cpptests/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() 的当前行为

  • 如果对象属于某个 SceneDestroy() 会委托给 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() 完成初始化,需要显式检查当前加载路径是否已补齐这一步。

相关方法

相关指南

相关文档