docs: sync api and planning docs

This commit is contained in:
2026-04-08 16:07:03 +08:00
parent 08c3278e10
commit 31756847ab
1826 changed files with 44502 additions and 29645 deletions

View File

@@ -22,9 +22,16 @@ virtual bool TryGetAvailableScriptClasses(
- 返回 `true`:后端支持类发现,且当前返回的数据可用。
- 返回 `false`:后端未初始化、当前没有这项能力,或发现流程失败。
调用前实现应清空 `outClasses``ScriptEngine::TryGetAvailableScriptClasses()` 会在此基础上继续过滤空类名并排序。
## 契约要求
- 实现应在调用前清空 `outClasses`
- 返回的应该是“可绑定类”,而不是后端里任意可见类型;例如抽象类、工具类或不满足脚本基类约束的类型,通常不应暴露给调用方。
- 描述里应尽量带上稳定的 `assemblyName`,因为上层会基于它做过滤和重绑定。
- 接口本身不强制排序,但提供稳定顺序是推荐行为;`ScriptEngine::TryGetAvailableScriptClasses()` 会在此基础上继续过滤空类名并排序。
## 相关文档
- [IScriptRuntime](IScriptRuntime.md)
- [ScriptEngine::TryGetAvailableScriptClasses](../ScriptEngine/TryGetAvailableScriptClasses.md)
- [NullScriptRuntime::TryGetAvailableScriptClasses](../NullScriptRuntime/TryGetAvailableScriptClasses.md)
- [MonoScriptRuntime::TryGetAvailableScriptClasses](../Mono/MonoScriptRuntime/TryGetAvailableScriptClasses.md)

View File

@@ -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 后端,没有并列的 IL2CPPLua 自研 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)
## 相关文档

View File

@@ -72,7 +72,7 @@ InternalCall_Object_Destroy
## 测试锚点
`tests/scripting/test_mono_script_runtime.cpp` 中的
`tests/Scripting/test_mono_script_runtime.cpp` 中的
- `UnityObjectApiSupportsHierarchyLookupAndDestroy`

View File

@@ -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`
## 相关文档

View File

@@ -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`
## 相关文档

View File

@@ -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)

View File

@@ -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)

View File

@@ -20,12 +20,34 @@ void ClearScriptClass();
- 内部等价于调用 `SetScriptClass(m_assemblyName, "", "")`
- 当前 `assemblyName` 会被保留。
- `namespaceName``className` 会被清空,因此 `HasScriptClass()` 立刻变成 `false`
- 如果运行时正在运行且该组件原本已有脚本类,`ScriptEngine` 会收到类变化通知,并销毁旧跟踪实例。
当前这条链路的实际停机顺序仍然是:
1. `OnDisable`
2. `OnDestroy`
3. `DestroyScriptInstance`
但因为组件字段会先被清空再通知引擎,所以停机过程中如果原生侧读取 `GetFullClassName()`,当前看到的是空字符串。
## 设计含义
“清空绑定”不是单纯改两个字符串。对运行时来说,它意味着这个组件不再有可执行脚本类,应该停止继续调度生命周期。
同时还要注意两点:
- `ClearScriptClass()` 不会重置 `assemblyName`
- `ClearScriptClass()` 不会删除 [ScriptFieldStorage](../ScriptFieldStorage/ScriptFieldStorage.md) 里的本地字段覆盖值。
这让脚本槽位可以被清空,但组件上的历史配置数据仍然保留在原生场景层里,便于后续重绑或调试。
## 真实行为依据
- `engine/src/Scripting/ScriptComponent.cpp`
- `engine/src/Scripting/ScriptEngine.cpp`
- `tests/Scripting/test_script_engine.cpp`
## 相关文档
- [SetScriptClass](SetScriptClass.md)

View File

@@ -14,30 +14,62 @@
- 这个组件绑定的是哪个程序集、命名空间和类。
- 这个脚本组件自己的稳定 UUID。
- 这份脚本实例的可持久化字段缓存 `ScriptFieldStorage`
- 这份脚本实例的可持久化字段缓存 [ScriptFieldStorage](../ScriptFieldStorage/ScriptFieldStorage.md)
这和 Unity 场景里挂着的 `MonoBehaviour` 槽位很接近,但当前实现更明确地区分了“原生数据层”和“运行时实例层”。
这和 Unity 场景里“脚本槽位”的定位很接近,但当前实现更明确地区分了:
## 生命周期
- 原生场景数据
- 运行时活体实例
也正因为如此,`ScriptComponent` 既可以在 Mono 未初始化时参与序列化和 Inspector 编辑,又可以在运行时作为 `ScriptEngine` 的实例描述对象。
## 生命周期与绑定语义
- 构造时会生成一个非零随机 `scriptComponentUUID`
- 默认程序集名是 `GameScripts`
- 首次绑定脚本类时,会通知 `ScriptEngine::OnScriptComponentEnabled()`
- 首次从“无脚本类”切到“有脚本类”时,会通知 `ScriptEngine::OnScriptComponentEnabled()`
- 已绑定脚本类发生变化时,会通知 `ScriptEngine::OnScriptComponentClassChanged()`,触发当前运行时实例停机并按新类重建。
- `ClearScriptClass()` 会保留当前 `assemblyName`,只清空命名空间和类名。
- 启用、禁用、销毁回调会直接转发给 `ScriptEngine`
- 序列化/反序列化会持久化 UUID、脚本类绑定和字段存储内容。
- 序列化 / 反序列化会持久化 UUID、脚本类绑定和字段存储内容。
这里有两个容易误判的实现细节:
- `HasScriptClass()` 当前只检查 `className` 是否非空。
- `SetScriptClass()` / `ClearScriptClass()` 是会触发运行时链路的高层 API`SetAssemblyName()` / `SetNamespaceName()` / `SetClassName()` 只是裸字段写入。
如果你只是改三个字符串,运行时并不会自动知道“类绑定已经变了”。
## 为什么它不是“托管实例本身”
很多脚本系统文档都会把“脚本组件”和“脚本实例”混着讲,但当前实现刻意没这么做。原因很现实:
- 场景资产在未运行时也要保存脚本绑定和字段覆盖值。
- 反序列化或拷贝场景对象时,不应该强依赖托管运行时已经启动。
- 编辑器大量操作只想读写字段和类名,不想立即创建 Mono 对象。
因此 `ScriptComponent` 更像“脚本实例描述与持久化容器”,而不是托管对象本体。
## 字段存储语义
`ScriptFieldStorage``ScriptComponent` 的内嵌成员,不单独分配。它承担的是“本地持久化覆盖值”这一层语义。
当前需要特别注意:
- `ClearScriptClass()` 不会清空字段存储;只是清空类绑定。
- `SetFieldStorage()` 是整体覆盖本地缓存,不会自动把当前活体托管实例同步成完全相同的状态。
- 运行时字段回写也是经由 `ScriptEngine -> IScriptRuntime::SyncManagedFieldsToStorage()` 完成,而不是 `ScriptComponent` 自己主动驱动。
## 所有权
- `ScriptComponent` 本身 `GameObject` 所有。
- `ScriptComponent` 本身 `GameObject` 所有。
- `ScriptFieldStorage` 作为成员对象直接内嵌,不单独分配。
## 当前实现边界
- 只有 `SetScriptClass()` / `ClearScriptClass()` 会通知 `ScriptEngine``SetAssemblyName()``SetNamespaceName()``SetClassName()` 是纯字段写入,不会自动触发重绑定。
- 反序列化使用引擎私有的分号分隔文本格式,不是通用 JSON/YAML。
- `SetFieldStorage()` 直接整体覆盖本地字段缓存,不会自动把活体托管实例同步到同一状态
- 反序列化使用引擎私有的分号分隔文本格式,不是通用 JSON / YAML。
- `ClearScriptClass()` 清空的是命名空间和类名,不会重置程序集名,也不会删除字段缓存
## 常用访问器
@@ -66,9 +98,10 @@
## 真实行为依据
- `engine/include/XCEngine/Scripting/ScriptComponent.h`
- `engine/src/Scripting/ScriptComponent.cpp`
- `tests/scripting/test_script_component.cpp`
- `tests/scripting/test_script_engine.cpp`
- `tests/Scripting/test_script_component.cpp`
- `tests/Scripting/test_script_engine.cpp`
## 相关文档

