From d33520752b2da6f9c4d3591f7f2c0f45bda826b5 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Fri, 3 Apr 2026 15:48:09 +0800 Subject: [PATCH] docs: sync gameobject tag layer docs --- .../Components/GameObject/CompareTag.md | 48 ++++ .../Components/GameObject/Deserialize.md | 63 +++-- .../GameObject/FindGameObjectsWithTag.md | 50 ++-- .../Components/GameObject/GameObject.md | 239 +++++++++++------- .../Components/GameObject/GetLayer.md | 35 +++ .../XCEngine/Components/GameObject/GetTag.md | 40 +++ .../Components/GameObject/Serialize.md | 66 +++-- .../Components/GameObject/SetLayer.md | 52 ++++ .../XCEngine/Components/GameObject/SetTag.md | 53 ++++ .../Scene/Scene/DeserializeFromString.md | 42 +-- .../Scene/Scene/FindGameObjectWithTag.md | 17 +- docs/api/XCEngine/Scene/Scene/Scene.md | 57 +++-- .../XCEngine/Scene/Scene/SerializeToString.md | 41 ++- .../MonoScriptRuntime/DestroyManagedObject.md | 94 +++++++ .../MonoScriptRuntime/MonoScriptRuntime.md | 73 +++++- .../TryGetAvailableScriptClasses.md | 16 ++ ...t-Component-Lifecycle-And-Serialization.md | 20 +- .../Scene-Lifecycle-And-Serialization.md | 6 +- 18 files changed, 805 insertions(+), 207 deletions(-) create mode 100644 docs/api/XCEngine/Components/GameObject/CompareTag.md create mode 100644 docs/api/XCEngine/Components/GameObject/GetLayer.md create mode 100644 docs/api/XCEngine/Components/GameObject/GetTag.md create mode 100644 docs/api/XCEngine/Components/GameObject/SetLayer.md create mode 100644 docs/api/XCEngine/Components/GameObject/SetTag.md create mode 100644 docs/api/XCEngine/Scripting/Mono/MonoScriptRuntime/DestroyManagedObject.md diff --git a/docs/api/XCEngine/Components/GameObject/CompareTag.md b/docs/api/XCEngine/Components/GameObject/CompareTag.md new file mode 100644 index 00000000..0922504c --- /dev/null +++ b/docs/api/XCEngine/Components/GameObject/CompareTag.md @@ -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) diff --git a/docs/api/XCEngine/Components/GameObject/Deserialize.md b/docs/api/XCEngine/Components/GameObject/Deserialize.md index bcd6f835..7b750ed9 100644 --- a/docs/api/XCEngine/Components/GameObject/Deserialize.md +++ b/docs/api/XCEngine/Components/GameObject/Deserialize.md @@ -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 +- `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) diff --git a/docs/api/XCEngine/Components/GameObject/FindGameObjectsWithTag.md b/docs/api/XCEngine/Components/GameObject/FindGameObjectsWithTag.md index c74b59c5..ccbcbf9e 100644 --- a/docs/api/XCEngine/Components/GameObject/FindGameObjectsWithTag.md +++ b/docs/api/XCEngine/Components/GameObject/FindGameObjectsWithTag.md @@ -1,31 +1,47 @@ # GameObject::FindGameObjectsWithTag -查找并返回匹配对象。 +按 tag 返回当前全局 registry 中的全部匹配对象。 ```cpp static std::vector FindGameObjectsWithTag(const std::string& tag); ``` -该方法声明于 `XCEngine/Components/GameObject.h`,当前页面用于固定 `GameObject` 类目录下的方法级 canonical 路径。 +## 行为说明 -**参数:** -- `tag` - 参数语义详见头文件声明。 - -**返回:** `std::vector` - 返回值语义详见头文件声明。 - -**示例:** +当前实现会遍历 `GameObject::GetGlobalRegistry()`,并对每个对象执行: ```cpp -#include - -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.md) -- [返回模块目录](../Components.md) +- [GameObject](GameObject.md) +- [GetTag](GetTag.md) +- [CompareTag](CompareTag.md) +- [Scene::FindGameObjectWithTag](../../Scene/Scene/FindGameObjectWithTag.md) diff --git a/docs/api/XCEngine/Components/GameObject/GameObject.md b/docs/api/XCEngine/Components/GameObject/GameObject.md index 26c0b44c..c4c41399 100644 --- a/docs/api/XCEngine/Components/GameObject/GameObject.md +++ b/docs/api/XCEngine/Components/GameObject/GameObject.md @@ -6,168 +6,222 @@ **头文件**: `XCEngine/Components/GameObject.h` -**描述**: 场景对象树的基础节点,负责对象身份、父子层级、激活状态、组件容器和生命周期分发。 +**描述**: 当前对象树模型的核心节点,负责身份、层级、激活状态、轻量 `tag/layer` 元数据、组件容器与生命周期分发。 -## 角色概述 +## 设计定位 -`GameObject` 是当前引擎对象模型的中心类型。你可以把它理解成一个“可挂组件的层级节点”: +`GameObject` 是当前引擎对象模型的中心类型。它的设计明显借鉴了 Unity 的 `GameObject + Transform + Component` 体系,但刻意保持为更轻的实现: -- 它自身保存 `name`、`id`、`uuid`、active 状态、父子关系和所属 `Scene`。 -- 它始终自带一个 `TransformComponent`。 -- 它通过模板 `AddComponent()`/`GetComponent()` 组织附加组件。 -- 它负责把生命周期回调分发给已挂接组件。 +- 用对象树而不是 ECS 组织运行时对象,便于编辑器层级、脚本调用和场景保存。 +- 把 `Transform` 作为基础设施内建到每个对象里,避免“对象没有空间变换”的特例分支。 +- 提供熟悉的 `tag` / `layer` 心智模型,但暂时不引入完整的项目级 TagManager、LayerMask 配置器或查询索引。 -这个设计明显接近 Unity 的 `GameObject` 模式,优点是对象树和组件树可以直接映射到编辑器层级与脚本使用习惯;代价是很多行为依赖“对象是否由 `Scene` 托管”,而不是只看对象本身。 +这种设计的好处是学习成本低、编辑器友好、序列化边界清晰;代价则是很多查询仍是线性扫描,生命周期也更依赖 `Scene -> GameObject -> Component` 的调用链。 -## 内建 Transform 语义 +## 当前真实数据模型 -`GameObject` 构造函数会直接 `new TransformComponent()`,并把该组件绑定为内建 `m_transform`。这带来几个重要结果: +`GameObject` 当前同时持有以下几类状态: -- 每个 `GameObject` 天生就有 `TransformComponent`,不需要也不应该自己创建。 -- `AddComponent()` 不会生成第二个 `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()` 只是创建并挂接组件,不会自动调用 `Awake()`、`Start()` 或 `OnEnable()`。 -- 如果 `GameObject` 已经进入过 `Start()` 阶段,后续再动态添加组件时,新的组件也不会在未来被自动补发 `Start()`,因为 `m_started` 已经锁定为 true。 +- 每个对象天然拥有 `Transform` +- `AddComponent()` 返回已有实例,而不是再造一个 +- `RemoveComponent()` 不允许移除 `Transform` -这意味着运行时动态加组件时,当前行为比 Unity 更原始,使用者需要自己控制初始化时机。 +普通组件则保存在 `m_components` 里,所有权由 `std::unique_ptr` 持有。也正因为 `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()` 只负责挂接,不会自动补发 `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` 以 `std::unique_ptr` 持有 +- `TransformComponent` 由 `m_transform` 单独持有并在析构时删除 -- 场景托管对象由 `Scene` 通过 `std::unique_ptr` 持有。 -- 普通组件由 `GameObject` 通过 `std::unique_ptr` 持有。 -- `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=;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()` 不会自动驱动新组件进入完整生命周期。 -- 已经 `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) diff --git a/docs/api/XCEngine/Components/GameObject/GetLayer.md b/docs/api/XCEngine/Components/GameObject/GetLayer.md new file mode 100644 index 00000000..3e981b33 --- /dev/null +++ b/docs/api/XCEngine/Components/GameObject/GetLayer.md @@ -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) diff --git a/docs/api/XCEngine/Components/GameObject/GetTag.md b/docs/api/XCEngine/Components/GameObject/GetTag.md new file mode 100644 index 00000000..b2b864d1 --- /dev/null +++ b/docs/api/XCEngine/Components/GameObject/GetTag.md @@ -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) diff --git a/docs/api/XCEngine/Components/GameObject/Serialize.md b/docs/api/XCEngine/Components/GameObject/Serialize.md index cb460a75..81a5d4d7 100644 --- a/docs/api/XCEngine/Components/GameObject/Serialize.md +++ b/docs/api/XCEngine/Components/GameObject/Serialize.md @@ -1,31 +1,61 @@ # GameObject::Serialize -公开方法,详见头文件声明。 +把当前对象的基础状态写成一行分号分隔的 token 流。 ```cpp void Serialize(std::ostream& os) const; ``` -该方法声明于 `XCEngine/Components/GameObject.h`,当前页面用于固定 `GameObject` 类目录下的方法级 canonical 路径。 +## 行为说明 -**参数:** -- `os` - 参数语义详见头文件声明。 +当前实现按固定顺序写出: -**返回:** `void` - 无返回值。 - -**示例:** - -```cpp -#include - -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) diff --git a/docs/api/XCEngine/Components/GameObject/SetLayer.md b/docs/api/XCEngine/Components/GameObject/SetLayer.md new file mode 100644 index 00000000..edd69977 --- /dev/null +++ b/docs/api/XCEngine/Components/GameObject/SetLayer.md @@ -0,0 +1,52 @@ +# GameObject::SetLayer + +设置当前对象的 layer。 + +```cpp +void SetLayer(uint8_t layer); +``` + +## 行为说明 + +当前 native 实现等价于: + +```cpp +m_layer = std::min(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) diff --git a/docs/api/XCEngine/Components/GameObject/SetTag.md b/docs/api/XCEngine/Components/GameObject/SetTag.md new file mode 100644 index 00000000..cc422df4 --- /dev/null +++ b/docs/api/XCEngine/Components/GameObject/SetTag.md @@ -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) diff --git a/docs/api/XCEngine/Scene/Scene/DeserializeFromString.md b/docs/api/XCEngine/Scene/Scene/DeserializeFromString.md index 6f047ff7..21e565de 100644 --- a/docs/api/XCEngine/Scene/Scene/DeserializeFromString.md +++ b/docs/api/XCEngine/Scene/Scene/DeserializeFromString.md @@ -1,6 +1,6 @@ # Scene::DeserializeFromString -从字符串重建当前场景内容。 +从自定义场景文本重建当前场景内容。 ```cpp void DeserializeFromString(const std::string& data); @@ -8,12 +8,32 @@ void DeserializeFromString(const std::string& data); ## 行为说明 -当前实现会先清空 `m_gameObjects`、`m_rootGameObjects` 和 `m_gameObjectIDs`,然后解析自定义文本格式,分两阶段重建场景: +当前实现会先清空: -1. 先读取所有 `gameobject_begin` / `gameobject_end` 块,收集待创建对象、父子关系、Transform 数据和组件 payload。 -2. 再创建所有 `GameObject` 实例,恢复 ID、UUID、active、自定义组件和层级关系。 +- `m_gameObjects` +- `m_rootGameObjects` +- `m_gameObjectIDs` +- 全局 registry 里属于旧场景的对象 -解析完成后,如果文件中的最大对象 ID 大于等于 `GameObject::s_nextID`,当前实现还会把全局自增 ID 推进到 `maxId + 1`。 +随后分两阶段重建: + +1. 先解析所有对象块,收集 `id`、`uuid`、`name`、`tag`、`active`、`layer`、`parent`、`transform` 和组件 payload +2. 再创建全部 `GameObject`,恢复基础字段、组件和父子关系 + +## tag 与 layer 恢复语义 + +- `tag` 会通过 `GameObject::SetTag()` 写回,因此空字符串会回退到 `"Untagged"` +- `layer` 在解析阶段会先限制到 `31`,再通过 `SetLayer()` 写回 + +因此按当前实现,场景 round-trip 会保留正常的 tag/layer 语义。 + +## 当前实现限制 + +- 旧对象清空时不会逐个调用 [DestroyGameObject](DestroyGameObject.md),因此不会触发销毁事件,也不会对组件调用 `OnDestroy()` +- 新对象不会触发 [OnGameObjectCreated](OnGameObjectCreated.md),也不会调用 `Awake()` / `Start()` +- 组件创建依赖 `ComponentFactoryRegistry` +- 对损坏数字字段抛出的异常当前不会在这里捕获 +- 如果父对象 ID 缺失或找不到,对象会回退为根对象 ## 参数 @@ -23,16 +43,8 @@ void DeserializeFromString(const std::string& data); - 无。 -## 当前实现限制 - -- 旧对象被清空时不会逐个调用 [DestroyGameObject](DestroyGameObject.md),因此不会触发销毁事件,也不会对组件调用 `OnDestroy()`。 -- 新对象是直接构建出来的,不会触发 [OnGameObjectCreated](OnGameObjectCreated.md),也不会调用 `Awake()` / `Start()`。 -- 组件创建依赖 `ComponentFactoryRegistry`;未注册的组件类型会被跳过。 -- 对于损坏的数字字段,`std::stoull` 抛出的异常当前不会在这里捕获。 -- 如果父对象 ID 缺失或找不到,对象会被回退为根对象。 - ## 相关文档 -- [返回类型总览](Scene.md) +- [Scene](Scene.md) - [SerializeToString](SerializeToString.md) -- [Load](Load.md) +- [GameObject::Deserialize](../../Components/GameObject/Deserialize.md) diff --git a/docs/api/XCEngine/Scene/Scene/FindGameObjectWithTag.md b/docs/api/XCEngine/Scene/Scene/FindGameObjectWithTag.md index e5165a75..40a1689c 100644 --- a/docs/api/XCEngine/Scene/Scene/FindGameObjectWithTag.md +++ b/docs/api/XCEngine/Scene/Scene/FindGameObjectWithTag.md @@ -1,6 +1,6 @@ # Scene::FindGameObjectWithTag -查找带指定“tag”的对象。 +按 tag 递归查找场景中的第一个匹配对象。 ```cpp GameObject* FindGameObjectWithTag(const std::string& tag) const; @@ -8,17 +8,17 @@ GameObject* FindGameObjectWithTag(const std::string& tag) const; ## 行为说明 -接口名称里写的是 tag,但当前实现实际比较的是: +当前实现从根对象开始做深度优先查找: -```cpp -go->GetName() == tag -``` +1. 先检查每个根对象自身的 `CompareTag(tag)` +2. 再递归检查子对象 +3. 找到第一个匹配项后立即返回 -也就是说,它现在的行为与“按名称递归查找第一个匹配对象”更接近,而不是使用独立 tag 字段。 +比较的是真实 tag 字段,不是对象名称。 ## 参数 -- `tag` - 当前实现下实际被当作对象名称使用的字符串。 +- `tag` - 目标 tag 字符串。 ## 返回值 @@ -26,5 +26,6 @@ go->GetName() == tag ## 相关文档 -- [返回类型总览](Scene.md) +- [Scene](Scene.md) - [Find](Find.md) +- [GameObject::CompareTag](../../Components/GameObject/CompareTag.md) diff --git a/docs/api/XCEngine/Scene/Scene/Scene.md b/docs/api/XCEngine/Scene/Scene/Scene.md index 97422e76..d3dd9517 100644 --- a/docs/api/XCEngine/Scene/Scene/Scene.md +++ b/docs/api/XCEngine/Scene/Scene/Scene.md @@ -6,40 +6,55 @@ **头文件**: `XCEngine/Scene/Scene.h` -**描述**: 持有一组 `GameObject`、驱动场景级更新,并提供场景保存/加载能力的场景容器。 +**描述**: 持有一组 `GameObject`、驱动场景级更新,并提供层级对象树保存/加载能力的场景容器。 ## 概述 `Scene` 是当前引擎里“对象集合”的所有权边界。它内部同时维护: -- `m_gameObjects`,按 ID 保存 `GameObject` 的唯一所有权。 -- `m_rootGameObjects`,保存根对象 ID 列表。 -- `m_gameObjectIDs`,保存当前场景内出现过的对象 ID 集合。 -- 两个创建/销毁事件入口。 +- `m_gameObjects`,按 ID 保存 `GameObject` 的唯一所有权 +- `m_rootGameObjects`,保存根对象 ID 列表 +- `m_gameObjectIDs`,保存当前场景内的对象 ID 集合 +- 两个创建/销毁事件入口 -这类结构在商业引擎里很常见:场景负责对象集合、根节点入口和存档边界;对象本身再负责组件与层级递归。 +`Scene` 负责对象集合、根节点入口、保存/加载边界和场景级更新;对象本身再负责组件与层级递归。 ## 生命周期与所有权 -- [Constructor](Constructor.md) 创建空场景,默认激活状态为 `true`。 -- [CreateGameObject](CreateGameObject.md) 分配并登记一个新对象。 -- [DestroyGameObject](DestroyGameObject.md) 从当前场景树中递归移除对象。 -- [Destructor](Destructor.md) 当前直接清空内部 `unique_ptr` 容器。 +- [Constructor](Constructor.md) 创建空场景,默认 `m_active = true` +- [CreateGameObject](CreateGameObject.md) 分配、登记并唤醒一个新对象 +- [DestroyGameObject](DestroyGameObject.md) 从当前场景树中递归移除对象 +- [Destructor](Destructor.md) 当前直接清空内部 `unique_ptr` 容器 -`Scene` 对对象有所有权,但绝大多数查询接口都返回裸指针。因此调用方必须把这些指针看成“场景拥有、只在场景存活且对象未被销毁时有效”的观察指针。 +查询接口返回的都是观察指针,不转移所有权。 + +## tag 查询与序列化边界 + +[FindGameObjectWithTag](FindGameObjectWithTag.md) 当前会: + +- 从根对象开始 +- 对每个对象调用 `CompareTag(tag)` +- 以深度优先顺序返回第一个匹配对象 + +场景保存/加载同样已经覆盖 tag/layer: + +- [SerializeToString](SerializeToString.md) 会写出每个对象的 `tag` 与 `layer` +- [DeserializeFromString](DeserializeFromString.md) 会恢复这些字段 + +因此场景级 tag 查询、原生对象字段和托管 `GameObject.tag` / `layer` 当前共享同一份底层状态。 ## 当前实现边界 -- `m_active` 当前不会参与更新门控;即使场景 inactive,`Update()` 系列接口照样执行。 -- [FindGameObjectWithTag](FindGameObjectWithTag.md) 名字里是 tag,实际实现里比较的是 `GameObject::GetName()`。 -- [Destructor](Destructor.md) 不会触发 `OnGameObjectDestroyed()`,也不会为组件调用 `OnDestroy()`。 -- [DeserializeFromString](DeserializeFromString.md) 直接重建内部状态,不经过 [CreateGameObject](CreateGameObject.md),因此不会触发生命周期和创建事件。 -- [Save](Save.md) / [Load](Load.md) 打开文件失败时当前静默返回。 -- [SetName](SetName.md) 只会修改场景对象内部名称;如果场景由 `SceneManager` 管理,管理器的 key 不会随之更新。 +- `m_active` 当前不会参与更新门控;即使场景 inactive,[Update](Update.md) 系列接口仍会执行 +- [Destructor](Destructor.md) 不会触发 `OnGameObjectDestroyed()`,也不会为组件调用 `OnDestroy()` +- [DeserializeFromString](DeserializeFromString.md) 直接重建内部状态,不经过 [CreateGameObject](CreateGameObject.md),因此不会触发创建事件或 `Awake()` +- [Save](Save.md) / [Load](Load.md) 打开文件失败时当前只做有限处理,不提供事务式恢复 +- [SetName](SetName.md) 只修改场景对象内部名称;如果场景由 `SceneManager` 管理,管理器 key 不会随之更新 ## 线程语义 -- 当前没有锁;应在主线程或外部同步前提下使用。 +- 当前没有锁 +- 默认应在主线程或外部同步前提下使用 ## 公开方法 @@ -55,7 +70,7 @@ | [DestroyGameObject](DestroyGameObject.md) | 销毁场景对象。 | | [Find](Find.md) | 按名称查找对象。 | | [FindByID](FindByID.md) | 按 ID 查找对象。 | -| [FindGameObjectWithTag](FindGameObjectWithTag.md) | 按“tag”查找;当前实际按名称匹配。 | +| [FindGameObjectWithTag](FindGameObjectWithTag.md) | 按真实 tag 递归查找第一个匹配对象。 | | [FindObjectOfType](FindObjectOfType.md) | 查找第一个匹配组件。 | | [FindObjectsOfType](FindObjectsOfType.md) | 查找所有匹配组件。 | | [GetRootGameObjects](GetRootGameObjects.md) | 获取根对象列表。 | @@ -64,8 +79,8 @@ | [LateUpdate](LateUpdate.md) | 驱动晚更新。 | | [Save](Save.md) | 保存到文件。 | | [Load](Load.md) | 从文件加载。 | -| [SerializeToString](SerializeToString.md) | 序列化为字符串。 | -| [DeserializeFromString](DeserializeFromString.md) | 从字符串反序列化。 | +| [SerializeToString](SerializeToString.md) | 序列化为完整场景文本。 | +| [DeserializeFromString](DeserializeFromString.md) | 从完整场景文本反序列化。 | | [OnGameObjectCreated](OnGameObjectCreated.md) | 访问对象创建事件。 | | [OnGameObjectDestroyed](OnGameObjectDestroyed.md) | 访问对象销毁事件。 | diff --git a/docs/api/XCEngine/Scene/Scene/SerializeToString.md b/docs/api/XCEngine/Scene/Scene/SerializeToString.md index e486303a..4a398137 100644 --- a/docs/api/XCEngine/Scene/Scene/SerializeToString.md +++ b/docs/api/XCEngine/Scene/Scene/SerializeToString.md @@ -1,6 +1,6 @@ # Scene::SerializeToString -把当前场景序列化为字符串。 +把当前场景序列化为自定义文本格式。 ```cpp std::string SerializeToString() const; @@ -8,26 +8,43 @@ std::string SerializeToString() const; ## 行为说明 -当前实现生成的是自定义文本格式,内容大致包括: +当前实现会输出: - 文件头 `# XCEngine Scene File` -- `scene=...` -- `active=...` +- 场景级 `scene=...` +- 场景级 `active=...` - 每个对象的 `gameobject_begin` / `gameobject_end` 块 -- 对象 ID、UUID、名称、父对象 ID、Transform 数据和组件数据 -对象输出顺序是“根对象顺序 + 深度优先递归子树”。 +每个对象块当前包含: + +- `id` +- `uuid` +- `name` +- `tag` +- `active` +- `layer` +- `parent` +- `transform` +- 每个普通组件的 `component=;payload` + +对象输出顺序是“当前根对象顺序 + 深度优先递归子树”。 + +## 字符串处理 + +`name` 和 `tag` 当前会经过 `EscapeString()` 处理,转义: + +- 反斜杠 +- 换行 +- 回车 + +这比 [GameObject::Serialize](../../Components/GameObject/Serialize.md) 的单对象紧凑 token 流更适合完整场景保存。 ## 返回值 - `std::string` - 序列化后的场景文本。 -## 注意事项 - -- 这不是 JSON、YAML 或 glTF 一类的通用格式。 -- 名称字段会经过简单转义;组件内容如何写入,取决于各组件自己的 `Serialize()` 实现。 - ## 相关文档 -- [返回类型总览](Scene.md) +- [Scene](Scene.md) - [DeserializeFromString](DeserializeFromString.md) +- [GameObject::Serialize](../../Components/GameObject/Serialize.md) diff --git a/docs/api/XCEngine/Scripting/Mono/MonoScriptRuntime/DestroyManagedObject.md b/docs/api/XCEngine/Scripting/Mono/MonoScriptRuntime/DestroyManagedObject.md new file mode 100644 index 00000000..364886c1 --- /dev/null +++ b/docs/api/XCEngine/Scripting/Mono/MonoScriptRuntime/DestroyManagedObject.md @@ -0,0 +1,94 @@ +# MonoScriptRuntime::DestroyManagedObject + +**命名空间**: `XCEngine::Scripting` + +**类型**: `method` + +**头文件**: `XCEngine/Scripting/Mono/MonoScriptRuntime.h` + +## 签名 + +```cpp +bool DestroyManagedObject(MonoObject* managedObject); +``` + +## 作用 + +根据传入的托管包装对象,销毁它在原生运行时中对应的 `GameObject` 或组件。 + +## 当前实现行为 + +- 只有在运行时已初始化,且 `managedObject` 非空时才会继续。 +- 函数会先切回当前 Mono app domain,再读取对象的实际 `MonoClass`。 + +### 当对象是 `GameObject` 包装对象时 + +- 读取 `managedGameObjectUUID` +- 必须能在当前 internal-call 活动场景里找到对应 `GameObject` +- 且该对象确实属于当前活动场景 +- 满足条件后调用 `scene->DestroyGameObject(gameObject)` + +### 当对象是组件包装对象时 + +- 必须是 `Component` 本体或其子类 +- 先读取 `gameObjectUUID` +- 若它是脚本行为包装对象,还会继续读取 `scriptComponentUUID` +- 能精确定位到原生脚本组件时,会调用内部 `DestroyNativeComponentInstance(...)` + +对于内建原生组件包装对象,当前支持: + +- `Camera` +- `Light` +- `MeshFilter` +- `MeshRenderer` + +以下类型当前不会被销毁: + +- `Transform` +- 无法识别的组件类型 +- 无法定位到有效原生对象的包装对象 + +## 组件销毁语义 + +当前底层 `DestroyNativeComponentInstance(...)` 会: + +1. 拒绝销毁空组件和 `Transform` +2. 若组件当前启用,且宿主对象在层级中处于激活状态,则先调用 `OnDisable()` +3. 调用 `OnDestroy()` +4. 最后执行 `gameObject->RemoveComponent(component)` + +这说明当前实现不是简单地把组件从数组里抹掉,而是尽量保持接近引擎生命周期语义的销毁顺序。 + +## 与托管侧 API 的关系 + +当前 `Object.Destroy(...)` 的 internal call 最终会走到这个方法: + +```text +InternalCall_Object_Destroy +-> MonoScriptRuntime::DestroyManagedObject +``` + +因此它是托管脚本“请求销毁原生对象”的核心落点之一。 + +## 测试锚点 + +`tests/scripting/test_mono_script_runtime.cpp` 中的 + +- `UnityObjectApiSupportsHierarchyLookupAndDestroy` + +直接覆盖了当前层级查询与销毁语义,包括: + +- 子对象与父对象查询 +- 组件查询 +- 销毁后对象与组件是否真的从场景中消失 + +## 返回值 + +- 成功定位并销毁原生对象或组件时返回 `true` +- 其余情况返回 `false` + +## 相关文档 + +- [MonoScriptRuntime](MonoScriptRuntime.md) +- [CreateManagedComponentWrapper](CreateManagedComponentWrapper.md) +- [DestroyScriptInstance](DestroyScriptInstance.md) diff --git a/docs/api/XCEngine/Scripting/Mono/MonoScriptRuntime/MonoScriptRuntime.md b/docs/api/XCEngine/Scripting/Mono/MonoScriptRuntime/MonoScriptRuntime.md index afe79be2..a1c7eb44 100644 --- a/docs/api/XCEngine/Scripting/Mono/MonoScriptRuntime/MonoScriptRuntime.md +++ b/docs/api/XCEngine/Scripting/Mono/MonoScriptRuntime/MonoScriptRuntime.md @@ -17,9 +17,11 @@ - 发现继承 `MonoBehaviour` 的可用脚本类,并缓存生命周期方法与字段元数据。 - 读取类字段默认值。 - 创建和销毁脚本实例。 -- 通过 internal call 让托管脚本访问原生 `GameObject`、`Transform`、`Camera` 等能力。 +- 通过 internal call 让托管脚本访问原生 `GameObject`、`Transform`、`Camera`、`Light`、`MeshFilter`、`MeshRenderer`、`tag` / `layer` 元数据、日志、输入与时间能力,并把 `Object.Destroy(...)` 这类托管销毁请求回落到原生对象或组件销毁逻辑。 - 把本地字段覆盖写入托管实例,并在生命周期之后同步回 `ScriptFieldStorage`。 +它的设计取向和商业引擎常见方案一致:先把脚本编译成程序集,再以程序集元数据作为类发现、默认值读取和真正实例化的共同事实来源。 + ## Settings 构造函数接收一个 `Settings` 结构体,当前公开字段如下: @@ -37,10 +39,76 @@ `ResolveSettings()` 会在构造时和 `Initialize()` 前再次运行,补全目录和程序集路径。因此只提供 `assemblyDirectory` 也是合法用法;`tests/scripting/test_project_script_assembly.cpp` 就是基于项目脚本程序集目录这样初始化的。 +## 程序集与类发现模型 + +当前 `MonoScriptRuntime` 的类发现有两个关键边界: + +- 只加载两份程序集: + - `XCEngine.ScriptCore.dll` + - `GameScripts.dll` +- 只把应用程序集,也就是 `m_appImage` 这一侧的非抽象 `MonoBehaviour` 子类纳入可绑定脚本类缓存。 + +这意味着: + +- `ScriptCore` 负责基础 API 和托管基类,不负责向 Inspector 暴露可绑定游戏脚本。 +- 项目脚本是否可绑定,不取决于源码目录本身,而取决于它最终有没有进入 `GameScripts.dll`,以及它是不是非抽象 `MonoBehaviour` 子类。 +- `GetScriptClassNames()` 和 `TryGetAvailableScriptClasses()` 都只是读取这份已发现缓存,不会在查询时临时再做一遍程序集扫描。 + +## internal call 桥接语义 + +当前 internal call 已经形成一套相对完整的脚本运行桥: + +- `Time.deltaTime` + - 来自 `InvokeMethod()` 在生命周期调用前写入的当前 delta。 +- `Time.fixedDeltaTime` + - 直接读取 `ScriptEngine::GetRuntimeFixedDeltaTime()`。 +- `Input` + - 直接转发到原生 [InputManager](../../../Input/InputManager/InputManager.md) 的当前状态。 + - `GetKeyUp()` 当前对应释放边沿语义。 + - `anyKey` / `anyKeyDown` 会把鼠标按钮也计入。 +- `Debug.Log*` + - 直接写入原生日志系统。 +- `GameObject.tag` / `GameObject.layer` / `GameObject.CompareTag()` + - 分别落到 `InternalCall_GameObject_GetTag / SetTag / CompareTag / GetLayer / SetLayer` + - `managed/XCEngine.ScriptCore/GameObject.cs` 同时提供 `Tag` / `tag`、`Layer` / `layer` 两组 Unity 风格属性别名 + - `managed/XCEngine.ScriptCore/Component.cs` 再把这些属性直接转发到宿主 `GameObject` + - tag setter 会把 `null` 先归一成空字符串,再由 native `SetTag("")` 回退到 `"Untagged"` + - layer setter 会在 native internal call 中显式 clamp 到 `[0, 31]` + +- `Object.Destroy(...)` + - 会走 `InternalCall_Object_Destroy -> MonoScriptRuntime::DestroyManagedObject(...)` + - 当前支持销毁活动运行时场景里的 `GameObject`,以及由托管包装对象映射到的 `Camera`、`Light`、`MeshFilter`、`MeshRenderer` 与脚本组件 + - `Transform` 包装对象不会被当作可移除组件销毁 + +这意味着托管脚本看到的 `Input` / `Time` / `tag` / `layer` 都不是独立副本,而是直接共享原生运行时当前状态。`tests/scripting/test_mono_script_runtime.cpp` 里 `GameObjectTagAndLayerApiExposeUnityStylePropertiesAndCompareTag` 也明确验证了: + +- 托管脚本能先读到原生侧已有的 tag/layer +- 托管脚本写回后的 `"Player"` / `31` 会真实更新 native `GameObject` +- 原生 `Scene::FindGameObjectWithTag("Player")` 会立刻看到这次更新 + +脚本端如果想正确理解边沿输入、fixed step 配置和对象元数据桥接,应同时对照 [Input Flow And Frame Semantics](../../../../_guides/Input/Input-Flow-and-Frame-Semantics.md) 与 [ScriptEngine](../../ScriptEngine/ScriptEngine.md)。 + +## 实例与字段同步 + +`MonoScriptRuntime` 当前把“创建实例”和“字段同步”拆成两步: + +- `CreateScriptInstance()` + - 先根据 `ScriptComponent` 绑定解析类元数据。 + - 创建托管对象并执行构造。 + - 注入 `gameObjectUUID` / `scriptComponentUUID`。 + - 再把 `ScriptFieldStorage` 中同名且类型匹配的字段写入托管实例。 +- `SyncManagedFieldsToStorage()` + - 只遍历本地已经存在的字段名。 + - 只在类元数据仍存在且类型匹配时回写。 + +这种策略比“看到字段就自动持久化”更保守,但更像工程级引擎的做法:场景层和运行时层边界更清楚,不容易被临时字段污染。 + ## 生命周期 - 构造时接收一份 `Settings`,并做路径补全。 - [Initialize](Initialize.md) 会完成完整 Mono 初始化流程。 +- [OnRuntimeStart](OnRuntimeStart.md) 会清空活动场景 / `deltaTime` 共享状态,并在初始化成功后接入当前场景。 +- [OnRuntimeStop](OnRuntimeStop.md) 会清理活动场景与实例缓存,但不会做完整 `Shutdown()`。 - [Shutdown](Shutdown.md) 会销毁 app domain、清空类缓存与实例缓存。 - 析构会自动调用 `Shutdown()`。 @@ -81,6 +149,7 @@ | [GetManagedInstanceCount](GetManagedInstanceCount.md) | 返回当前托管实例数。 | | [GetManagedInstanceObject](GetManagedInstanceObject.md) | 读取托管对象裸指针。 | | [CreateManagedComponentWrapper](CreateManagedComponentWrapper.md) | 为原生组件创建托管包装对象。 | +| [DestroyManagedObject](DestroyManagedObject.md) | 根据托管包装对象销毁对应的原生 `GameObject` 或组件。 | | [TryGetFieldValue](TryGetFieldValue.md) | 直接读取托管实例字段。 | | [OnRuntimeStart](OnRuntimeStart.md) | 启动脚本运行时上下文。 | | [OnRuntimeStop](OnRuntimeStop.md) | 停止当前运行场景的托管上下文。 | @@ -94,6 +163,8 @@ ## 真实行为依据 - `engine/src/Scripting/Mono/MonoScriptRuntime.cpp` +- `managed/XCEngine.ScriptCore/Input.cs` +- `managed/XCEngine.ScriptCore/Time.cs` - `tests/scripting/test_mono_script_runtime.cpp` - `tests/scripting/test_project_script_assembly.cpp` diff --git a/docs/api/XCEngine/Scripting/Mono/MonoScriptRuntime/TryGetAvailableScriptClasses.md b/docs/api/XCEngine/Scripting/Mono/MonoScriptRuntime/TryGetAvailableScriptClasses.md index 65489fbd..d3adbbdf 100644 --- a/docs/api/XCEngine/Scripting/Mono/MonoScriptRuntime/TryGetAvailableScriptClasses.md +++ b/docs/api/XCEngine/Scripting/Mono/MonoScriptRuntime/TryGetAvailableScriptClasses.md @@ -25,6 +25,22 @@ bool TryGetAvailableScriptClasses( - 只来自当前已发现的应用程序集脚本类缓存。 - 不包含抽象类。 - 不包含非 `MonoBehaviour` 子类。 +- 不会在这里重新加载程序集或重新做类型扫描;发现动作发生在 `Initialize()` 期间。 +- 当前写入到 `descriptor.assemblyName` 的值来自 `m_settings.appAssemblyName`。 + +## 为什么这个列表可以直接给 UI 用 + +当前实现已经在运行时层做了稳定排序,因此: + +- `GetScriptClassNames()` 可以直接在此基础上再做一层字符串提取。 +- `ScriptEngine::TryGetAvailableScriptClasses()` 也只需要再补一层程序集过滤和空类名剔除。 +- Inspector 下拉框或脚本选择器不需要自己再去理解 Mono 反射细节。 + +## 真实行为依据 + +- `engine/src/Scripting/Mono/MonoScriptRuntime.cpp` +- `tests/scripting/test_mono_script_runtime.cpp` +- `tests/scripting/test_project_script_assembly.cpp` ## 相关文档 diff --git a/docs/api/_guides/Components/GameObject-Component-Lifecycle-And-Serialization.md b/docs/api/_guides/Components/GameObject-Component-Lifecycle-And-Serialization.md index 9b02fb9f..2612b896 100644 --- a/docs/api/_guides/Components/GameObject-Component-Lifecycle-And-Serialization.md +++ b/docs/api/_guides/Components/GameObject-Component-Lifecycle-And-Serialization.md @@ -13,7 +13,7 @@ 可以把它粗略类比成 Unity 的对象体系: - `Scene` 像场景容器,拥有对象并驱动更新与序列化。 -- `GameObject` 像层级树节点,负责身份、父子关系、激活状态和组件容器。 +- `GameObject` 像层级树节点,负责身份、轻量 `tag` / `layer`、父子关系、激活状态和组件容器。 - `TransformComponent` 像内建 `Transform`,每个对象必有一个。 - 其他组件按职责挂接到对象上,例如 `CameraComponent`、`LightComponent`、`MeshRendererComponent`、`AudioSourceComponent`。 @@ -160,7 +160,9 @@ auto* source = go->AddComponent(); 只写对象基础状态: - `name` +- `tag` - `active` +- `layer` - `id` - `uuid` - `transform` @@ -207,9 +209,19 @@ component=Camera;projection=0;fov=60;... ## 你现在必须知道的限制 -### 1. `FindGameObjectsWithTag()` 不是 tag 系统 +### 1. `FindGameObjectsWithTag()` 现在是真实 tag 查询,但仍是轻量实现 -当前实现按对象名称匹配,不是独立 tag 字段。 +当前实现已经有独立 `m_tag` 字段: + +- 默认是 `"Untagged"` +- `SetTag("")` 会回退到 `"Untagged"` +- `FindGameObjectsWithTag()` / `Scene::FindGameObjectWithTag()` 都通过 `CompareTag()` 查询 + +但它仍不是完整 TagManager: + +- 没有项目级 tag 定义表 +- 没有索引或缓存 +- 查询仍是线性扫描 ### 2. `TransformComponent::Find()` 不是局部子树查找 @@ -253,7 +265,7 @@ component=Camera;projection=0;fov=60;... - 它在对象模型上明显借鉴了 Unity。 - 它提供了足够熟悉的 `GameObject + Transform + Component` 心智模型。 -- 但当前实现仍然比 Unity 轻得多,特别是在 tag、运行时加组件生命周期、完整组件序列化和层级顺序控制上。 +- 但当前实现仍然比 Unity 轻得多,特别是在 tag/layer 管理、运行时加组件生命周期、完整组件序列化和层级顺序控制上。 把它理解成“Unity 风格、但当前版本能力更收敛的对象系统”是比较准确的。 diff --git a/docs/api/_guides/Scene/Scene-Lifecycle-And-Serialization.md b/docs/api/_guides/Scene/Scene-Lifecycle-And-Serialization.md index 46077117..1c78a52a 100644 --- a/docs/api/_guides/Scene/Scene-Lifecycle-And-Serialization.md +++ b/docs/api/_guides/Scene/Scene-Lifecycle-And-Serialization.md @@ -61,10 +61,12 @@ - 解析鲁棒性有限。 - 组件恢复依赖 `ComponentFactoryRegistry`,组件名或注册表变化都会影响加载结果。 +当前这份文本格式已经覆盖 `GameObject` 的 `tag` / `layer` 字段;场景 round-trip 会把它们和层级、Transform、普通组件一起恢复。 + ## 当前最需要小心的几个事实 - 场景析构不会自动为所有对象走显式销毁流程,所以不要把 `~Scene()` 当成完整生命周期清理器。 -- `FindGameObjectWithTag()` 现在其实是在按名字查。 +- `FindGameObjectWithTag()` 当前已经是按真实 tag 做深度优先查询,但它仍是线性扫描,不是带索引的完整 tag 系统。 - `LoadSceneAsync()` 现在不是异步,更多只是一个名字上预留好的 API。 - `LoadScene()` 后的管理器 key 来自文件名,而不是场景内部保存的 `scene=` 名称。 @@ -75,7 +77,7 @@ - 需要 `OnDestroy` 或场景事件时,显式调用对象销毁入口,不要只依赖场景析构。 - 需要稳定查场景时,优先用 `CreateScene()` 的名称或加载文件的文件名 stem,不要假设一定等于场景内部名字。 - 不要把 `LoadSceneAsync()` 当成真正的后台加载接口。 -- 不要把 `FindGameObjectWithTag()` 当成完整 tag 系统。 +- 可以把 `FindGameObjectWithTag()` 当成真实 tag 查询,但不要把它当成带项目配置和索引优化的完整 TagManager。 ## 相关 API