2026-03-28 15:10:54 +08:00
|
|
|
|
# MonoScriptRuntime
|
|
|
|
|
|
|
|
|
|
|
|
**命名空间**: `XCEngine::Scripting`
|
|
|
|
|
|
|
|
|
|
|
|
**类型**: `class`
|
|
|
|
|
|
|
|
|
|
|
|
**头文件**: `XCEngine/Scripting/Mono/MonoScriptRuntime.h`
|
|
|
|
|
|
|
2026-04-02 22:23:29 +08:00
|
|
|
|
**描述**: 基于 Mono 的脚本运行时实现,负责程序集解析与加载、类发现、实例创建、字段桥接、默认值读取和生命周期调用。
|
2026-03-28 15:10:54 +08:00
|
|
|
|
|
|
|
|
|
|
## 概览
|
|
|
|
|
|
|
|
|
|
|
|
`MonoScriptRuntime` 是当前唯一的真实托管脚本后端。它把 `ScriptEngine` 提供的抽象调用翻译成 Mono 世界里的具体动作:
|
|
|
|
|
|
|
|
|
|
|
|
- 初始化 root domain 和 app domain。
|
|
|
|
|
|
- 加载脚本核心程序集与游戏程序集。
|
2026-04-02 22:23:29 +08:00
|
|
|
|
- 发现继承 `MonoBehaviour` 的可用脚本类,并缓存生命周期方法与字段元数据。
|
|
|
|
|
|
- 读取类字段默认值。
|
2026-03-28 15:10:54 +08:00
|
|
|
|
- 创建和销毁脚本实例。
|
2026-04-03 15:48:09 +08:00
|
|
|
|
- 通过 internal call 让托管脚本访问原生 `GameObject`、`Transform`、`Camera`、`Light`、`MeshFilter`、`MeshRenderer`、`tag` / `layer` 元数据、日志、输入与时间能力,并把 `Object.Destroy(...)` 这类托管销毁请求回落到原生对象或组件销毁逻辑。
|
2026-04-02 22:23:29 +08:00
|
|
|
|
- 把本地字段覆盖写入托管实例,并在生命周期之后同步回 `ScriptFieldStorage`。
|
|
|
|
|
|
|
2026-04-03 15:48:09 +08:00
|
|
|
|
它的设计取向和商业引擎常见方案一致:先把脚本编译成程序集,再以程序集元数据作为类发现、默认值读取和真正实例化的共同事实来源。
|
|
|
|
|
|
|
2026-04-02 22:23:29 +08:00
|
|
|
|
## 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` 就是基于项目脚本程序集目录这样初始化的。
|
2026-03-28 15:10:54 +08:00
|
|
|
|
|
2026-04-03 15:48:09 +08:00
|
|
|
|
## 程序集与类发现模型
|
|
|
|
|
|
|
|
|
|
|
|
当前 `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()`
|
|
|
|
|
|
- 只遍历本地已经存在的字段名。
|
|
|
|
|
|
- 只在类元数据仍存在且类型匹配时回写。
|
|
|
|
|
|
|
|
|
|
|
|
这种策略比“看到字段就自动持久化”更保守,但更像工程级引擎的做法:场景层和运行时层边界更清楚,不容易被临时字段污染。
|
|
|
|
|
|
|
2026-03-28 15:10:54 +08:00
|
|
|
|
## 生命周期
|
|
|
|
|
|
|
|
|
|
|
|
- 构造时接收一份 `Settings`,并做路径补全。
|
|
|
|
|
|
- [Initialize](Initialize.md) 会完成完整 Mono 初始化流程。
|
2026-04-03 15:48:09 +08:00
|
|
|
|
- [OnRuntimeStart](OnRuntimeStart.md) 会清空活动场景 / `deltaTime` 共享状态,并在初始化成功后接入当前场景。
|
|
|
|
|
|
- [OnRuntimeStop](OnRuntimeStop.md) 会清理活动场景与实例缓存,但不会做完整 `Shutdown()`。
|
2026-03-28 15:10:54 +08:00
|
|
|
|
- [Shutdown](Shutdown.md) 会销毁 app domain、清空类缓存与实例缓存。
|
|
|
|
|
|
- 析构会自动调用 `Shutdown()`。
|
|
|
|
|
|
|
|
|
|
|
|
## 设计要点
|
|
|
|
|
|
|
|
|
|
|
|
- `ScriptEngine` 不直接碰 Mono API;所有后端细节收敛在这个类里。
|
2026-04-02 22:23:29 +08:00
|
|
|
|
- 类缓存和实例缓存都基于稳定键,便于场景重建、类切换和实例回绑。
|
|
|
|
|
|
- `TryGetClassFieldDefaultValues()` 会创建一个临时托管对象并读取字段值,因此脚本字段默认值可以反映 C# 初始化表达式,而不只是原生零值。
|
|
|
|
|
|
- `CreateScriptInstance()` 会先注入上下文 UUID,再把本地存储里同名且类型匹配的字段写入托管实例。
|
|
|
|
|
|
- `SyncManagedFieldsToStorage()` 只回写本地已有字段,保证运行时不会把临时字段偷偷持久化到场景。
|
2026-03-28 15:10:54 +08:00
|
|
|
|
|
|
|
|
|
|
## 当前实现边界
|
|
|
|
|
|
|
|
|
|
|
|
- 当前只发现应用程序集中的非抽象 `MonoBehaviour` 子类。
|
|
|
|
|
|
- 支持的公共字段类型只覆盖 `float / double / bool / int32 / uint64 / string / Vector2 / Vector3 / Vector4 / GameObject`。
|
2026-04-02 22:23:29 +08:00
|
|
|
|
- `TryGetAvailableScriptClasses()`、`GetScriptClassNames()` 和字段元数据查询都要求运行时已经初始化完成。
|
|
|
|
|
|
- `SyncManagedFieldsToStorage()` 只会回写已经存在于 `ScriptFieldStorage` 中且类型仍匹配的字段;运行时临时字段不会自动持久化。
|
|
|
|
|
|
- `OnRuntimeStop()` 只清理当前活动场景与实例,不会做完整 `Shutdown()`;程序集和类缓存会保留到显式 `Shutdown()` 或析构。
|
|
|
|
|
|
- 目前没有程序集热重载、增量编译监听或多场景并发运行支持。
|
2026-03-28 15:10:54 +08:00
|
|
|
|
|
|
|
|
|
|
## 公开方法
|
|
|
|
|
|
|
|
|
|
|
|
| 方法 | 说明 |
|
|
|
|
|
|
|------|------|
|
|
|
|
|
|
| [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) | 返回已发现脚本类名列表。 |
|
2026-04-02 22:23:29 +08:00
|
|
|
|
| `IsInitialized()` | 判断运行时是否已完成初始化。 |
|
|
|
|
|
|
| `GetSettings()` | 返回已解析的运行时设置。 |
|
|
|
|
|
|
| [TryGetAvailableScriptClasses](TryGetAvailableScriptClasses.md) | 返回完整脚本类描述列表。 |
|
2026-03-28 15:10:54 +08:00
|
|
|
|
| [TryGetClassFieldMetadata](TryGetClassFieldMetadata.md) | 读取脚本类字段元数据。 |
|
2026-04-02 22:23:29 +08:00
|
|
|
|
| [TryGetClassFieldDefaultValues](TryGetClassFieldDefaultValues.md) | 读取脚本类字段默认值。 |
|
2026-03-28 15:10:54 +08:00
|
|
|
|
| [HasManagedInstance](HasManagedInstance.md) | 判断某脚本组件是否已有托管实例。 |
|
|
|
|
|
|
| [GetManagedInstanceCount](GetManagedInstanceCount.md) | 返回当前托管实例数。 |
|
|
|
|
|
|
| [GetManagedInstanceObject](GetManagedInstanceObject.md) | 读取托管对象裸指针。 |
|
|
|
|
|
|
| [CreateManagedComponentWrapper](CreateManagedComponentWrapper.md) | 为原生组件创建托管包装对象。 |
|
2026-04-03 15:48:09 +08:00
|
|
|
|
| [DestroyManagedObject](DestroyManagedObject.md) | 根据托管包装对象销毁对应的原生 `GameObject` 或组件。 |
|
2026-03-28 15:10:54 +08:00
|
|
|
|
| [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`
|
2026-04-03 15:48:09 +08:00
|
|
|
|
- `managed/XCEngine.ScriptCore/Input.cs`
|
|
|
|
|
|
- `managed/XCEngine.ScriptCore/Time.cs`
|
2026-03-28 15:10:54 +08:00
|
|
|
|
- `tests/scripting/test_mono_script_runtime.cpp`
|
2026-04-02 22:23:29 +08:00
|
|
|
|
- `tests/scripting/test_project_script_assembly.cpp`
|
2026-03-28 15:10:54 +08:00
|
|
|
|
|
|
|
|
|
|
## 相关文档
|
|
|
|
|
|
|
|
|
|
|
|
- [Mono](../Mono.md)
|
|
|
|
|
|
- [IScriptRuntime](../../IScriptRuntime/IScriptRuntime.md)
|
|
|
|
|
|
- [ScriptEngine](../../ScriptEngine/ScriptEngine.md)
|
|
|
|
|
|
- [Scripting Runtime And Field Model](../../../../_guides/Scripting/Scripting-Runtime-And-Field-Model.md)
|
2026-04-02 22:23:29 +08:00
|
|
|
|
- [Project Script Assembly And Field Sync](../../../../_guides/Scripting/Project-Script-Assembly-And-Field-Sync.md)
|