docs: sync gameobject tag layer docs
This commit is contained in:
48
docs/api/XCEngine/Components/GameObject/CompareTag.md
Normal file
48
docs/api/XCEngine/Components/GameObject/CompareTag.md
Normal 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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
35
docs/api/XCEngine/Components/GameObject/GetLayer.md
Normal file
35
docs/api/XCEngine/Components/GameObject/GetLayer.md
Normal 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)
|
||||
40
docs/api/XCEngine/Components/GameObject/GetTag.md
Normal file
40
docs/api/XCEngine/Components/GameObject/GetTag.md
Normal 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)
|
||||
@@ -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)
|
||||
|
||||
52
docs/api/XCEngine/Components/GameObject/SetLayer.md
Normal file
52
docs/api/XCEngine/Components/GameObject/SetLayer.md
Normal 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)
|
||||
53
docs/api/XCEngine/Components/GameObject/SetTag.md
Normal file
53
docs/api/XCEngine/Components/GameObject/SetTag.md
Normal 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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) | 访问对象销毁事件。 |
|
||||
|
||||
|
||||
@@ -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=<type>;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)
|
||||
|
||||
@@ -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)
|
||||
@@ -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`
|
||||
|
||||
|
||||
@@ -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`
|
||||
|
||||
## 相关文档
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
可以把它粗略类比成 Unity 的对象体系:
|
||||
|
||||
- `Scene` 像场景容器,拥有对象并驱动更新与序列化。
|
||||
- `GameObject` 像层级树节点,负责身份、父子关系、激活状态和组件容器。
|
||||
- `GameObject` 像层级树节点,负责身份、轻量 `tag` / `layer`、父子关系、激活状态和组件容器。
|
||||
- `TransformComponent` 像内建 `Transform`,每个对象必有一个。
|
||||
- 其他组件按职责挂接到对象上,例如 `CameraComponent`、`LightComponent`、`MeshRendererComponent`、`AudioSourceComponent`。
|
||||
|
||||
@@ -160,7 +160,9 @@ auto* source = go->AddComponent<XCEngine::Components::AudioSourceComponent>();
|
||||
只写对象基础状态:
|
||||
|
||||
- `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 风格、但当前版本能力更收敛的对象系统”是比较准确的。
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user