11 KiB
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.dllGameScripts.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 的当前状态。
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先归一成空字符串,再由 nativeSetTag("")回退到"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会真实更新 nativeGameObject - 原生
Scene::FindGameObjectWithTag("Player")会立刻看到这次更新
脚本端如果想正确理解边沿输入、fixed step 配置和对象元数据桥接,应同时对照 Input Flow And Frame Semantics 与 ScriptEngine。
实例与字段同步
MonoScriptRuntime 当前把“创建实例”和“字段同步”拆成两步:
CreateScriptInstance()- 先根据
ScriptComponent绑定解析类元数据。 - 创建托管对象并执行构造。
- 注入
gameObjectUUID/scriptComponentUUID。 - 再把
ScriptFieldStorage中同名且类型匹配的字段写入托管实例。
- 先根据
SyncManagedFieldsToStorage()- 只遍历本地已经存在的字段名。
- 只在类元数据仍存在且类型匹配时回写。
这种策略比“看到字段就自动持久化”更保守,但更像工程级引擎的做法:场景层和运行时层边界更清楚,不容易被临时字段污染。
生命周期
- 构造时接收一份
Settings,并做路径补全。 - Initialize 会完成完整 Mono 初始化流程。
- OnRuntimeStart 会清空活动场景 /
deltaTime共享状态,并在初始化成功后接入当前场景。 - OnRuntimeStop 会清理活动场景与实例缓存,但不会做完整
Shutdown()。 - Shutdown 会销毁 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 | 创建 Mono 运行时对象并解析设置。 |
| Destructor | 析构时执行 Shutdown()。 |
| Initialize | 初始化 Mono 域并发现脚本类。 |
| Shutdown | 关闭当前 Mono 运行时。 |
| GetLastError | 读取最近一次错误描述。 |
| IsClassAvailable | 查询脚本类是否已发现。 |
| GetScriptClassNames | 返回已发现脚本类名列表。 |
IsInitialized() |
判断运行时是否已完成初始化。 |
GetSettings() |
返回已解析的运行时设置。 |
| TryGetAvailableScriptClasses | 返回完整脚本类描述列表。 |
| TryGetClassFieldMetadata | 读取脚本类字段元数据。 |
| TryGetClassFieldDefaultValues | 读取脚本类字段默认值。 |
| HasManagedInstance | 判断某脚本组件是否已有托管实例。 |
| GetManagedInstanceCount | 返回当前托管实例数。 |
| GetManagedInstanceObject | 读取托管对象裸指针。 |
| CreateManagedComponentWrapper | 为原生组件创建托管包装对象。 |
| DestroyManagedObject | 根据托管包装对象销毁对应的原生 GameObject 或组件。 |
| TryGetFieldValue | 直接读取托管实例字段。 |
| OnRuntimeStart | 启动脚本运行时上下文。 |
| OnRuntimeStop | 停止当前运行场景的托管上下文。 |
| TrySetManagedFieldValue | 写托管字段。 |
| TryGetManagedFieldValue | 读托管字段。 |
| SyncManagedFieldsToStorage | 回写本地字段缓存。 |
| CreateScriptInstance | 创建脚本实例。 |
| DestroyScriptInstance | 销毁脚本实例。 |
| InvokeMethod | 调用生命周期方法。 |
真实行为依据
engine/src/Scripting/Mono/MonoScriptRuntime.cppmanaged/XCEngine.ScriptCore/Input.csmanaged/XCEngine.ScriptCore/Time.cstests/scripting/test_mono_script_runtime.cpptests/scripting/test_project_script_assembly.cpp