Files
XCEngine/docs/api/XCEngine/Scripting/Mono/MonoScriptRuntime/MonoScriptRuntime.md

178 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# MonoScriptRuntime
**命名空间**: `XCEngine::Scripting`
**类型**: `class`
**头文件**: `XCEngine/Scripting/Mono/MonoScriptRuntime.h`
**描述**: 基于 Mono 的脚本运行时实现,负责程序集解析与加载、类发现、实例创建、字段桥接、默认值读取和生命周期调用。
## 概览
`MonoScriptRuntime` 是当前唯一的真实托管脚本后端。它把 `ScriptEngine` 提供的抽象调用翻译成 Mono 世界里的具体动作:
- 初始化 root domain 和 app domain。
- 加载脚本核心程序集与游戏程序集。
- 发现继承 `MonoBehaviour` 的可用脚本类,并缓存生命周期方法与字段元数据。
- 读取类字段默认值。
- 创建和销毁脚本实例。
- 通过 internal call 让托管脚本访问原生 `GameObject``Transform``Camera``Light``MeshFilter``MeshRenderer``tag` / `layer` 元数据、日志、输入与时间能力,并把 `Object.Destroy(...)` 这类托管销毁请求回落到原生对象或组件销毁逻辑。
- 把本地字段覆盖写入托管实例,并在生命周期之后同步回 `ScriptFieldStorage`
它的设计取向和商业引擎常见方案一致:先把脚本编译成程序集,再以程序集元数据作为类发现、默认值读取和真正实例化的共同事实来源。
## Settings
构造函数接收一个 `Settings` 结构体,当前公开字段如下:
| 字段 | 说明 |
|------|------|
| `assemblyDirectory` | 程序集目录;若 `coreAssemblyPath` / `appAssemblyPath` 缺失,会以它为基准推导 DLL 路径。 |
| `corlibDirectory` | Mono 解析 `mscorlib.dll` 的目录;为空时会回退到 `assemblyDirectory``coreAssemblyPath` 所在目录。 |
| `coreAssemblyPath` | `XCEngine.ScriptCore.dll` 的完整路径。 |
| `appAssemblyPath` | `GameScripts.dll` 的完整路径。 |
| `coreAssemblyName` | 脚本核心程序集名,默认 `XCEngine.ScriptCore`。 |
| `appAssemblyName` | 应用程序集名,默认 `GameScripts`。 |
| `baseNamespace` | 核心托管 API 的命名空间,默认 `XCEngine`。 |
| `baseClassName` | 作为脚本基类查找入口的类型名,默认 `MonoBehaviour`。 |
`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()`
## 设计要点
- `ScriptEngine` 不直接碰 Mono API所有后端细节收敛在这个类里。
- 类缓存和实例缓存都基于稳定键,便于场景重建、类切换和实例回绑。
- `TryGetClassFieldDefaultValues()` 会创建一个临时托管对象并读取字段值,因此脚本字段默认值可以反映 C# 初始化表达式,而不只是原生零值。
- `CreateScriptInstance()` 会先注入上下文 UUID再把本地存储里同名且类型匹配的字段写入托管实例。
- `SyncManagedFieldsToStorage()` 只回写本地已有字段,保证运行时不会把临时字段偷偷持久化到场景。
## 当前实现边界
- 当前只发现应用程序集中的非抽象 `MonoBehaviour` 子类。
- 支持的公共字段类型只覆盖 `float / double / bool / int32 / uint64 / string / Vector2 / Vector3 / Vector4 / GameObject`
- `TryGetAvailableScriptClasses()``GetScriptClassNames()` 和字段元数据查询都要求运行时已经初始化完成。
- `SyncManagedFieldsToStorage()` 只会回写已经存在于 `ScriptFieldStorage` 中且类型仍匹配的字段;运行时临时字段不会自动持久化。
- `OnRuntimeStop()` 只清理当前活动场景与实例,不会做完整 `Shutdown()`;程序集和类缓存会保留到显式 `Shutdown()` 或析构。
- 目前没有程序集热重载、增量编译监听或多场景并发运行支持。
## 公开方法
| 方法 | 说明 |
|------|------|
| [Constructor](Constructor.md) | 创建 Mono 运行时对象并解析设置。 |
| [Destructor](Destructor.md) | 析构时执行 `Shutdown()`。 |
| [Initialize](Initialize.md) | 初始化 Mono 域并发现脚本类。 |
| [Shutdown](Shutdown.md) | 关闭当前 Mono 运行时。 |
| [GetLastError](GetLastError.md) | 读取最近一次错误描述。 |
| [IsClassAvailable](IsClassAvailable.md) | 查询脚本类是否已发现。 |
| [GetScriptClassNames](GetScriptClassNames.md) | 返回已发现脚本类名列表。 |
| `IsInitialized()` | 判断运行时是否已完成初始化。 |
| `GetSettings()` | 返回已解析的运行时设置。 |
| [TryGetAvailableScriptClasses](TryGetAvailableScriptClasses.md) | 返回完整脚本类描述列表。 |
| [TryGetClassFieldMetadata](TryGetClassFieldMetadata.md) | 读取脚本类字段元数据。 |
| [TryGetClassFieldDefaultValues](TryGetClassFieldDefaultValues.md) | 读取脚本类字段默认值。 |
| [HasManagedInstance](HasManagedInstance.md) | 判断某脚本组件是否已有托管实例。 |
| [GetManagedInstanceCount](GetManagedInstanceCount.md) | 返回当前托管实例数。 |
| [GetManagedInstanceObject](GetManagedInstanceObject.md) | 读取托管对象裸指针。 |
| [CreateManagedComponentWrapper](CreateManagedComponentWrapper.md) | 为原生组件创建托管包装对象。 |
| [DestroyManagedObject](DestroyManagedObject.md) | 根据托管包装对象销毁对应的原生 `GameObject` 或组件。 |
| [TryGetFieldValue](TryGetFieldValue.md) | 直接读取托管实例字段。 |
| [OnRuntimeStart](OnRuntimeStart.md) | 启动脚本运行时上下文。 |
| [OnRuntimeStop](OnRuntimeStop.md) | 停止当前运行场景的托管上下文。 |
| [TrySetManagedFieldValue](TrySetManagedFieldValue.md) | 写托管字段。 |
| [TryGetManagedFieldValue](TryGetManagedFieldValue.md) | 读托管字段。 |
| [SyncManagedFieldsToStorage](SyncManagedFieldsToStorage.md) | 回写本地字段缓存。 |
| [CreateScriptInstance](CreateScriptInstance.md) | 创建脚本实例。 |
| [DestroyScriptInstance](DestroyScriptInstance.md) | 销毁脚本实例。 |
| [InvokeMethod](InvokeMethod.md) | 调用生命周期方法。 |
## 真实行为依据
- `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`
## 相关文档
- [Mono](../Mono.md)
- [IScriptRuntime](../../IScriptRuntime/IScriptRuntime.md)
- [ScriptEngine](../../ScriptEngine/ScriptEngine.md)
- [Scripting Runtime And Field Model](../../../../_guides/Scripting/Scripting-Runtime-And-Field-Model.md)
- [Project Script Assembly And Field Sync](../../../../_guides/Scripting/Project-Script-Assembly-And-Field-Sync.md)