docs: sync gameobject tag layer docs

This commit is contained in:
2026-04-03 15:48:09 +08:00
parent 24a200e126
commit d33520752b
18 changed files with 805 additions and 207 deletions

View File

@@ -0,0 +1,48 @@
# GameObject::CompareTag
判断当前对象的 tag 是否与给定字符串精确匹配。
```cpp
bool CompareTag(const std::string& tag) const;
```
## 行为说明
当前实现等价于:
```cpp
return m_tag == tag;
```
因此它的语义非常明确:
- 比较的是真实 tag 字段
- 比较方式是精确字符串比较
- 大小写敏感
- 不做模糊匹配,也不看对象名称
由于 [SetTag](SetTag.md) 会把空字符串规范化成 `"Untagged"`,在引擎正常路径下,`CompareTag("")` 通常会返回 `false`
## 参数
- `tag` - 要比较的目标 tag。
## 返回值
- `true` - 当前对象 tag 与参数完全相同。
- `false` - 其余情况。
## 为什么推荐用它而不是手写字符串比较
虽然当前实现只是 `m_tag == tag`,但对上层代码来说,`CompareTag()` 仍然更稳妥:
- 语义更接近 Unity脚本和原生更一致
- 未来如果引擎在这里加入规范化、诊断或更快查询路径,调用点不需要改
- API 意图更清楚读文档的人一眼能看出你在做“tag 分类判断”
## 相关文档
- [GameObject](GameObject.md)
- [GetTag](GetTag.md)
- [SetTag](SetTag.md)
- [FindGameObjectsWithTag](FindGameObjectsWithTag.md)

View File

@@ -1,31 +1,62 @@
# GameObject::Deserialize
公开方法,详见头文件声明
从一行分号分隔的 token 流恢复当前对象的基础状态
```cpp
void Deserialize(std::istream& is);
```
该方法声明于 `XCEngine/Components/GameObject.h`,当前页面用于固定 `GameObject` 类目录下的方法级 canonical 路径。
## 行为说明
**参数:**
- `is` - 参数语义详见头文件声明。
当前实现会持续读取 token直到遇到
**返回:** `void` - 无返回值。
- `EOF`
- 换行
- 回车
**示例:**
它能识别的 key 包括:
```cpp
#include <XCEngine/Components/GameObject.h>
- `name`
- `tag`
- `active`
- `layer`
- `id`
- `uuid`
- `transform`
void Example() {
XCEngine::Components::GameObject object;
// 根据上下文补齐参数后调用 GameObject::Deserialize(...)。
(void)object;
}
```
未知 key 会被跳过,因此它对“多写了一些暂时不认识的字段”具备有限容错能力。
## Tag 与 Layer 恢复语义
- `tag` 最终走 [SetTag](SetTag.md),因此空字符串会回退到 `"Untagged"`
- `layer` 最终走 [SetLayer](SetLayer.md),因此上界仍会被限制到 `31`
按当前引擎自己生成的数据格式,这可以稳定 round-trip tag 与 `0..31` 范围内的 layer。
## 当前边界
- 不会创建普通组件
- 不会恢复父子关系
- 不会设置所属 `Scene`
- 不会触发 `Awake()``Start()` 或创建事件
- 只恢复这一个对象的基础状态
完整场景恢复应使用 [Scene::DeserializeFromString](../../Scene/Scene/DeserializeFromString.md)。
## 参数
- `is` - 输入流。
## 返回值
- 无。
## 设计提示
这个接口更像“对象基础状态的底层反序列化原语”,而不是面向最终用户的完整场景加载入口。商业引擎里把这类低层接口和场景容器级加载分开,是为了避免单对象 API 意外承担过多树结构与资源恢复责任。
## 相关文档
- [返回类总览](GameObject.md)
- [返回模块目录](../Components.md)
- [GameObject](GameObject.md)
- [Serialize](Serialize.md)
- [Scene::DeserializeFromString](../../Scene/Scene/DeserializeFromString.md)

View File

