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

11 KiB
Raw Blame History

MonoScriptRuntime

命名空间: XCEngine::Scripting

类型: class

头文件: XCEngine/Scripting/Mono/MonoScriptRuntime.h

描述: 基于 Mono 的脚本运行时实现,负责程序集解析与加载、类发现、实例创建、字段桥接、默认值读取和生命周期调用。

概览

MonoScriptRuntime 是当前唯一的真实托管脚本后端。它把 ScriptEngine 提供的抽象调用翻译成 Mono 世界里的具体动作:

  • 初始化 root domain 和 app domain。
  • 加载脚本核心程序集与游戏程序集。
  • 发现继承 MonoBehaviour 的可用脚本类,并缓存生命周期方法与字段元数据。
  • 读取类字段默认值。
  • 创建和销毁脚本实例。
  • 通过 internal call 让托管脚本访问原生 GameObjectTransformCameraLightMeshFilterMeshRenderertag / layer 元数据、日志、输入与时间能力,并把 Object.Destroy(...) 这类托管销毁请求回落到原生对象或组件销毁逻辑。
  • 把本地字段覆盖写入托管实例,并在生命周期之后同步回 ScriptFieldStorage

它的设计取向和商业引擎常见方案一致:先把脚本编译成程序集,再以程序集元数据作为类发现、默认值读取和真正实例化的共同事实来源。

Settings

构造函数接收一个 Settings 结构体,当前公开字段如下:

字段 说明
assemblyDirectory 程序集目录;若 coreAssemblyPath / appAssemblyPath 缺失,会以它为基准推导 DLL 路径。
corlibDirectory Mono 解析 mscorlib.dll 的目录;为空时会回退到 assemblyDirectorycoreAssemblyPath 所在目录。
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 的当前状态。
    • GetKeyUp() 当前对应释放边沿语义。
    • anyKey / anyKeyDown 会把鼠标按钮也计入。
  • Debug.Log*

    • 直接写入原生日志系统。
  • GameObject.tag / GameObject.layer / GameObject.CompareTag()

    • 分别落到 InternalCall_GameObject_GetTag / SetTag / CompareTag / GetLayer / SetLayer
    • managed/XCEngine.ScriptCore/GameObject.cs 同时提供 Tag / tagLayer / 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,以及由托管包装对象映射到的 CameraLightMeshFilterMeshRenderer 与脚本组件
    • Transform 包装对象不会被当作可移除组件销毁

这意味着托管脚本看到的 Input / Time / tag / layer 都不是独立副本,而是直接共享原生运行时当前状态。tests/scripting/test_mono_script_runtime.cppGameObjectTagAndLayerApiExposeUnityStylePropertiesAndCompareTag 也明确验证了:

  • 托管脚本能先读到原生侧已有的 tag/layer
  • 托管脚本写回后的 "Player" / 31 会真实更新 native GameObject
  • 原生 Scene::FindGameObjectWithTag("Player") 会立刻看到这次更新

脚本端如果想正确理解边沿输入、fixed step 配置和对象元数据桥接,应同时对照 Input Flow And Frame SemanticsScriptEngine

实例与字段同步

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.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

相关文档