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,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)

View File

@@ -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`

View File

@@ -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`
## 相关文档