@@ -1,31 +1,47 @@
# GameObject::FindGameObjectsWithTag
查找并返回匹配对象。
按 tag 返回当前全局 registry 中的全部匹配对象。
```cpp
static std::vector<GameObject*> FindGameObjectsWithTag(const std::string& tag);
```
该方法声明于 `XCEngine/Components/GameObject.h`,当前页面用于固定 `GameObject` 类目录下的方法级 canonical 路径。
## 行为说明
**参数:**
- `tag` - 参数语义详见头文件声明。
**返回:** `std::vector<GameObject*>` - 返回值语义详见头文件声明。
**示例:**
当前实现会遍历 `GameObject::GetGlobalRegistry()`,并对每个对象执行:
```cpp
#include <XCEngine/Components/GameObject.h>
void Example() {
XCEngine::Components::GameObject object;
// 根据上下文补齐参数后调用 GameObject::FindGameObjectsWithTag(...)。
(void)object;
}
pair.second->CompareTag(tag)
```
所有匹配对象都会被追加到结果数组中。
这意味着:
- 比较的是真实 tag 字段,不是对象名称
- 查询范围是全局 registry而不是“当前某个 Scene 的子树”
- 未加入场景的独立 `GameObject` 默认不会出现在结果里
-`Scene::DeserializeFromString()` 恢复出来并重新注册的对象也会被查到
由于 registry 底层是 `std::unordered_map`,返回顺序当前不构成稳定契约。如果你需要“当前场景树里的第一个匹配对象”,应优先使用 [Scene::FindGameObjectWithTag](../../Scene/Scene/FindGameObjectWithTag.md)。
## 参数
- `tag` - 要精确匹配的 tag 字符串。
## 返回值
- `std::vector<GameObject*>` - 当前所有匹配对象;找不到时返回空数组。
## 适用场景
- 做全局调试扫描
- 脚本或工具侧收集某类对象
- 不要求结果顺序稳定,只关心“有哪些对象匹配”
## 相关文档
- [返回类总览](GameObject.md)
- [返回模块目录](../Components.md)
- [GameObject](GameObject.md)
- [GetTag](GetTag.md)
- [CompareTag](CompareTag.md)
- [Scene::FindGameObjectWithTag](../../Scene/Scene/FindGameObjectWithTag.md)

View File

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

View File

@@ -0,0 +1,35 @@
# GameObject::GetLayer
返回当前对象的 layer 值。
```cpp
uint8_t GetLayer() const;
```
## 行为说明
这个接口直接返回底层 `m_layer` 的当前值。
按当前实现:
- 默认值是 `0`
- 原生 [SetLayer](SetLayer.md) 会把上界限制到 `31`
- 托管 `GameObject.layer` / `Component.layer` 会先把 `int` 显式限制到 `[0, 31]`
- `GameObject::Serialize()``Scene::SerializeToString()` 都会保存这个字段
因此在引擎正常产生的数据路径里,这个值会稳定落在 `0..31`
## 返回值
- `uint8_t` - 当前对象的 layer。
## 常见用途
- 渲染层过滤
- 轻量对象分组
- 向脚本层暴露与 Unity 类似的 `layer` 属性
## 相关文档
- [GameObject](GameObject.md)
- [SetLayer](SetLayer.md)

View File

@@ -0,0 +1,40 @@
# GameObject::GetTag
返回当前对象的 tag 字符串。
```cpp
const std::string& GetTag() const;
```
## 行为说明
这个接口直接返回底层 `m_tag` 的只读引用,不做复制,也不做格式转换。
按当前实现:
- 默认构造出的对象 tag 是 `"Untagged"`
- [SetTag](SetTag.md) 传入空字符串时也会回退到 `"Untagged"`
- 场景反序列化恢复 tag 时同样走 `SetTag()` 逻辑
因此,只要对象来自引擎正常构造、反序列化或 setter 路径,这里通常不会返回空字符串。
## 返回值
- `const std::string&` - 当前对象 tag 的只读引用。
## 使用建议
- 需要判断对象分类时,优先用 [CompareTag](CompareTag.md),不要到处手写 `GetTag() == "Player"`
- 需要显示当前值、做日志输出或写入编辑器面板时,再直接读取 `GetTag()`
## 线程语义
- 返回的是对象内部字符串引用
- 在对象销毁前、且没有并发修改该对象时读取才安全
- 当前实现没有为 tag 读写提供内部锁
## 相关文档
- [GameObject](GameObject.md)
- [SetTag](SetTag.md)
- [CompareTag](CompareTag.md)

View File

