9.2 KiB
MonoScriptRuntime
命名空间: XCEngine::Scripting
类型: class
头文件: XCEngine/Scripting/Mono/MonoScriptRuntime.h
描述: 当前唯一的托管脚本运行时后端,实现程序集加载、脚本类发现、字段元数据/默认值查询、实例创建、字段同步和生命周期调用。
概览
MonoScriptRuntime 是 IScriptRuntime 的当前唯一实现。它把 ScriptEngine 需要的抽象操作落到 Mono 世界中:
- 初始化和关闭 Mono 运行时上下文
- 加载核心程序集与应用程序集
- 发现可绑定脚本类
- 缓存字段元数据和生命周期方法
- 创建/销毁托管实例
- 读写托管字段
- 把托管 API 通过 internal call 接回原生系统
这层分工的关键价值,在于让 ScriptEngine 负责“什么时候调度”,让 MonoScriptRuntime 负责“如何在 Mono 里执行”。这样后续即使更换后端,场景生命周期和字段同步模型也仍然可以保持一致。
Settings
构造函数接收一个 Settings 结构体。当前重要字段如下:
| 字段 | 说明 |
|---|---|
assemblyDirectory |
程序集目录;若显式路径为空,会以它推导 DLL 路径。 |
corlibDirectory |
Mono 查找 mscorlib.dll 的目录。 |
coreAssemblyPath |
XCEngine.ScriptCore.dll 的路径。 |
appAssemblyPath |
GameScripts.dll 的路径。 |
coreAssemblyName |
核心程序集名,默认 XCEngine.ScriptCore。 |
appAssemblyName |
应用程序集名,默认 GameScripts。 |
baseNamespace |
托管基础 API 命名空间,默认 XCEngine。 |
baseClassName |
作为脚本基类入口的类型名,默认 MonoBehaviour。 |
ResolveSettings() 会补全这些路径,因此当前既可以显式指定 DLL,也可以只指定程序集目录。
程序集与类发现模型
当前脚本类发现遵循两个核心边界:
- 只从
m_appImage,也就是应用程序集这一侧发现可绑定脚本类 - 只接受非抽象的
MonoBehaviour子类
这意味着:
XCEngine.ScriptCore负责基础 API,不直接成为可绑定游戏脚本来源GameScripts.dll才是当前脚本类列表、字段元数据和默认值读取的共同事实来源
字段发现规则
字段发现发生在 DiscoverScriptClassesInImage(...) 阶段。当前真实筛选顺序是:
- 排除
static - 排除
literal / const - 排除
init-only / readonly - 接受
public字段,或者带[SerializeField]的private字段 - 再由
BuildFieldMetadata(...)检查字段类型是否受支持
这让当前字段模型和 Unity 风格更加接近:既支持公开字段,也支持“保持封装但允许序列化”的 [SerializeField] private 字段。
当前支持的字段类型
BuildFieldMetadata(...) 当前支持以下字段类型:
floatdoubleboolint32uint64stringenum- 当前以
ScriptFieldType::Int32暴露 - 仅支持基础类型为
I1 / U1 / I2 / U2 / I4 / U4的枚举
- 当前以
Vector2Vector3Vector4GameObject- 具体组件引用
TransformCameraLightMeshFilterMeshRenderer- 具体脚本组件类型
以下类型当前不会进入字段模型:
- 未标注的
private字段 readonly字段QuaternionComponent/MonoBehaviour这类过于宽泛的基类引用- 数组、列表、泛型容器等当前未桥接类型
- 64 位底层枚举等未列入支持范围的枚举底层类型
TryGetClassFieldMetadata() 与 TryGetClassFieldDefaultValues()
TryGetClassFieldMetadata 和 TryGetClassFieldDefaultValues 都只操作“已经通过上述规则进入字段缓存”的字段集合。
其中默认值查询当前会:
- 切到 app domain
- 临时创建一个托管对象
- 执行默认构造和字段初始化
- 逐个读取字段值
因此它返回的是“真实 C# 初始化后”的默认值,而不是单纯的 CLR 零值。
这也是为什么测试里可以直接读到:
FieldMetadataProbe.State == 2FieldMetadataProbe.HiddenFlag == trueProjectScriptProbe.Label == "ProjectScriptProbe"
实例创建与字段同步
当前实例与字段同步模型是:
- CreateScriptInstance
- 根据
ScriptComponent找类 - 创建托管对象
- 注入
gameObjectUUID/scriptComponentUUID - 把
ScriptFieldStorage里的同名且类型匹配字段写回托管实例
- 根据
- SyncManagedFieldsToStorage
- 把运行中的托管字段同步回本地存储
这一模型和商业引擎的常见做法一致:类默认值提供初始模板,场景存储提供覆盖值,运行时再在同一字段集合上做双向同步。
为什么支持 [SerializeField] private
这是当前设计里非常关键的一点。支持它不是为了“多兼容一个语法糖”,而是为了兼顾两件事:
- 脚本作者需要字段能进入序列化和 Inspector 体系
- 但不希望因此把类的外部 API 全部暴露为
public
这带来的直接好处是:
- 封装边界更清晰
- Inspector / 场景序列化体验更接近 Unity
- 重构字段访问级别时更安全
而继续忽略“未标注的 private 字段”,则能避免把纯内部实现状态误持久化。
internal call 桥接
当前 internal call 已经接通一套相对完整的托管 API:
Debug.Log*TimeInputGameObjectTransformCameraLightMeshFilterMeshRendererObject.Destroy(...)
这些托管 API 当前并不是各自维护一套独立状态,而是直接桥接回原生系统。
测试锚点
当前行为直接受到以下测试约束:
ClassFieldMetadataListsSupportedPublicInstanceFieldsClassFieldDefaultValueQueryReturnsSerializeFieldPrivateInitializersSerializeFieldPrivateFieldsApplyStoredValuesAndPersistAcrossSceneRoundTriptest_project_script_assembly.cpp中的项目程序集发现与默认值读取用例
当前实现边界
- 当前只发现应用程序集里的非抽象
MonoBehaviour子类。 [SerializeField] private已纳入字段模型,但“未标注 private”依旧被忽略。- 默认值查询依赖真实托管对象实例化,因此类缺失、实例构造失败或字段读取失败都会让整次查询失败。
- 当前没有程序集热重载、增量刷新或调试器集成。
公开方法
| 方法 | 说明 |
|---|---|
| Constructor | 创建运行时对象并解析设置。 |
| Destructor | 析构时执行 Shutdown()。 |
| Initialize | 初始化 Mono 域并发现脚本类。 |
| Shutdown | 关闭当前 Mono 运行时。 |
| GetLastError | 读取最近一次错误描述。 |
| IsClassAvailable | 查询脚本类是否已发现。 |
| GetScriptClassNames | 返回已发现脚本类全名列表。 |
IsInitialized() |
判断运行时是否完成初始化。 |
GetSettings() |
返回已解析的设置。 |
| TryGetAvailableScriptClasses | 返回脚本类描述列表。 |
| TryGetClassFieldMetadata | 返回当前字段模型中的字段元数据。 |
| TryGetClassFieldDefaultValues | 返回当前字段模型中的真实默认值。 |
| HasManagedInstance | 判断脚本组件是否已有托管实例。 |
| GetManagedInstanceCount | 返回当前托管实例数。 |
| GetManagedInstanceObject | 获取托管对象裸指针。 |
| CreateManagedComponentWrapper | 为原生组件创建托管包装对象。 |
| DestroyManagedObject | 销毁托管对象映射到的原生对象或组件。 |
| TryGetFieldValue | 直接读取托管实例字段。 |
| OnRuntimeStart | 启动当前场景的托管上下文。 |
| OnRuntimeStop | 停止当前场景的托管上下文。 |
| TrySetManagedFieldValue | 向托管实例写字段。 |
| TryGetManagedFieldValue | 从托管实例读字段。 |
| SyncManagedFieldsToStorage | 把托管字段同步回存储。 |
| CreateScriptInstance | 创建脚本实例。 |
| DestroyScriptInstance | 销毁脚本实例。 |
| InvokeMethod | 调用生命周期方法。 |
真实行为依据
engine/src/Scripting/Mono/MonoScriptRuntime.cppmanaged/XCEngine.ScriptCore/SerializeField.csmanaged/GameScripts/FieldMetadataProbe.csmanaged/GameScripts/SerializeFieldProbe.cstests/Scripting/test_mono_script_runtime.cpptests/Scripting/test_project_script_assembly.cpp