View File

@@ -23,18 +23,49 @@ void SetScriptClass(
设置当前脚本组件绑定的托管类信息。
## 重载差异
- `SetScriptClass(namespaceName, className)`
- 只修改命名空间和类名。
- 保留当前 `assemblyName`,默认场景下一般就是 `GameScripts`
- `SetScriptClass(assemblyName, namespaceName, className)`
- 同时修改程序集名、命名空间和类名。
## 当前实现行为
- 两个重载都会先记录“之前是否已经有脚本类”。
- 然后覆盖程序集名、命名空间和类名。
- 如果“之前没有脚本类,设置后有脚本类”,会调用 `ScriptEngine::Get().OnScriptComponentEnabled(this)`
- 如果之前已经绑定脚本类,并且程序集名 / 命名空间 / 类名发生变化,会调用 `ScriptEngine::Get().OnScriptComponentClassChanged(this)`
两个重载都遵循同一套顺序:
1. 记录调用前是否已有脚本类绑定
2. 计算绑定字符串是否真的发生变化
3. 先把新的程序集名 / 命名空间 / 类名写入 `ScriptComponent`
4. 如果“之前没有脚本类,设置后有脚本类”,调用 `ScriptEngine::Get().OnScriptComponentEnabled(this)`
5. 如果之前已经绑定脚本类,并且绑定字符串发生变化,调用 `ScriptEngine::Get().OnScriptComponentClassChanged(this)`
## 关键语义
- “首次绑定”走的是 `OnScriptComponentEnabled()` 路径,不是 `OnScriptComponentClassChanged()`
- “已绑定后改类”走的是显式重绑定路径。
- 如果新值和旧值完全相同,当前不会重复通知 `ScriptEngine`
- 因为新字符串是先写入再通知引擎,所以运行时收到类切换通知时,`ScriptComponent` 身上已经是新绑定。
最后这一点很重要。它意味着:
- 旧托管实例的停机确实会发生。
- 但如果停机过程里有原生侧日志或调试代码读取 `component->GetFullClassName()`,读到的会是新类名,而不是旧类名。
## 设计含义
- 当前实现把“首次绑定脚本类”视作启用事件。
- 已绑定类发生变化时,`ScriptEngine` 会停掉旧实例并按新类重新跟踪,这已经是当前实现的一部分
- `SetAssemblyName()` / `SetNamespaceName()` / `SetClassName()` 这些原始 setter 不会触发同样的流程;需要真正重绑定时应走 `SetScriptClass()`
- 已绑定类发生变化时,`ScriptEngine` 会停掉旧实例并按新类重新跟踪,这正是运行时重绑定的 canonical 入口
- `SetAssemblyName()` / `SetNamespaceName()` / `SetClassName()` 这些原始 setter 不会触发同样的流程;需要真正重绑定时应走 `SetScriptClass()`
这和商业引擎里常见的“改 Inspector 脚本槽位 = 触发一次受控重绑定”是同一种设计取向。
## 真实行为依据
- `engine/src/Scripting/ScriptComponent.cpp`
- `engine/src/Scripting/ScriptEngine.cpp`
- `tests/Scripting/test_script_engine.cpp`
## 相关文档

View File

@@ -0,0 +1,41 @@
# ScriptEngine::DefaultFixedDeltaTime
**命名空间**: `XCEngine::Scripting`
**类型**: `constant`
**头文件**: `XCEngine/Scripting/ScriptEngine.h`
## 定义
```cpp
static constexpr float DefaultFixedDeltaTime = 1.0f / 50.0f;
```
## 当前语义
这是 `ScriptEngine` 的默认运行时固定步长配置,当前值等于 `0.02f`,也就是 `50 Hz`
它会在以下场景被使用:
- `ScriptEngine` 初始构造时,`m_runtimeFixedDeltaTime` 用它初始化
- [SetRuntimeFixedDeltaTime](SetRuntimeFixedDeltaTime.md) 传入非正值时回退到该值
- [OnRuntimeStop](OnRuntimeStop.md) 清理运行态后把当前配置重置为该值
## 设计含义
它不是 `SceneRuntime` 的强制固定帧率控制器,而是 `ScriptEngine` 暴露给原生和托管脚本的“默认 fixed delta 配置值”。
当前具体某次 fixed tick 仍由上层调用 [OnFixedUpdate](OnFixedUpdate.md) 时传入的参数决定。
这和商业引擎里的固定步长设置很像:它更接近“推荐配置值”或“当前运行态默认值”,而不是单独一个能自动驱动物理循环的时钟。
## 托管侧可见性
当运行时没有显式配置过新的 fixed delta托管 `Time.fixedDeltaTime` 最终读到的也是这份默认值。
## 相关文档
- [SetRuntimeFixedDeltaTime](SetRuntimeFixedDeltaTime.md)
- [GetRuntimeFixedDeltaTime](GetRuntimeFixedDeltaTime.md)
- [OnRuntimeStop](OnRuntimeStop.md)

View File

@@ -0,0 +1,56 @@
# ScriptEngine::GetRuntimeFixedDeltaTime
**命名空间**: `XCEngine::Scripting`
**类型**: `method`
**头文件**: `XCEngine/Scripting/ScriptEngine.h`
## 签名
```cpp
float GetRuntimeFixedDeltaTime() const;
```
## 当前语义
返回 `ScriptEngine` 当前持有的运行时固定步长配置值,也就是 `m_runtimeFixedDeltaTime`
它的来源当前只有两类:
- [SetRuntimeFixedDeltaTime](SetRuntimeFixedDeltaTime.md) 写入的正值
- 默认回退值 [DefaultFixedDeltaTime](DefaultFixedDeltaTime.md)
## 生命周期行为
- 启动前如果已配置非默认值,[OnRuntimeStart](OnRuntimeStart.md) 会在内部 stop/start 之间保留这份配置。
- [OnRuntimeStop](OnRuntimeStop.md) 结束后,该值会重置为 [DefaultFixedDeltaTime](DefaultFixedDeltaTime.md)。
- 调用 [SetRuntimeFixedDeltaTime](SetRuntimeFixedDeltaTime.md) 并传入非正值,也会立刻回到默认值。
## 与 `OnFixedUpdate()` 的区别
这个 getter 返回的是“配置的固定步长”,不是“某次具体 fixed tick 传进来的参数”。
当前 [OnFixedUpdate](OnFixedUpdate.md) 仍使用调用方显式传入的 `fixedDeltaTime` 参数驱动脚本生命周期;如果上层把两者配置成不同值,脚本回调收到的 delta 与这里返回的配置值就可能出现偏差。
## 托管侧可见性
Mono 运行时的 `Time.fixedDeltaTime` 当前就是通过内部调用直接读取这个 getter因此它也是托管脚本看到的固定步长配置值。
这也是为什么测试里会同时区分:
- `FixedUpdate()` 里通过 `Time.deltaTime` 观察到的本次 fixed tick delta
- 以及通过 `Time.fixedDeltaTime` 观察到的当前配置值
## 真实行为依据
- `engine/src/Scripting/ScriptEngine.cpp`
- `engine/src/Scripting/Mono/MonoScriptRuntime.cpp`
- `managed/XCEngine.ScriptCore/Time.cs`
- `tests/Scripting/test_mono_script_runtime.cpp`
## 相关文档
- [SetRuntimeFixedDeltaTime](SetRuntimeFixedDeltaTime.md)
- [DefaultFixedDeltaTime](DefaultFixedDeltaTime.md)
- [OnFixedUpdate](OnFixedUpdate.md)

View File

@@ -16,16 +16,42 @@ void OnFixedUpdate(float fixedDeltaTime);
- 只在 `m_runtimeRunning` 为真时继续执行。
- 复制当前脚本顺序表,按顺序遍历。
- 本次回调真正传给脚本生命周期的是调用方传入的 `fixedDeltaTime` 参数。
- 对每个脚本要求同时满足:
- `ShouldScriptRun(state)`
- `EnsureScriptReady(state, true)`
- `state.enabled == true`
- 满足后调用运行时 `FixedUpdate` 生命周期。
- 每次生命周期调用后,`InvokeLifecycleMethod()` 还会立即触发 `SyncManagedFieldsToStorage()`
## 关键语义
- `EnsureScriptReady(state, true)` 不只是空检查;如果实例还不存在,它会创建实例,并补发 `Awake / OnEnable`
- 因此某个脚本第一次真正进入 fixed step 调度前,可能会先完成实例创建和启用流程。
- `Start` 不在这里触发;它仍然会留到第一次 [OnUpdate](OnUpdate.md) 前补发。
## 设计意义
复制顺序表再遍历,可以避免遍历过程中容器被增删时直接打乱当前帧顺序。
需要注意的是,[GetRuntimeFixedDeltaTime](GetRuntimeFixedDeltaTime.md) 返回的是另一条“运行时固定步长配置值”通道。当前 `OnFixedUpdate()` 并不会主动读取该配置并覆盖自己的参数,所以时间系统调用方应自行保证两者一致。
## 托管侧可见性
- 当前 fixed tick 传给托管 `Time.deltaTime` 的,是这里的 `fixedDeltaTime` 实参。
- 托管 `Time.fixedDeltaTime` 读到的,则是 [GetRuntimeFixedDeltaTime](GetRuntimeFixedDeltaTime.md) 返回的配置值。
如果上层把两者配成不同值,脚本在同一轮 fixed update 里就可能同时读到两份不一致的时间数据。
## 真实行为依据
- `engine/src/Scripting/ScriptEngine.cpp`
- `engine/src/Scripting/Mono/MonoScriptRuntime.cpp`
- `managed/XCEngine.ScriptCore/Time.cs`
- `tests/Scripting/test_mono_script_runtime.cpp`
## 相关文档
- [SetRuntimeFixedDeltaTime](SetRuntimeFixedDeltaTime.md)
- [GetRuntimeFixedDeltaTime](GetRuntimeFixedDeltaTime.md)
- [OnUpdate](OnUpdate.md)

View File

@@ -16,20 +16,41 @@ void OnRuntimeStart(Components::Scene* scene);
`engine/src/Scripting/ScriptEngine.cpp`
1.调用 `OnRuntimeStop()` 清理旧运行时
2. 如果 `scene == nullptr`,直接返回
3. 记录 `m_runtimeScene`,置 `m_runtimeRunning = true`
4. 调用当前运行时 `m_runtime->OnRuntimeStart(scene)`
5. 订阅 `scene->OnGameObjectCreated()`,保证运行中创建的新对象也会被脚本系统追踪
6. 递归收集场景现有的所有 `ScriptComponent`
7. 对满足 `ShouldScriptRun()` 的组件调用 `EnsureScriptReady(..., true)`,从而创建实例并触发 `Awake / OnEnable`
1.暂存当前 `m_runtimeFixedDeltaTime` 配置值
2. 调用 [OnRuntimeStop](OnRuntimeStop.md) 清理旧运行时
3. 把暂存的 fixed delta 配置恢复回 `m_runtimeFixedDeltaTime`
4. 如果 `scene == nullptr`,直接返回
5. 记录 `m_runtimeScene`,置 `m_runtimeRunning = true`
6. 调用当前运行时 `m_runtime->OnRuntimeStart(scene)`
7. 订阅 `scene->OnGameObjectCreated()`,保证运行中创建的新对象也会被脚本系统追踪
8. 递归收集场景现有的所有 `ScriptComponent`
9. 对满足 `ShouldScriptRun()` 的组件调用 `EnsureScriptReady(..., true)`,从而创建实例并触发 `Awake / OnEnable`
## 设计重点
## 关键语义
- 这一步不会直接触发 `Start``Start` 留到第一次 `OnUpdate()` 再补发。
- 这一步不会直接触发 `Start``Start` 留到第一次 [OnUpdate](OnUpdate.md) 再补发。
- 启动前通过 [SetRuntimeFixedDeltaTime](SetRuntimeFixedDeltaTime.md) 配好的 fixed delta不会被这次内部 [OnRuntimeStop](OnRuntimeStop.md) 清掉。
- 订阅场景创建事件,保证 runtime-spawn 出来的对象不会漏掉脚本初始化。
- 这里只会对当前已经满足运行条件的脚本调用 `EnsureScriptReady()`;未激活对象、禁用组件或未绑定类的组件不会在这里被强行创建实例。
## 为什么要先 stop 再 start
这种写法看上去有点“重”,但它解决的是工程上一致性问题:
- 避免旧场景残留跟踪状态带进新运行态。
- 保证 fixed delta、场景订阅和实例缓存都回到干净起点。
- 让“重新开始运行”与“第一次开始运行”共享同一条启动路径。
这和商业引擎里 Play 模式切换常见的“全量重建运行时态”思路一致。
## 真实行为依据
- `engine/src/Scripting/ScriptEngine.cpp`
- `tests/Scene/test_scene_runtime.cpp`
- `tests/Scripting/test_script_engine.cpp`
## 相关文档
- [SetRuntimeFixedDeltaTime](SetRuntimeFixedDeltaTime.md)
- [OnRuntimeStop](OnRuntimeStop.md)
- [OnUpdate](OnUpdate.md)

View File

@@ -18,19 +18,40 @@ void OnRuntimeStop();
- 若当前并未运行:
- 清空 `m_runtimeScene`
- 清空跟踪状态和顺序表
-`m_runtimeFixedDeltaTime` 重置为 [DefaultFixedDeltaTime](DefaultFixedDeltaTime.md)
- 返回
- 若正在运行:
- 遍历所有跟踪脚本,调用 `StopTrackingScript(..., true)`
- 清空状态表与顺序表
-`m_runtimeRunning` 置为 `false`
- 清空 `m_runtimeScene`
-`m_runtimeFixedDeltaTime` 重置为 [DefaultFixedDeltaTime](DefaultFixedDeltaTime.md)
- 最后调用 `m_runtime->OnRuntimeStop(stoppedScene)`
## 当前真实语义
`StopTrackingScript()` 会在实例存在时按 `OnDisable -> OnDestroy -> DestroyScriptInstance` 清理,因此这不是简单停更,而是完整结束脚本运行态。
此外,这一步也会把当前运行时固定步长配置恢复成默认值;如果下一次启动仍想使用非默认 fixed delta需要在启动前重新调用 [SetRuntimeFixedDeltaTime](SetRuntimeFixedDeltaTime.md)。
## 关键语义
- 即使当前 `m_runtimeRunning == false`,这个方法也仍然会清空 `m_runtimeScene`、跟踪表和 fixed delta 配置。它不只是“停止已运行中的脚本”,也是“把脚本运行时状态整体收回默认值”的统一入口。
- 只有在原本确实处于运行态时,才会调用底层运行时 `m_runtime->OnRuntimeStop(stoppedScene)`
- 场景创建事件的退订发生在最前面,避免停机过程中再被新对象创建事件打断。
## 设计意义
把“实例停机”“状态表清空”“fixed delta 恢复默认值”“通知具体运行时停机”统一收口到一个出口,可以减少 Play 模式切换和场景重启时的残留状态。
## 真实行为依据
- `engine/src/Scripting/ScriptEngine.cpp`
- `tests/Scripting/test_script_engine.cpp`
## 相关文档
- [DefaultFixedDeltaTime](DefaultFixedDeltaTime.md)
- [SetRuntimeFixedDeltaTime](SetRuntimeFixedDeltaTime.md)
- [OnRuntimeStart](OnRuntimeStart.md)
- [OnScriptComponentDestroyed](OnScriptComponentDestroyed.md)

View File

@@ -24,13 +24,36 @@ void OnScriptComponentClassChanged(ScriptComponent* component);
- 如有实例,会触发 `OnDisable -> OnDestroy -> DestroyScriptInstance`
- 然后从跟踪表移除
4. 如果组件已经没有脚本类,流程结束。
5. 否则按新类重新 `TrackScriptComponent()`
5. 否则按当前组件上的新绑定重新 `TrackScriptComponent()`
6. 若新状态满足 `ShouldScriptRun()`,立即 `EnsureScriptReady(..., true)`,从而创建新实例并触发 `Awake / OnEnable`
## 关键语义
- `ScriptComponent::SetScriptClass()` / `ClearScriptClass()` 是先写入组件上的新绑定,再调用这里。因此这个方法执行时,`component` 身上已经是“新类名”或“清空后的类名”。
- 这意味着“停掉旧实例”和“读取组件当前绑定字符串”是两件不同的事:
- 停掉的是旧托管实例
- 但如果此时有人通过 `component->GetFullClassName()` 看组件数据,看到的会是新绑定或空绑定
- 如果清空绑定,方法会只执行停机和移除跟踪,不会重新创建状态。
- 如果新绑定仍存在,但运行时后续无法创建新实例,这个方法不会回滚组件上的新绑定;组件会保留新类信息,只是当前没有活体实例。
## 使用场景
`ScriptComponent::SetScriptClass()` 在已绑定类发生变化时会调用这里;`ClearScriptClass()` 也会走同一条停机路径。
## 设计意义
商业引擎里脚本类切换通常都不是“裸改字段”,而是“显式重绑定动作”。当前实现把这条链路收口到 `ScriptEngine`,好处是:
- 生命周期停机顺序固定。
- 跟踪表和实例缓存不会漏清。
- 后续即便更换脚本后端,类切换的上层语义也能保持一致。
## 真实行为依据
- `engine/src/Scripting/ScriptEngine.cpp`
- `engine/src/Scripting/ScriptComponent.cpp`
- `tests/Scripting/test_script_engine.cpp`
## 相关文档
- [OnScriptComponentEnabled](OnScriptComponentEnabled.md)

View File

@@ -10,29 +10,56 @@
## 概览
`ScriptEngine` 是当前脚本模块里最重要的原生协调者。它自己不执行脚本字节码,也不保存场景数据,而是负责把下面几层真正串起来:
`ScriptEngine` 是当前脚本模块里最重要的原生协调者。它自己不执行脚本字节码,也不保存场景资产,而是把下面几层真正串起来:
- `SceneRuntime` 提供“场景开始运行/停止运行/每帧更新”的入口。
- `SceneRuntime` 提供“场景开始运行 / 停止运行 / 每帧更新”的入口。
- `ScriptComponent` 提供脚本绑定与字段缓存。
- `IScriptRuntime` 提供具体脚本后端能力。
这也是商业引擎里最常见的做法之一生命周期顺序和场景级追踪必须掌握在原生调度器手里不能散落在各个后端实现里否则脚本后端一换Play 模式语义就会漂。
当前它的核心职责包括:
- 在运行时开始时收集场景中的脚本组件并建立追踪表。
- 订阅 `Scene::OnGameObjectCreated()`,把运行中创建的新对象也纳入脚本追踪。
- 保存运行时固定步长配置,并把它暴露给托管时间查询接口。
- 根据对象激活状态、组件启用状态和脚本类绑定状态决定脚本是否应当运行。
- 按顺序创建实例、调用 `Awake / OnEnable / Start / Update...`
- 按顺序创建实例、调用 `Awake / OnEnable / Start / FixedUpdate / Update / LateUpdate`
- 在运行时字段、本地字段缓存、类元数据和类默认值之间建立一致的读取模型。
- 为编辑器/Inspector 提供脚本类列表、字段模型、批量写入和清除覆盖能力。
- 为编辑器 / Inspector 提供脚本类列表、字段模型、批量写入和清除覆盖能力。
## 生命周期
## 生命周期主控
- [Get](Get.md) 返回进程级单例。
- 默认运行时是内部持有的 `NullScriptRuntime`
- [SetRuntime](SetRuntime.md) 可替换具体后端;传 `nullptr` 时回退到空运行时。
- [OnRuntimeStart](OnRuntimeStart.md) / [OnRuntimeStop](OnRuntimeStop.md) 管理整条脚本运行链路。
- [OnRuntimeStart](OnRuntimeStart.md) 启动整条脚本运行链路。
- [OnRuntimeStop](OnRuntimeStop.md) 停止运行、销毁活体实例并清理跟踪表。
## 内部追踪模型
`OnRuntimeStart()` 的当前流程有两个很重要的实现细节:
- 它会先暂存当前 fixed delta 配置,再内部调用一次 `OnRuntimeStop()` 清旧状态,然后把配置写回去。
- 它会在通知运行时启动后,再递归收集场景中的脚本组件,并订阅 `Scene::OnGameObjectCreated()`
这让“切场景 / 重启 Play 模式 / 运行中生成对象”的脚本生命周期都收口到一条统一流程里。
## 固定步长配置与托管时间
`ScriptEngine` 当前把“固定步长配置值”和“某次 fixed tick 的实际回调参数”分成了两层:
- [SetRuntimeFixedDeltaTime](SetRuntimeFixedDeltaTime.md) 写入的是引擎保存的配置值。
- [OnFixedUpdate](OnFixedUpdate.md) 使用的是调用方本次传入的 `fixedDeltaTime`
通常这两者应该保持一致,但当前代码不会自动强制它们一致。
另外还要注意两点:
- [OnRuntimeStart](OnRuntimeStart.md) 会在内部 stop/start 之间保留启动前配置的 fixed delta。
- [OnRuntimeStop](OnRuntimeStop.md) 完成后会把配置重置回 [DefaultFixedDeltaTime](DefaultFixedDeltaTime.md)。
Mono 侧 `Time.fixedDeltaTime` 当前直接读取 [GetRuntimeFixedDeltaTime](GetRuntimeFixedDeltaTime.md),而 `Time.deltaTime` 则来自每次生命周期回调传入的 delta。因此脚本里这两个时间值虽然名字相近但当前来源并不相同。
## 运行时追踪模型
当前 `ScriptEngine` 通过 `(gameObjectUUID, scriptComponentUUID)` 唯一标识一个脚本实例状态,并保存:
@@ -43,9 +70,34 @@
- `startCalled`
- `startPending`
这说明当前生命周期状态是显式状态机,而不是每次调用都从托管世界反查。
这说明当前生命周期状态是显式状态机,而不是每次调用都从托管世界反查。好处是:
## 脚本类与字段模型
- 生命周期顺序可控。
- 运行中增删对象时更容易保持稳定顺序。
- 后端只需要关心“如何创建 / 销毁 / 调方法”,而不需要再自己维护一份场景状态机。
`ShouldScriptRun()` 当前还明确要求以下条件同时成立:
- 运行时已经启动。
- `state.context.scene == m_runtimeScene`
- 场景处于激活状态。
- `GameObject` 处于 hierarchy active。
- `ScriptComponent` 已启用。
- `ScriptComponent` 当前仍有脚本类绑定。
## 生命周期调用与字段回写
`ScriptEngine` 不是只做“调方法”这么简单。当前 `InvokeLifecycleMethod()` 在每次调用运行时生命周期方法后,都会立即调用 `SyncManagedFieldsToStorage()`
这意味着:
- 字段模型中的活体值和本地存储值会在生命周期边界上持续靠拢。
- Inspector 或调试工具不需要等到运行时结束,才有机会看到脚本侧最新写回的字段。
- 当前回写策略仍然是保守的,只同步本地已存在且类型匹配的字段。
这也是为什么 `ScriptEngine` 既是生命周期总调度器,也是脚本字段数据服务入口。
## 脚本类发现与重绑定
除了生命周期调度,`ScriptEngine` 还负责把运行时暴露成编辑器可消费的数据接口:
@@ -54,7 +106,13 @@
- `ApplyScriptFieldWrites()` 会逐项返回 `Applied / UnknownField / TypeMismatch / StoredOnlyField` 等状态。
- `ClearScriptFieldOverrides()` 会把声明字段重置回类默认值,并删除本地存储中的覆盖项。
这也是当前 Inspector 和脚本字段编辑工具最该依赖的 API 面。
类重绑定链路也收口在这里:
- `ScriptComponent::SetScriptClass()` / `ClearScriptClass()` 先更新组件上的绑定字符串。
- 然后由 `ScriptEngine::OnScriptComponentClassChanged()` 负责停掉旧实例、移除旧跟踪、按新绑定重新跟踪。
- 如果新绑定满足运行条件,会立即尝试 `EnsureScriptReady()`,也就是创建实例并补发 `Awake / OnEnable`
需要注意的是,`OnScriptComponentClassChanged()` 并不会回滚 `ScriptComponent` 上已经写入的新绑定。如果新类在运行时里不存在,或 `CreateScriptInstance()` 失败,组件仍然保持新绑定,只是当前没有活体实例。
## 线程语义
@@ -66,6 +124,7 @@
- 只跟踪当前运行场景里的脚本组件。
- `Start` 生命周期会在第一次 `OnUpdate()` 前补发一次,而不是在 `OnRuntimeStart()` 里立即调用。
- 当前不会自动把 [SetRuntimeFixedDeltaTime](SetRuntimeFixedDeltaTime.md) 的配置值与 [OnFixedUpdate](OnFixedUpdate.md) 的参数强制同步;时间系统调用方需要自己保持一致。
- `TrySetScriptFieldValue()` 只有在后端能返回类字段元数据时,才会强校验字段名和类型;否则会退回到纯本地存储写入。
- `TryGetScriptFieldModel()` 会优先使用运行时返回的类默认值,而不是简单地把每种类型置零。
- `TryGetScriptFieldSnapshots()` 在模型成功但字段为空时会返回 `false`,调用方不能把“返回 `false`”简单等同于“接口失败”。
@@ -73,17 +132,26 @@
## 常用访问器
- `GetRuntime()`
- `GetRuntimeFixedDeltaTime()`
- `IsRuntimeRunning()`
- `GetRuntimeScene()`
这些是简单内联访问器,当前文档重点放在影响状态机和运行行为的核心方法上。
## 公开常量
| 常量 | 说明 |
|------|------|
| [DefaultFixedDeltaTime](DefaultFixedDeltaTime.md) | 默认运行时固定步长配置,当前为 `1/50` 秒。 |
## 公开方法
| 方法 | 说明 |
|------|------|
| [Get](Get.md) | 获取全局脚本引擎实例。 |
| [SetRuntime](SetRuntime.md) | 设置具体脚本后端。 |
| [SetRuntimeFixedDeltaTime](SetRuntimeFixedDeltaTime.md) | 设置运行时固定步长配置。 |
| [GetRuntimeFixedDeltaTime](GetRuntimeFixedDeltaTime.md) | 读取当前运行时固定步长配置。 |
| [OnRuntimeStart](OnRuntimeStart.md) | 启动脚本运行时。 |
| [OnRuntimeStop](OnRuntimeStop.md) | 停止脚本运行时。 |
| [OnFixedUpdate](OnFixedUpdate.md) | 驱动固定步长脚本更新。 |
@@ -107,9 +175,9 @@
## 真实行为依据
- `engine/src/Scripting/ScriptEngine.cpp`
- `tests/scripting/test_script_engine.cpp`
- `tests/Scripting/test_script_engine.cpp`
- `tests/Scene/test_scene_runtime.cpp`
- `tests/scripting/test_mono_script_runtime.cpp`
- `tests/Scripting/test_mono_script_runtime.cpp`
## 相关文档

View File

@@ -0,0 +1,56 @@
# ScriptEngine::SetRuntimeFixedDeltaTime
**命名空间**: `XCEngine::Scripting`
**类型**: `method`
**头文件**: `XCEngine/Scripting/ScriptEngine.h`
## 签名
```cpp
void SetRuntimeFixedDeltaTime(float fixedDeltaTime);
```
## 当前实现行为
-`fixedDeltaTime > 0.0f` 时,直接把 `m_runtimeFixedDeltaTime` 设为该值。
-`fixedDeltaTime <= 0.0f` 时,回退到 [DefaultFixedDeltaTime](DefaultFixedDeltaTime.md)。
它不会立即驱动一次 `FixedUpdate`,也不会主动要求场景运行时调整时间步长;当前只是更新 `ScriptEngine` 持有的“运行时固定步长配置值”。
## 与运行时启停的关系
- 如果在 [OnRuntimeStart](OnRuntimeStart.md) 之前先配置该值,启动时会先暂存当前配置、内部调用一次 [OnRuntimeStop](OnRuntimeStop.md) 清理旧状态,再把这个配置值恢复回来,因此“启动前配置”不会被那次内部清理抹掉。
- 一旦显式执行 [OnRuntimeStop](OnRuntimeStop.md),当前配置会被重置回 [DefaultFixedDeltaTime](DefaultFixedDeltaTime.md)。
这意味着非默认 fixed delta 更像“本次运行前的配置值”,而不是跨 stop/start 永久保留的全局偏好。
## 与 `OnFixedUpdate()` 的关系
当前有两条相关但不完全相同的通道:
- [OnFixedUpdate](OnFixedUpdate.md) 的 `fixedDeltaTime` 参数决定本次 `FixedUpdate` 生命周期回调收到的 delta
- [GetRuntimeFixedDeltaTime](GetRuntimeFixedDeltaTime.md) 返回的是这里设置的运行时配置值
`ScriptEngine` 当前不会自动强制这两者一致。正常情况下应由上层时间系统保持同步。
## 托管侧可见性
Mono 运行时的 `Time.fixedDeltaTime` 内部调用当前直接读取 [GetRuntimeFixedDeltaTime](GetRuntimeFixedDeltaTime.md),因此这个配置值不仅影响原生侧说明,也会被托管脚本读到。
这也是为什么它虽然只是一个原生配置 setter却会直接影响脚本里 `Time.fixedDeltaTime` 的观察结果。
## 真实行为依据
- `engine/src/Scripting/ScriptEngine.cpp`
- `engine/src/Scripting/Mono/MonoScriptRuntime.cpp`
- `managed/XCEngine.ScriptCore/Time.cs`
## 相关文档
- [GetRuntimeFixedDeltaTime](GetRuntimeFixedDeltaTime.md)
- [DefaultFixedDeltaTime](DefaultFixedDeltaTime.md)
- [OnRuntimeStart](OnRuntimeStart.md)
- [OnRuntimeStop](OnRuntimeStop.md)
- [OnFixedUpdate](OnFixedUpdate.md)

View File

@@ -21,15 +21,35 @@ bool TryGetAvailableScriptClasses(
## 当前实现流程
1. 清空 `outClasses`
2. 调用运行时 `TryGetAvailableScriptClasses()`
3. 若传入了 `assemblyName`,只保留匹配该程序集的类
4. 过滤掉 `className` 为空的无效描述
5. `assemblyName -> namespaceName -> className` 排序
2. 调用当前运行时 `TryGetAvailableScriptClasses(runtimeClasses)`
3. 如果运行时返回 `false`,直接返回 `false`
4. 若传入了 `assemblyName`,只保留匹配该程序集的条目
5. 过滤掉 `className` 为空的无效描述
6.`assemblyName -> namespaceName -> className` 排序。
7. 返回 `true`
## 关键语义
- 这个方法消费的是“运行时已经发现好的类列表”,不会在这里重新扫描程序集,也不会为结果做实例创建验证。
- 如果运行时能提供列表,但过滤后一个类都不剩,当前仍返回 `true`,只是 `outClasses` 为空。
- 这一步的排序是给 Inspector / 脚本类选择 UI 做稳定输出用的;调用方不需要再按自己的规则重排一遍。
- 当前 `MonoScriptRuntime` 本身也会返回排序后的列表,但 `ScriptEngine` 仍然会再做一层规范化,这样换后端时对上层 UI 的输出形态更稳定。
## 返回值语义
- 返回 `true`运行时支持类发现,排序/过滤后的结果可用。
- 返回 `false`运行时不支持或当前不能返回类列表。
- 返回 `true`: 运行时支持类发现,排序 / 过滤后的结果可用。
- 返回 `false`: 运行时不支持或当前不能返回类列表。
## 典型用途
- Inspector 中的脚本类下拉列表。
- 运行时或编辑器中的脚本类过滤器。
- 需要显示 `Namespace.Class` 稳定排序结果的调试面板。
## 真实行为依据
- `engine/src/Scripting/ScriptEngine.cpp`
- `tests/Scripting/test_script_engine.cpp`
## 相关文档

View File

@@ -8,51 +8,87 @@
## 概览
当前脚本模块已经形成一条比较明确的“数据层 -> 调度层 -> 后端层”链路:
当前脚本模块已经形成一条明确的“场景数据层 -> 运行时调度层 -> 托管后端层”链路:
1. [ScriptComponent](ScriptComponent/ScriptComponent.md) 挂在 `GameObject` 上,保存程序集名、命名空间、类名、组件 UUID 和 [ScriptFieldStorage](ScriptFieldStorage/ScriptFieldStorage.md)。
2. [ScriptEngine](ScriptEngine/ScriptEngine.md) 在运行时收集场景内所有脚本组件,按 `(gameObjectUUID, scriptComponentUUID)` 跟踪实例状态,并统一驱动生命周期。
3. [IScriptRuntime](IScriptRuntime/IScriptRuntime.md) 定义“类发现、字段元数据、默认值读取、实例创建、生命周期调用、字段同步”的统一契约。
4. [NullScriptRuntime](NullScriptRuntime/NullScriptRuntime.md) 负责在没有真实托管后端时兜底,让脚本数据层和编辑流程仍然可工作。
4. [NullScriptRuntime](NullScriptRuntime/NullScriptRuntime.md) 在没有真实托管后端时兜底,让脚本数据层和编辑流程仍然可工作。
5. [Mono](Mono/Mono.md) 子目录中的 [MonoScriptRuntime](Mono/MonoScriptRuntime/MonoScriptRuntime.md) 是当前唯一的真实托管后端,负责加载 `XCEngine.ScriptCore.dll``GameScripts.dll`、发现脚本类、创建托管实例并完成字段桥接。
如果从引擎分层角度理解:
如果从商业级游戏引擎常见的分层方式理解:
- `ScriptComponent + ScriptFieldStorage` 是可序列化的数据层。
- `ScriptEngine` 是场景运行时的调度层。
- `IScriptRuntime`/`MonoScriptRuntime` 是托管执行后端。
- `IScriptRuntime` / `MonoScriptRuntime` 是托管执行后端。
## 设计要点
这种拆分和 Unity 一类引擎的设计取向很接近。核心目的不是“为了抽象而抽象”,而是把“场景持久化”“运行时生命周期”“托管语言桥接”三件本来容易互相缠死的事拆开。
- 场景序列化只依赖 `ScriptComponent``ScriptFieldStorage`,不依赖 Mono 是否已经初始化。
- 字段模型分成“类声明默认值、存储覆盖值、活体托管值”三层,便于 Inspector 和调试工具说明字段来源。
- `ScriptEngine` 把生命周期顺序、运行中生成对象追踪、类切换重建都集中在一处
- `IScriptRuntime` 让主流程只依赖契约,后续可以接别的托管后端,而不是把 Mono API 直接扩散到引擎各处
## 为什么当前要这样设计
- 场景、Prefab 或 Inspector 在运行时未启动时也必须能稳定保存脚本绑定和字段覆盖值,因此 `ScriptComponent` 不能直接等价于活体托管对象
- 生命周期顺序、场景内动态创建对象追踪、类切换重建这些规则需要由原生层统一控制,否则更换脚本后端时行为会漂
- 类发现、字段默认值读取和真正实例化都以编译后的程序集为事实来源能让“下拉列表里看到的类”“Inspector 里看到的默认值”“运行时真正创建出的类”尽量来自同一份元数据。
- 把 Mono API、GCHandle、internal call 注册等细节收敛在 `MonoScriptRuntime`,可以避免后端实现扩散到引擎各个模块。
## 托管桥接的当前真实语义
当前 Mono 桥接已经不是单纯“脚本能跑起来”这一层,而是直接暴露了一套和原生状态中心相连的脚本 API 面:
- `Time.deltaTime`
-`MonoScriptRuntime::InvokeMethod()` 在每次生命周期调用前写入 internal call 全局状态。
- `FixedUpdate()` 中读到的是本次 `OnFixedUpdate(fixedDeltaTime)` 传入的参数。
- `Update()` / `LateUpdate()` 中读到的是各自回调传入的逐帧 delta。
- `Time.fixedDeltaTime`
- 不读取某次 fixed tick 的历史参数,而是直接走 `ScriptEngine::GetRuntimeFixedDeltaTime()`
- 也就是说,它反映的是“当前运行时固定步长配置值”,不是“本次 `FixedUpdate()` 实参快照”。
- `Input`
- `Input.GetKey()` / `GetKeyDown()` / `GetKeyUp()``GetButton*()``GetAxis*()``anyKey` / `anyKeyDown` 都是对 [InputManager](../Input/InputManager/InputManager.md) 当前状态的直接转发。
- `GetKeyUp()` 当前对应的是释放边沿语义,也就是原生 `IsKeyReleased()`,不是“当前键没有按住”。
- `anyKey` / `anyKeyDown` 也会把鼠标按钮状态算进去,并继承输入系统的帧边界语义。
如果要进一步理解 `Pressed` / `Released` / `anyKeyDown` 的帧语义,应同时阅读 [Input Flow And Frame Semantics](../../_guides/Input/Input-Flow-and-Frame-Semantics.md)。
## 当前运行链路
1. `SceneRuntime` 启动场景时调用 `ScriptEngine::OnRuntimeStart(scene)`
2. `ScriptEngine` 先通知当前运行时启动,再递归收集场景里现有的 `ScriptComponent`,同时订阅 `Scene::OnGameObjectCreated()` 以追踪运行中创建的新对象。
3. 对满足运行条件的组件,运行时创建托管实例;`MonoScriptRuntime` 会先把 `ScriptFieldStorage` 中同名且类型匹配的字段写入实例。
4. `ScriptEngine` 统一按 `Awake -> OnEnable -> Start -> FixedUpdate/Update/LateUpdate` 驱动生命周期。
5. 每次生命周期调用后,运行时执行 `SyncManagedFieldsToStorage()`;当前 Mono 实现只会回写“本地已存在且类型仍匹配”的字段。
3. 对满足运行条件的组件,运行时创建托管实例;`MonoScriptRuntime` 会先注入 `gameObjectUUID` / `scriptComponentUUID`,再`ScriptFieldStorage` 中同名且类型匹配的字段写入实例。
4. `ScriptEngine` 统一按 `Awake -> OnEnable -> Start -> FixedUpdate / Update / LateUpdate` 驱动生命周期;其中 `Start` 会延后到第一次 `OnUpdate()` 前补发,而不是在 `OnRuntimeStart()` 中立即执行
5. 每次生命周期调用后,运行时都会执行 `SyncManagedFieldsToStorage()`;当前 Mono 实现只会回写“本地已存在且类型仍匹配”的字段。
6. 编辑器或调试工具可通过 `ScriptEngine::TryGetScriptFieldModel()` / `TryGetScriptFieldSnapshots()` 看到字段默认值、覆盖值、活体值以及 `StoredOnly` / `TypeMismatch` 等诊断状态。
## 脚本类发现与重绑定
当前“脚本类下拉列表”和“运行中改类名”的语义已经明确落在源码里:
- `MonoScriptRuntime` 只扫描应用程序集,也就是 `GameScripts.dll` 这一侧的非抽象 `MonoBehaviour` 子类,不会把 `XCEngine.ScriptCore.dll` 里的基础类型暴露成可绑定脚本类。
- `ScriptEngine::TryGetAvailableScriptClasses()` 在运行时列表之上追加两步规范化:
- 可选的按程序集过滤
-`assemblyName -> namespaceName -> className` 排序,并剔除空类名条目
- `ScriptComponent::SetScriptClass()` / `ClearScriptClass()` 是显式重绑定入口。
- 运行时里真正执行“停掉旧实例 -> 重新跟踪 -> 视情况立即创建新实例”的,是 [ScriptEngine::OnScriptComponentClassChanged](ScriptEngine/OnScriptComponentClassChanged.md)。
这套设计和商业引擎里常见的 Inspector 行为一致:类切换不是随便改几个字符串,而是走一条受控的重绑定链路。
## 项目程序集来源
`XCENGINE_ENABLE_MONO_SCRIPTING` 打开时,`managed/CMakeLists.txt` 会构建两类程序集:
- 引擎脚本核心程序集`XCEngine.ScriptCore.dll`
- 游戏脚本程序集`GameScripts.dll`
- 引擎脚本核心程序集: `XCEngine.ScriptCore.dll`
- 游戏脚本程序集: `GameScripts.dll`
除了测试用的 `build/managed` 输出CMake 还会把项目 `project/Assets/**/*.cs` 编译到 `project/Library/ScriptAssemblies/GameScripts.dll``MonoScriptRuntime::Settings` 可以直接指向该目录,因此项目资产脚本和默认字段值已经进入当前文档的真实行为范围
除了测试用的 `build/managed` 输出,同一套 CMake 还会把项目 `project/Assets/**/*.cs` 编译到 `project/Library/ScriptAssemblies/GameScripts.dll`如果项目暂时没有任何 `.cs` 文件,还会生成占位源文件,确保 `GameScripts.dll` 仍然存在
`MonoScriptRuntime::Settings` 可以直接指向 `project/Library/ScriptAssemblies`,因此项目资产脚本、它们的字段默认值、类发现结果,已经进入当前文档的真实行为范围,而不是“未来规划”。
## 当前实现边界
- 当前公开支持的脚本字段类型是有限集合标量、字符串、`Vector2/3/4``GameObject` 引用。
- 当前公开支持的脚本字段类型是有限集合: 标量、字符串、`Vector2/3/4``GameObject` 引用。
- 生命周期覆盖 `Awake / OnEnable / Start / FixedUpdate / Update / LateUpdate / OnDisable / OnDestroy`
- `NullScriptRuntime` 只是桥接占位,不会真正执行脚本代码,也不会返回脚本类列表或字段默认值。
- `MonoScriptRuntime` 目前只发现应用程序集中的非抽象 `MonoBehaviour` 子类,不做热重载、域增量刷新或完整编辑器脚本生态。
- `Time.fixedDeltaTime` 的配置值与 `OnFixedUpdate(fixedDeltaTime)` 的回调参数当前仍是两条通道,上层时间系统需要自己保持一致。
- 字段同步目前不会自动把“运行中新增但本地没有声明”的字段持久化下来。
## 头文件
@@ -67,8 +103,9 @@
## 相关指南
- [Scripting Runtime And Field Model](../../_guides/Scripting/Scripting-Runtime-And-Field-Model.md) - 解释当前脚本系统如何把场景、脚本字段缓存和 Mono 运行时衔接起来,以及为什么这样设计
- [Project Script Assembly And Field Sync](../../_guides/Scripting/Project-Script-Assembly-And-Field-Sync.md) - 解释 `project/Assets/**/*.cs` 如何进入 `GameScripts.dll`,以及默认值、存储覆盖和活体字段如何相互覆盖。
- [Scripting Runtime And Field Model](../../_guides/Scripting/Scripting-Runtime-And-Field-Model.md) - 把场景、脚本字段缓存、运行时调度和 Mono 后端放回同一条真实链路里解释清楚,并补上设计理念
- [Project Script Assembly And Field Sync](../../_guides/Scripting/Project-Script-Assembly-And-Field-Sync.md) - 解释 `project/Assets/**/*.cs` 如何进入 `GameScripts.dll`哪些类会被发现,以及默认值、存储覆盖和活体字段如何相互覆盖。
- [Input Flow And Frame Semantics](../../_guides/Input/Input-Flow-and-Frame-Semantics.md) - 解释托管 `Input` API 当前所继承的原生输入帧语义。
## 相关文档