@@ -1,31 +1,61 @@
# GameObject::Serialize
公开方法,详见头文件声明
把当前对象的基础状态写成一行分号分隔的 token 流
```cpp
void Serialize(std::ostream& os) const;
```
该方法声明于 `XCEngine/Components/GameObject.h`,当前页面用于固定 `GameObject` 类目录下的方法级 canonical 路径。
## 行为说明
**参数:**
- `os` - 参数语义详见头文件声明。
当前实现按固定顺序写出:
**返回:** `void` - 无返回值。
**示例:**
```cpp
#include <XCEngine/Components/GameObject.h>
void Example() {
XCEngine::Components::GameObject object;
// 根据上下文补齐参数后调用 GameObject::Serialize(...)。
(void)object;
}
```text
name=...;
tag=...;
active=...;
layer=...;
id=...;
uuid=...;
transform=...;
```
其中:
- `tag` 是真实对象 tag而不是名字别名
- `layer` 是当前对象 layer
- `transform` 继续委托给 `TransformComponent::Serialize()`
`tests/Components/test_game_object.cpp` 里的 `Layer_GetSetAndSerializeRoundTrip` 已验证这一行 token 流会保留 tag 和 layer 的 round-trip 结果。
## 当前边界
- 这里只写单个 `GameObject` 的基础状态
- 不写普通组件列表
- 不写父子关系
- 不写所属场景
如果你要保存完整对象树,应使用 [Scene::SerializeToString](../../Scene/Scene/SerializeToString.md)。
另外,这条紧凑 token 流当前不会像 `Scene::SerializeToString()` 那样对 `name` / `tag` 做换行和反斜杠转义;它更适合引擎内部受控 round-trip而不是通用交换格式。
## 参数
- `os` - 输出流。
## 返回值
- 无。
## 设计提示
把“单对象基础状态”和“完整场景树”拆成两层序列化接口,是商业引擎里常见的职责分离方式:
- `GameObject::Serialize()` 负责描述“我自己是谁”
- `Scene::SerializeToString()` 负责描述“整棵树如何保存”
## 相关文档
- [返回类总览](GameObject.md)
- [返回模块目录](../Components.md)
- [GameObject](GameObject.md)
- [Deserialize](Deserialize.md)
- [Scene::SerializeToString](../../Scene/Scene/SerializeToString.md)

View File

@@ -0,0 +1,52 @@
# GameObject::SetLayer
设置当前对象的 layer。
```cpp
void SetLayer(uint8_t layer);
```
## 行为说明
当前 native 实现等价于:
```cpp
m_layer = std::min<uint8_t>(layer, 31u);
```
因此对 C++ 调用者来说,精确语义是:
- 参数类型本身就是 `uint8_t`
- `0..31` 会原样保留
- 大于 `31` 的值会被写成 `31`
## 原生与托管的差异
很多文档会把它简写成“clamp 到 `[0, 31]`”。这对托管调用链是正确的,但对原生头文件还不够精确:
- 原生公开 API 没有负数输入通道,因为参数已经是 `uint8_t`
- 托管 `GameObject.layer` / `Component.layer` 先在 internal call 里把 `int32` 显式限制到 `[0, 31]`
- 然后托管层再调用这个 native setter
所以:
- C++ 视角:这是“`uint8_t` + 上界 clamp”
- C# 视角:这是完整的 `[0, 31]` clamp
## 参数
- `layer` - 目标 layer 值。
## 返回值
- 无。
## 设计提示
把 layer 收敛为 `0..31` 的轻量整数,可以让渲染过滤、对象分类、脚本暴露与场景序列化共享同一套最小稳定模型,而不必提前引入更重的 LayerMask 资产系统。
## 相关文档
- [GameObject](GameObject.md)
- [GetLayer](GetLayer.md)
- [Serialize](Serialize.md)

View File

@@ -0,0 +1,53 @@
# GameObject::SetTag
设置当前对象的 tag。
```cpp
void SetTag(const std::string& tag);
```
## 行为说明
当前实现等价于:
```cpp
m_tag = tag.empty() ? std::string("Untagged") : tag;
```
这意味着:
- 传入空字符串会被规范化为 `"Untagged"`
- 非空字符串会原样写入
- 不会自动改对象名字
- 不会检查这个 tag 是否已在项目里“预先声明”
- 不会维护任何全局 tag 注册表
因此它已经是真实 tag 写入接口,但仍是轻量 Unity 风格实现,而不是完整 TagManager。
## 参数
- `tag` - 新的 tag空字符串会回退到 `"Untagged"`
## 设计提示
把空 tag 统一回落到 `"Untagged"`,能让引擎在编辑器、序列化和脚本桥接层保持一个稳定的“未分类对象”约定,而不是把 `""``null``Untagged` 混成多种语义。
## 与托管脚本的关系
托管 `GameObject.Tag` / `GameObject.tag` / `Component.Tag` / `Component.tag` 最终都会走到这个 native setter。
- C# 侧把 `null` 先归一化成 `string.Empty`
- native 再把空字符串统一回落到 `"Untagged"`
所以原生和托管看到的是同一套 tag 规范化结果。
## 返回值
- 无。
## 相关文档
- [GameObject](GameObject.md)
- [GetTag](GetTag.md)
- [CompareTag](CompareTag.md)
- [Scene::FindGameObjectWithTag](../../Scene/Scene/FindGameObjectWithTag.md)