docs: sync api and planning docs
This commit is contained in:
@@ -4,51 +4,102 @@
|
||||
|
||||
**类型**: `submodule`
|
||||
|
||||
**描述**: 收纳基于 Mono 的托管脚本运行时实现。
|
||||
**描述**: 收纳基于 Mono 的托管脚本运行时实现,以及当前脚本字段发现、默认值读取与运行时桥接的 Mono 后端文档。
|
||||
|
||||
## 概览
|
||||
|
||||
`docs/api/XCEngine/Scripting/Mono` 对应 `engine/include/XCEngine/Scripting/Mono` 子目录。它不是独立命名空间,而是脚本模块按后端实现拆出来的子目录。
|
||||
`docs/api/XCEngine/Scripting/Mono` 对应 `engine/include/XCEngine/Scripting/Mono`。它不是独立命名空间,而是脚本系统按运行时后端拆出的子目录。
|
||||
|
||||
当前这里唯一的公开类型是 [MonoScriptRuntime](MonoScriptRuntime/MonoScriptRuntime.md)。它负责:
|
||||
当前这里的核心公开类型是 [MonoScriptRuntime](MonoScriptRuntime/MonoScriptRuntime.md)。它负责:
|
||||
|
||||
- 初始化 Mono root domain 和 app domain。
|
||||
- 解析 `MonoScriptRuntime::Settings`,推导程序集目录、脚本核心程序集和游戏程序集路径。
|
||||
- 加载 `XCEngine.ScriptCore.dll` 与 `GameScripts.dll`。
|
||||
- 发现应用程序集中的非抽象 `MonoBehaviour` 子类,并缓存支持的公共实例字段与生命周期方法。
|
||||
- 为 `ScriptComponent` 创建托管实例,注入 `GameObject`/`ScriptComponent` 上下文,并在生命周期后同步字段。
|
||||
- 初始化 Mono root domain / app domain
|
||||
- 加载 `XCEngine.ScriptCore.dll` 与 `GameScripts.dll`
|
||||
- 发现应用程序集中的非抽象 `MonoBehaviour` 子类
|
||||
- 构建字段元数据与字段默认值查询能力
|
||||
- 创建脚本实例、注入上下文并调用生命周期
|
||||
- 通过 internal call 把 `Debug`、`Time`、`Input`、`GameObject`、`Transform` 等托管 API 接回原生实现
|
||||
|
||||
## 为什么单独分目录
|
||||
## 字段发现模型
|
||||
|
||||
把 Mono 运行时放进独立子目录,而不是直接塞进 `Scripting` 根目录,有两个直接好处:
|
||||
这一轮最重要的语义更新,是当前 Mono 后端已经不再等同于“只发现 public 实例字段”。
|
||||
|
||||
- 可以清楚区分“脚本系统公共契约”和“具体后端实现”。
|
||||
- 以后如果接入别的脚本后端,这里天然就是平行扩展点。
|
||||
当前真实规则是:
|
||||
|
||||
## 程序集来源
|
||||
- 先排除:
|
||||
- `static`
|
||||
- `const / literal`
|
||||
- `readonly / init-only`
|
||||
- 然后接受:
|
||||
- `public` 实例字段
|
||||
- 或者标了 `[SerializeField]` 的 `private` 实例字段
|
||||
- 最后再按当前支持的字段类型过滤
|
||||
|
||||
当前 Mono 后端既支持测试程序集目录,也支持项目资产脚本程序集目录:
|
||||
这意味着:
|
||||
|
||||
- `managed/CMakeLists.txt` 会构建基础 `XCEngine.ScriptCore.dll` 与 `GameScripts.dll`。
|
||||
- 同一份 CMake 还会把 `project/Assets/**/*.cs` 编译到 `project/Library/ScriptAssemblies/GameScripts.dll`。
|
||||
- `tests/scripting/test_project_script_assembly.cpp` 已验证运行时能从项目程序集目录发现 `ProjectScripts.ProjectScriptProbe` 及其默认字段值。
|
||||
- `FieldMetadataProbe.Health` / `Speed` / `Label` 这类 `public` 字段会被发现
|
||||
- `FieldMetadataProbe.HiddenFlag` 这种 `[SerializeField] private` 字段也会被发现
|
||||
- `IgnoredPrivateCounter` 这类未标注的 `private` 字段仍然会被忽略
|
||||
- `SharedCounter` 这类 `static` 字段会被忽略
|
||||
- `UnsupportedRotation` 这类当前不支持的类型也会被忽略
|
||||
|
||||
## 为什么要支持 `[SerializeField] private`
|
||||
|
||||
这是一个很典型的 Unity 风格设计,但它的价值不只是“和 Unity 看齐”。更重要的是它解决了工程里的两个真实矛盾:
|
||||
|
||||
- 你希望字段能被序列化、能被 Inspector 或场景存储驱动
|
||||
- 但你又不希望把这个字段暴露成脚本类的公开 API
|
||||
|
||||
支持 `[SerializeField] private` 的好处包括:
|
||||
|
||||
- 保留封装边界,不必为了可持久化而把内部状态全部改成 `public`
|
||||
- 更适合后续重构,减少脚本 API 面被外部任意依赖
|
||||
- 和商业引擎用户的认知模型一致,降低脚本作者迁移成本
|
||||
|
||||
而继续忽略“未标注的 private 字段”,则能防止把纯运行时内部状态意外持久化。
|
||||
|
||||
## 为什么要排除 `readonly`
|
||||
|
||||
当前字段同步模型要求:
|
||||
|
||||
- 运行前可以把 `ScriptFieldStorage` 里的覆盖值写回托管实例
|
||||
- 运行后可以把托管实例里的值同步回本地存储
|
||||
|
||||
`readonly` 字段不适合承担这类双向同步职责,因此当前实现把 `init-only` 直接排除。这是一个偏工程安全的取舍。
|
||||
|
||||
## 程序集事实来源
|
||||
|
||||
当前 Mono 后端的类发现和字段发现,依赖的是“编译后的程序集事实”,而不是直接扫描 `.cs` 源文件目录:
|
||||
|
||||
- `XCEngine.ScriptCore.dll` 提供托管基类和属性
|
||||
- `GameScripts.dll` 提供应用层脚本类
|
||||
|
||||
这和商业引擎常见的做法一致:Inspector 列表、字段元数据、默认值读取和真正实例化,都应该基于同一份已编译产物。
|
||||
|
||||
## 测试锚点
|
||||
|
||||
当前行为直接受到以下测试约束:
|
||||
|
||||
- `ClassFieldMetadataListsSupportedPublicInstanceFields`
|
||||
- `ClassFieldDefaultValueQueryReturnsSerializeFieldPrivateInitializers`
|
||||
- `SerializeFieldPrivateFieldsApplyStoredValuesAndPersistAcrossSceneRoundTrip`
|
||||
- `test_project_script_assembly.cpp` 里的项目程序集发现与默认值读取用例
|
||||
|
||||
## 当前实现边界
|
||||
|
||||
- 当前只实现了 Mono 后端,没有并列的 IL2CPP、Lua 或自研 VM 后端。
|
||||
- 目录里只有一个 public header,说明当前重点仍然是把一条 Mono 托管链路跑通。
|
||||
- 类发现只扫描当前应用程序集里的非抽象 `MonoBehaviour` 子类,不会把工具类或抽象基类暴露给脚本绑定 UI。
|
||||
- 字段发现只接受受支持类型的 `public` 非 `static` 实例字段。
|
||||
- internal call 目前已覆盖 `GameObject`、`Transform`、`Camera`、`Light`、`MeshFilter`、`MeshRenderer`、日志和时间桥接,但远不是完整编辑器级脚本 API 面。
|
||||
- 当前只有 Mono 后端,没有并列的 IL2CPP / Lua / 自研 VM 后端。
|
||||
- 字段发现只扫描应用程序集里的非抽象 `MonoBehaviour` 子类。
|
||||
- `[SerializeField] private` 已支持,但“未标注 private”“`readonly`”“不支持类型”仍然不会进入字段模型。
|
||||
- 项目脚本能否被发现,取决于它是否成功进入 `GameScripts.dll`,而不是取决于源文件是否存在于目录里。
|
||||
|
||||
## 头文件
|
||||
|
||||
- [MonoScriptRuntime](MonoScriptRuntime/MonoScriptRuntime.md) - `MonoScriptRuntime.h`
|
||||
- [MonoScriptRuntime](MonoScriptRuntime/MonoScriptRuntime.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)
|
||||
- [Game View Runtime Input Bridge](../../../_guides/Editor/Game-View-Runtime-Input-Bridge.md)
|
||||
|
||||
## 相关文档
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ InternalCall_Object_Destroy
|
||||
|
||||
## 测试锚点
|
||||
|
||||
`tests/scripting/test_mono_script_runtime.cpp` 中的
|
||||
`tests/Scripting/test_mono_script_runtime.cpp` 中的
|
||||
|
||||
- `UnityObjectApiSupportsHierarchyLookupAndDestroy`
|
||||
|
||||
|
||||
@@ -6,156 +6,205 @@
|
||||
|
||||
**头文件**: `XCEngine/Scripting/Mono/MonoScriptRuntime.h`
|
||||
|
||||
**描述**: 基于 Mono 的脚本运行时实现,负责程序集解析与加载、类发现、实例创建、字段桥接、默认值读取和生命周期调用。
|
||||
**描述**: 当前唯一的托管脚本运行时后端,实现程序集加载、脚本类发现、字段元数据/默认值查询、实例创建、字段同步和生命周期调用。
|
||||
|
||||
## 概览
|
||||
|
||||
`MonoScriptRuntime` 是当前唯一的真实托管脚本后端。它把 `ScriptEngine` 提供的抽象调用翻译成 Mono 世界里的具体动作:
|
||||
`MonoScriptRuntime` 是 `IScriptRuntime` 的当前唯一实现。它把 `ScriptEngine` 需要的抽象操作落到 Mono 世界中:
|
||||
|
||||
- 初始化 root domain 和 app domain。
|
||||
- 加载脚本核心程序集与游戏程序集。
|
||||
- 发现继承 `MonoBehaviour` 的可用脚本类,并缓存生命周期方法与字段元数据。
|
||||
- 读取类字段默认值。
|
||||
- 创建和销毁脚本实例。
|
||||
- 通过 internal call 让托管脚本访问原生 `GameObject`、`Transform`、`Camera`、`Light`、`MeshFilter`、`MeshRenderer`、`tag` / `layer` 元数据、日志、输入与时间能力,并把 `Object.Destroy(...)` 这类托管销毁请求回落到原生对象或组件销毁逻辑。
|
||||
- 把本地字段覆盖写入托管实例,并在生命周期之后同步回 `ScriptFieldStorage`。
|
||||
- 初始化和关闭 Mono 运行时上下文
|
||||
- 加载核心程序集与应用程序集
|
||||
- 发现可绑定脚本类
|
||||
- 缓存字段元数据和生命周期方法
|
||||
- 创建/销毁托管实例
|
||||
- 读写托管字段
|
||||
- 把托管 API 通过 internal call 接回原生系统
|
||||
|
||||
它的设计取向和商业引擎常见方案一致:先把脚本编译成程序集,再以程序集元数据作为类发现、默认值读取和真正实例化的共同事实来源。
|
||||
这层分工的关键价值,在于让 `ScriptEngine` 负责“什么时候调度”,让 `MonoScriptRuntime` 负责“如何在 Mono 里执行”。这样后续即使更换后端,场景生命周期和字段同步模型也仍然可以保持一致。
|
||||
|
||||
## Settings
|
||||
|
||||
构造函数接收一个 `Settings` 结构体,当前公开字段如下:
|
||||
构造函数接收一个 `Settings` 结构体。当前重要字段如下:
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| `assemblyDirectory` | 程序集目录;若 `coreAssemblyPath` / `appAssemblyPath` 缺失,会以它为基准推导 DLL 路径。 |
|
||||
| `corlibDirectory` | Mono 解析 `mscorlib.dll` 的目录;为空时会回退到 `assemblyDirectory` 或 `coreAssemblyPath` 所在目录。 |
|
||||
| `coreAssemblyPath` | `XCEngine.ScriptCore.dll` 的完整路径。 |
|
||||
| `appAssemblyPath` | `GameScripts.dll` 的完整路径。 |
|
||||
| `coreAssemblyName` | 脚本核心程序集名,默认 `XCEngine.ScriptCore`。 |
|
||||
| `assemblyDirectory` | 程序集目录;若显式路径为空,会以它推导 DLL 路径。 |
|
||||
| `corlibDirectory` | Mono 查找 `mscorlib.dll` 的目录。 |
|
||||
| `coreAssemblyPath` | `XCEngine.ScriptCore.dll` 的路径。 |
|
||||
| `appAssemblyPath` | `GameScripts.dll` 的路径。 |
|
||||
| `coreAssemblyName` | 核心程序集名,默认 `XCEngine.ScriptCore`。 |
|
||||
| `appAssemblyName` | 应用程序集名,默认 `GameScripts`。 |
|
||||
| `baseNamespace` | 核心托管 API 的命名空间,默认 `XCEngine`。 |
|
||||
| `baseClassName` | 作为脚本基类查找入口的类型名,默认 `MonoBehaviour`。 |
|
||||
| `baseNamespace` | 托管基础 API 命名空间,默认 `XCEngine`。 |
|
||||
| `baseClassName` | 作为脚本基类入口的类型名,默认 `MonoBehaviour`。 |
|
||||
|
||||
`ResolveSettings()` 会在构造时和 `Initialize()` 前再次运行,补全目录和程序集路径。因此只提供 `assemblyDirectory` 也是合法用法;`tests/scripting/test_project_script_assembly.cpp` 就是基于项目脚本程序集目录这样初始化的。
|
||||
`ResolveSettings()` 会补全这些路径,因此当前既可以显式指定 DLL,也可以只指定程序集目录。
|
||||
|
||||
## 程序集与类发现模型
|
||||
|
||||
当前 `MonoScriptRuntime` 的类发现有两个关键边界:
|
||||
当前脚本类发现遵循两个核心边界:
|
||||
|
||||
- 只加载两份程序集:
|
||||
- `XCEngine.ScriptCore.dll`
|
||||
- `GameScripts.dll`
|
||||
- 只把应用程序集,也就是 `m_appImage` 这一侧的非抽象 `MonoBehaviour` 子类纳入可绑定脚本类缓存。
|
||||
- 只从 `m_appImage`,也就是应用程序集这一侧发现可绑定脚本类
|
||||
- 只接受非抽象的 `MonoBehaviour` 子类
|
||||
|
||||
这意味着:
|
||||
|
||||
- `ScriptCore` 负责基础 API 和托管基类,不负责向 Inspector 暴露可绑定游戏脚本。
|
||||
- 项目脚本是否可绑定,不取决于源码目录本身,而取决于它最终有没有进入 `GameScripts.dll`,以及它是不是非抽象 `MonoBehaviour` 子类。
|
||||
- `GetScriptClassNames()` 和 `TryGetAvailableScriptClasses()` 都只是读取这份已发现缓存,不会在查询时临时再做一遍程序集扫描。
|
||||
- `XCEngine.ScriptCore` 负责基础 API,不直接成为可绑定游戏脚本来源
|
||||
- `GameScripts.dll` 才是当前脚本类列表、字段元数据和默认值读取的共同事实来源
|
||||
|
||||
## internal call 桥接语义
|
||||
## 字段发现规则
|
||||
|
||||
当前 internal call 已经形成一套相对完整的脚本运行桥:
|
||||
字段发现发生在 `DiscoverScriptClassesInImage(...)` 阶段。当前真实筛选顺序是:
|
||||
|
||||
1. 排除 `static`
|
||||
2. 排除 `literal / const`
|
||||
3. 排除 `init-only / readonly`
|
||||
4. 接受 `public` 字段,或者带 `[SerializeField]` 的 `private` 字段
|
||||
5. 再由 `BuildFieldMetadata(...)` 检查字段类型是否受支持
|
||||
|
||||
这让当前字段模型和 Unity 风格更加接近:既支持公开字段,也支持“保持封装但允许序列化”的 `[SerializeField] private` 字段。
|
||||
|
||||
## 当前支持的字段类型
|
||||
|
||||
`BuildFieldMetadata(...)` 当前支持以下字段类型:
|
||||
|
||||
- `float`
|
||||
- `double`
|
||||
- `bool`
|
||||
- `int32`
|
||||
- `uint64`
|
||||
- `string`
|
||||
- `enum`
|
||||
- 当前以 `ScriptFieldType::Int32` 暴露
|
||||
- 仅支持基础类型为 `I1 / U1 / I2 / U2 / I4 / U4` 的枚举
|
||||
- `Vector2`
|
||||
- `Vector3`
|
||||
- `Vector4`
|
||||
- `GameObject`
|
||||
- 具体组件引用
|
||||
- `Transform`
|
||||
- `Camera`
|
||||
- `Light`
|
||||
- `MeshFilter`
|
||||
- `MeshRenderer`
|
||||
- 具体脚本组件类型
|
||||
|
||||
以下类型当前不会进入字段模型:
|
||||
|
||||
- 未标注的 `private` 字段
|
||||
- `readonly` 字段
|
||||
- `Quaternion`
|
||||
- `Component` / `MonoBehaviour` 这类过于宽泛的基类引用
|
||||
- 数组、列表、泛型容器等当前未桥接类型
|
||||
- 64 位底层枚举等未列入支持范围的枚举底层类型
|
||||
|
||||
## `TryGetClassFieldMetadata()` 与 `TryGetClassFieldDefaultValues()`
|
||||
|
||||
[TryGetClassFieldMetadata](TryGetClassFieldMetadata.md) 和 [TryGetClassFieldDefaultValues](TryGetClassFieldDefaultValues.md) 都只操作“已经通过上述规则进入字段缓存”的字段集合。
|
||||
|
||||
其中默认值查询当前会:
|
||||
|
||||
1. 切到 app domain
|
||||
2. 临时创建一个托管对象
|
||||
3. 执行默认构造和字段初始化
|
||||
4. 逐个读取字段值
|
||||
|
||||
因此它返回的是“真实 C# 初始化后”的默认值,而不是单纯的 CLR 零值。
|
||||
|
||||
这也是为什么测试里可以直接读到:
|
||||
|
||||
- `FieldMetadataProbe.State == 2`
|
||||
- `FieldMetadataProbe.HiddenFlag == true`
|
||||
- `ProjectScriptProbe.Label == "ProjectScriptProbe"`
|
||||
|
||||
## 实例创建与字段同步
|
||||
|
||||
当前实例与字段同步模型是:
|
||||
|
||||
- [CreateScriptInstance](CreateScriptInstance.md)
|
||||
- 根据 `ScriptComponent` 找类
|
||||
- 创建托管对象
|
||||
- 注入 `gameObjectUUID` / `scriptComponentUUID`
|
||||
- 把 `ScriptFieldStorage` 里的同名且类型匹配字段写回托管实例
|
||||
- [SyncManagedFieldsToStorage](SyncManagedFieldsToStorage.md)
|
||||
- 把运行中的托管字段同步回本地存储
|
||||
|
||||
这一模型和商业引擎的常见做法一致:类默认值提供初始模板,场景存储提供覆盖值,运行时再在同一字段集合上做双向同步。
|
||||
|
||||
## 为什么支持 `[SerializeField] private`
|
||||
|
||||
这是当前设计里非常关键的一点。支持它不是为了“多兼容一个语法糖”,而是为了兼顾两件事:
|
||||
|
||||
- 脚本作者需要字段能进入序列化和 Inspector 体系
|
||||
- 但不希望因此把类的外部 API 全部暴露为 `public`
|
||||
|
||||
这带来的直接好处是:
|
||||
|
||||
- 封装边界更清晰
|
||||
- Inspector / 场景序列化体验更接近 Unity
|
||||
- 重构字段访问级别时更安全
|
||||
|
||||
而继续忽略“未标注的 private 字段”,则能避免把纯内部实现状态误持久化。
|
||||
|
||||
## internal call 桥接
|
||||
|
||||
当前 internal call 已经接通一套相对完整的托管 API:
|
||||
|
||||
- `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]`
|
||||
|
||||
- `Time`
|
||||
- `Input`
|
||||
- `GameObject`
|
||||
- `Transform`
|
||||
- `Camera`
|
||||
- `Light`
|
||||
- `MeshFilter`
|
||||
- `MeshRenderer`
|
||||
- `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` 也明确验证了:
|
||||
这些托管 API 当前并不是各自维护一套独立状态,而是直接桥接回原生系统。
|
||||
|
||||
- 托管脚本能先读到原生侧已有的 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()` 只回写本地已有字段,保证运行时不会把临时字段偷偷持久化到场景。
|
||||
- `ClassFieldMetadataListsSupportedPublicInstanceFields`
|
||||
- `ClassFieldDefaultValueQueryReturnsSerializeFieldPrivateInitializers`
|
||||
- `SerializeFieldPrivateFieldsApplyStoredValuesAndPersistAcrossSceneRoundTrip`
|
||||
- `test_project_script_assembly.cpp` 中的项目程序集发现与默认值读取用例
|
||||
|
||||
## 当前实现边界
|
||||
|
||||
- 当前只发现应用程序集中的非抽象 `MonoBehaviour` 子类。
|
||||
- 支持的公共字段类型只覆盖 `float / double / bool / int32 / uint64 / string / Vector2 / Vector3 / Vector4 / GameObject`。
|
||||
- `TryGetAvailableScriptClasses()`、`GetScriptClassNames()` 和字段元数据查询都要求运行时已经初始化完成。
|
||||
- `SyncManagedFieldsToStorage()` 只会回写已经存在于 `ScriptFieldStorage` 中且类型仍匹配的字段;运行时临时字段不会自动持久化。
|
||||
- `OnRuntimeStop()` 只清理当前活动场景与实例,不会做完整 `Shutdown()`;程序集和类缓存会保留到显式 `Shutdown()` 或析构。
|
||||
- 目前没有程序集热重载、增量编译监听或多场景并发运行支持。
|
||||
- 当前只发现应用程序集里的非抽象 `MonoBehaviour` 子类。
|
||||
- `[SerializeField] private` 已纳入字段模型,但“未标注 private”依旧被忽略。
|
||||
- 默认值查询依赖真实托管对象实例化,因此类缺失、实例构造失败或字段读取失败都会让整次查询失败。
|
||||
- 当前没有程序集热重载、增量刷新或调试器集成。
|
||||
|
||||
## 公开方法
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| [Constructor](Constructor.md) | 创建 Mono 运行时对象并解析设置。 |
|
||||
| [Constructor](Constructor.md) | 创建运行时对象并解析设置。 |
|
||||
| [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) | 判断某脚本组件是否已有托管实例。 |
|
||||
| [GetScriptClassNames](GetScriptClassNames.md) | 返回已发现脚本类全名列表。 |
|
||||
| `IsInitialized()` | 判断运行时是否完成初始化。 |
|
||||
| `GetSettings()` | 返回已解析的设置。 |
|
||||
| [TryGetAvailableScriptClasses](TryGetAvailableScriptClasses.md) | 返回脚本类描述列表。 |
|
||||
| [TryGetClassFieldMetadata](TryGetClassFieldMetadata.md) | 返回当前字段模型中的字段元数据。 |
|
||||
| [TryGetClassFieldDefaultValues](TryGetClassFieldDefaultValues.md) | 返回当前字段模型中的真实默认值。 |
|
||||
| [HasManagedInstance](HasManagedInstance.md) | 判断脚本组件是否已有托管实例。 |
|
||||
| [GetManagedInstanceCount](GetManagedInstanceCount.md) | 返回当前托管实例数。 |
|
||||
| [GetManagedInstanceObject](GetManagedInstanceObject.md) | 读取托管对象裸指针。 |
|
||||
| [GetManagedInstanceObject](GetManagedInstanceObject.md) | 获取托管对象裸指针。 |
|
||||
| [CreateManagedComponentWrapper](CreateManagedComponentWrapper.md) | 为原生组件创建托管包装对象。 |
|
||||
| [DestroyManagedObject](DestroyManagedObject.md) | 根据托管包装对象销毁对应的原生 `GameObject` 或组件。 |
|
||||
| [DestroyManagedObject](DestroyManagedObject.md) | 销毁托管对象映射到的原生对象或组件。 |
|
||||
| [TryGetFieldValue](TryGetFieldValue.md) | 直接读取托管实例字段。 |
|
||||
| [OnRuntimeStart](OnRuntimeStart.md) | 启动脚本运行时上下文。 |
|
||||
| [OnRuntimeStop](OnRuntimeStop.md) | 停止当前运行场景的托管上下文。 |
|
||||
| [TrySetManagedFieldValue](TrySetManagedFieldValue.md) | 写托管字段。 |
|
||||
| [TryGetManagedFieldValue](TryGetManagedFieldValue.md) | 读托管字段。 |
|
||||
| [SyncManagedFieldsToStorage](SyncManagedFieldsToStorage.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) | 调用生命周期方法。 |
|
||||
@@ -163,10 +212,11 @@
|
||||
## 真实行为依据
|
||||
|
||||
- `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`
|
||||
- `managed/XCEngine.ScriptCore/SerializeField.cs`
|
||||
- `managed/GameScripts/FieldMetadataProbe.cs`
|
||||
- `managed/GameScripts/SerializeFieldProbe.cs`
|
||||
- `tests/Scripting/test_mono_script_runtime.cpp`
|
||||
- `tests/Scripting/test_project_script_assembly.cpp`
|
||||
|
||||
## 相关文档
|
||||
|
||||
|
||||
@@ -39,8 +39,8 @@ bool TryGetAvailableScriptClasses(
|
||||
## 真实行为依据
|
||||
|
||||
- `engine/src/Scripting/Mono/MonoScriptRuntime.cpp`
|
||||
- `tests/scripting/test_mono_script_runtime.cpp`
|
||||
- `tests/scripting/test_project_script_assembly.cpp`
|
||||
- `tests/Scripting/test_mono_script_runtime.cpp`
|
||||
- `tests/Scripting/test_project_script_assembly.cpp`
|
||||
|
||||
## 相关文档
|
||||
|
||||
|
||||
@@ -16,33 +16,79 @@ bool TryGetClassFieldDefaultValues(
|
||||
std::vector<ScriptFieldDefaultValue>& outFields) const override;
|
||||
```
|
||||
|
||||
## 作用
|
||||
|
||||
返回指定脚本类当前字段模型中各字段的真实默认值。
|
||||
|
||||
这里的“默认值”不是简单的语言零值,而是 C# 字段初始化和默认构造执行后的结果。
|
||||
|
||||
## 当前实现流程
|
||||
|
||||
1. 清空 `outFields`。
|
||||
2. 用 `(assemblyName, namespaceName, className)` 查找缓存的类元数据。
|
||||
2. 查找类缓存;找不到则返回 `false`。
|
||||
3. 切换到当前 app domain。
|
||||
4. 创建一个临时托管对象并执行默认构造。
|
||||
5. 遍历缓存字段,逐个读取字段值并写入 `ScriptFieldDefaultValue`。
|
||||
6. 按 `fieldName` 排序后返回。
|
||||
4. 创建一个临时托管对象。
|
||||
5. 执行默认初始化。
|
||||
6. 遍历字段缓存,逐个读取字段值并填充 `ScriptFieldDefaultValue`。
|
||||
7. 按 `fieldName` 排序后返回。
|
||||
|
||||
## 为什么它重要
|
||||
如果任何一步失败,当前实现会清空输出并返回 `false`。
|
||||
|
||||
这里返回的是托管类真实初始化后的字段值,所以它能反映:
|
||||
## 它读取的是哪一批字段
|
||||
|
||||
这个方法不会自行重新筛字段,而是复用 [TryGetClassFieldMetadata](TryGetClassFieldMetadata.md) 所对应的同一批字段集合。
|
||||
|
||||
因此它同样支持:
|
||||
|
||||
- `public` 实例字段
|
||||
- `[SerializeField] private` 字段
|
||||
|
||||
并同样排除:
|
||||
|
||||
- `static`
|
||||
- `const / literal`
|
||||
- `readonly / init-only`
|
||||
- 未标注的 `private`
|
||||
- 不支持的字段类型
|
||||
|
||||
## 为什么它比“零值表”更有意义
|
||||
|
||||
因为它真的构造了一个托管对象,所以能反映:
|
||||
|
||||
- C# 字段初始化表达式
|
||||
- 默认构造后留下的初始状态
|
||||
- 枚举默认值
|
||||
- `string.Empty`
|
||||
- 布尔或数值的自定义初始值
|
||||
|
||||
`ScriptEngine::TryGetScriptFieldModel()` 和 `ClearScriptFieldOverrides()` 都依赖这条数据。
|
||||
例如当前测试直接验证了:
|
||||
|
||||
## 失败路径
|
||||
- `FieldMetadataProbe.State` 的默认值是 `2`
|
||||
- `FieldMetadataProbe.HiddenFlag` 的默认值是 `true`
|
||||
- `ProjectScriptProbe.Label` 的默认值是 `"ProjectScriptProbe"`
|
||||
- `ProjectScriptProbe.Speed` 的默认值是 `2.5f`
|
||||
|
||||
- 类找不到
|
||||
- Mono 无法创建临时对象
|
||||
- 任意字段读取失败
|
||||
这也是商业引擎里常见的设计:Inspector 默认值和运行时实例的首帧初值,都应来自同一份真实脚本初始化结果。
|
||||
|
||||
这些情况都会返回 `false`,并清空输出。
|
||||
## `[SerializeField] private` 的默认值也会被读到
|
||||
|
||||
这一点很重要。既然 `[SerializeField] private` 已经进入字段模型,它的默认值也应和 `public` 字段一样被查询到。
|
||||
|
||||
当前测试
|
||||
`ClassFieldDefaultValueQueryReturnsSerializeFieldPrivateInitializers`
|
||||
已经明确验证:`FieldMetadataProbe.HiddenFlag` 会以 `true` 返回。
|
||||
|
||||
## 返回值语义
|
||||
|
||||
- `true`
|
||||
- 类存在,且全部字段默认值都成功读取。
|
||||
- `false`
|
||||
- 类不存在、临时对象创建失败,或任意字段读取失败。
|
||||
|
||||
失败时 `outFields` 会被清空,调用方不应读取旧内容。
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [MonoScriptRuntime](MonoScriptRuntime.md)
|
||||
- [TryGetClassFieldMetadata](TryGetClassFieldMetadata.md)
|
||||
- [Project Script Assembly And Field Sync](../../../../_guides/Scripting/Project-Script-Assembly-And-Field-Sync.md)
|
||||
- [Scripting Runtime And Field Model](../../../../_guides/Scripting/Scripting-Runtime-And-Field-Model.md)
|
||||
|
||||
@@ -16,21 +16,83 @@ bool TryGetClassFieldMetadata(
|
||||
std::vector<ScriptFieldMetadata>& outFields) const override;
|
||||
```
|
||||
|
||||
## 作用
|
||||
|
||||
返回指定脚本类当前“可进入字段模型”的字段元数据列表。
|
||||
|
||||
这里返回的不是类里声明的全部字段,而是经过 Mono 运行时筛选后的字段集合。
|
||||
|
||||
## 当前实现行为
|
||||
|
||||
- 先查找类缓存。
|
||||
- 找不到返回 `false` 并清空输出。
|
||||
- 找到后把缓存中的字段元数据复制到 `outFields`。
|
||||
- 最终按字段名排序。
|
||||
1. 先清空 `outFields`。
|
||||
2. 用 `(assemblyName, namespaceName, className)` 查缓存里的类元数据。
|
||||
3. 如果类不存在,返回 `false`,并保持 `outFields` 为空。
|
||||
4. 如果类存在:
|
||||
- 遍历 `metadata->fields`
|
||||
- 复制成 `ScriptFieldMetadata{name, type}`
|
||||
- 最终按字段名排序后返回 `true`
|
||||
|
||||
## 字段来源边界
|
||||
因此,这个方法本身不再扫描程序集;它只读取初始化阶段已经构建好的类缓存。
|
||||
|
||||
当前只会收录:
|
||||
## 字段筛选规则
|
||||
|
||||
- 公共实例字段
|
||||
- 非静态字段
|
||||
- 可映射到当前支持脚本字段类型的字段
|
||||
字段集合来源于 `DiscoverScriptClassesInImage(...)` 的筛选结果。当前真实规则是:
|
||||
|
||||
- 排除 `static`
|
||||
- 排除 `const / literal`
|
||||
- 排除 `readonly / init-only`
|
||||
- 接受:
|
||||
- `public` 实例字段
|
||||
- 或带 `[SerializeField]` 的 `private` 实例字段
|
||||
- 再要求字段类型属于当前支持集合
|
||||
|
||||
这意味着“private”本身不是问题,问题在于它有没有显式进入序列化/字段模型。
|
||||
|
||||
## 典型例子
|
||||
|
||||
`managed/GameScripts/FieldMetadataProbe.cs` 当前很好地展示了这些边界:
|
||||
|
||||
- 会被发现:
|
||||
- `Health`
|
||||
- `Speed`
|
||||
- `Label`
|
||||
- `SpawnPoint`
|
||||
- `State`
|
||||
- `Target`
|
||||
- `HiddenFlag`
|
||||
- 不会被发现:
|
||||
- `SharedCounter`
|
||||
- `IgnoredPrivateCounter`
|
||||
- `UnsupportedRotation`
|
||||
|
||||
其中 `HiddenFlag` 能被发现,正是因为它是 `[SerializeField] private bool`。
|
||||
|
||||
## 为什么这很重要
|
||||
|
||||
这条规则直接决定了:
|
||||
|
||||
- Inspector / 字段编辑 UI 能看到哪些字段
|
||||
- 场景序列化允许存哪些字段
|
||||
- 运行时字段默认值和字段同步覆盖哪些字段
|
||||
|
||||
如果一个字段没进入这里,它后续也不会出现在 [TryGetClassFieldDefaultValues](TryGetClassFieldDefaultValues.md) 或字段同步链里。
|
||||
|
||||
## 返回值语义
|
||||
|
||||
- `true`
|
||||
- 类存在;即使类存在但筛完没有可用字段,也仍然可能返回 `true` 且 `outFields` 为空。
|
||||
- `false`
|
||||
- 类不存在,无法找到对应类元数据。
|
||||
|
||||
## 测试锚点
|
||||
|
||||
`tests/Scripting/test_mono_script_runtime.cpp` 里的
|
||||
`ClassFieldMetadataListsSupportedPublicInstanceFields`
|
||||
已经明确验证:`HiddenFlag` 会进入字段元数据,而未标注 private 字段不会。
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [MonoScriptRuntime](MonoScriptRuntime.md)
|
||||
- [TryGetClassFieldDefaultValues](TryGetClassFieldDefaultValues.md)
|
||||
- [IScriptRuntime::TryGetClassFieldMetadata](../../IScriptRuntime/TryGetClassFieldMetadata.md)
|
||||
- [Scripting Runtime And Field Model](../../../../_guides/Scripting/Scripting-Runtime-And-Field-Model.md)
|
||||
|
||||
Reference in New Issue
Block a user