8.7 KiB
GameObject
命名空间: XCEngine::Components
类型: class
头文件: XCEngine/Components/GameObject.h
描述: 场景对象树的基础节点,负责对象身份、父子层级、激活状态、组件容器和生命周期分发。
角色概述
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() 当前只写入:
nameactiveiduuidtransform
它不会负责写出普通组件列表,也不会负责写出父子关系。完整场景序列化是在 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()。
推荐使用方式
- 运行时对象优先通过
Scene::CreateGameObject()创建,而不是直接构造裸对象。 - 操作父子关系优先使用
GameObject::SetParent(),而不是只改TransformComponent::SetParent()。 - 需要保存完整对象树时,优先走
Scene::SerializeToString()/Scene::DeserializeFromString()。 - 运行时动态挂组件时,不要假定引擎已经自动补发
Awake/Start。 - 场景反序列化后如果某些组件依赖
Awake()完成初始化,需要显式检查当前加载路径是否已补齐这一步。
相关方法
- Constructor
- AddComponent
- GetComponent
- GetComponents
- GetComponentInChildren
- GetComponentInParent
- GetComponentsInChildren
- GetTransform
- SetParent
- DetachFromParent
- DetachChildren
- SetActive
- IsActive
- IsActiveInHierarchy
- Find
- FindObjectsOfType
- FindGameObjectsWithTag
- Destroy
- Serialize
- Deserialize