diff --git a/docs/api/XCEngine/Scripting/IScriptRuntime/IScriptRuntime.md b/docs/api/XCEngine/Scripting/IScriptRuntime/IScriptRuntime.md index cd65da6c..de208e26 100644 --- a/docs/api/XCEngine/Scripting/IScriptRuntime/IScriptRuntime.md +++ b/docs/api/XCEngine/Scripting/IScriptRuntime/IScriptRuntime.md @@ -10,13 +10,14 @@ ## 概览 -`IScriptRuntime` 是 `ScriptEngine` 唯一应该依赖的脚本后端接口。它把脚本后端抽象成三类能力: +`IScriptRuntime` 是 `ScriptEngine` 唯一应该直接依赖的脚本后端接口。当前这份契约覆盖四类能力: - 运行时启停。 -- 托管类/字段元数据查询与字段读写。 -- 脚本实例创建销毁与生命周期方法调用。 +- 可用脚本类发现、字段元数据和默认值读取。 +- 托管字段读写与字段同步。 +- 脚本实例创建/销毁与生命周期方法调用。 -这种设计让 `ScriptEngine` 能专注于“调度”,而把 Mono、GCHandle、程序集加载这些实现细节留给具体后端。 +这使得 `ScriptEngine` 只负责“什么时候该调什么”,而把 Mono 域、程序集加载、GCHandle 和 internal call 之类的细节留给具体实现。 ## 公开概念 @@ -33,6 +34,18 @@ - `OnDisable` - `OnDestroy` +### ScriptClassDescriptor + +`ScriptClassDescriptor` 用来表示一个可绑定脚本类: + +| 字段 | 说明 | +|------|------| +| `assemblyName` | 所属程序集,如默认的 `GameScripts`。 | +| `namespaceName` | 托管命名空间,可为空。 | +| `className` | 托管类名。 | + +`GetFullName()` 会把命名空间和类名拼成 `Namespace.Class` 形式;`ScriptEngine::TryGetAvailableScriptClasses()` 会直接返回这一结构的排序结果。 + ### ScriptRuntimeContext `ScriptRuntimeContext` 是后端执行脚本实例时的最小上下文: @@ -56,7 +69,9 @@ |------|------| | [OnRuntimeStart](OnRuntimeStart.md) | 运行时开始时的后端入口。 | | [OnRuntimeStop](OnRuntimeStop.md) | 运行时停止时的后端入口。 | +| [TryGetAvailableScriptClasses](TryGetAvailableScriptClasses.md) | 返回后端当前可绑定的脚本类列表。 | | [TryGetClassFieldMetadata](TryGetClassFieldMetadata.md) | 查询脚本类字段元数据。 | +| [TryGetClassFieldDefaultValues](TryGetClassFieldDefaultValues.md) | 查询脚本类字段默认值。 | | [TrySetManagedFieldValue](TrySetManagedFieldValue.md) | 向托管实例写字段。 | | [TryGetManagedFieldValue](TryGetManagedFieldValue.md) | 从托管实例读字段。 | | [SyncManagedFieldsToStorage](SyncManagedFieldsToStorage.md) | 把托管字段同步回本地存储。 | @@ -64,6 +79,12 @@ | [DestroyScriptInstance](DestroyScriptInstance.md) | 销毁脚本实例。 | | [InvokeMethod](InvokeMethod.md) | 调用生命周期方法。 | +## 当前契约边界 + +- 接口不承诺线程安全;当前调用点默认都在主线程。 +- 返回 `false` 可能代表类不存在、后端未初始化,或实现根本不支持该能力;调用方需要按方法语义区分。 +- `TryGetClassFieldDefaultValues()` 的默认值应反映托管类构造后的初始字段状态,而不只是原生类型零值。 + ## 相关文档 - [ScriptEngine](../ScriptEngine/ScriptEngine.md) diff --git a/docs/api/XCEngine/Scripting/IScriptRuntime/TryGetAvailableScriptClasses.md b/docs/api/XCEngine/Scripting/IScriptRuntime/TryGetAvailableScriptClasses.md new file mode 100644 index 00000000..9ca45fd7 --- /dev/null +++ b/docs/api/XCEngine/Scripting/IScriptRuntime/TryGetAvailableScriptClasses.md @@ -0,0 +1,30 @@ +# IScriptRuntime::TryGetAvailableScriptClasses + +**命名空间**: `XCEngine::Scripting` + +**类型**: `method` + +**头文件**: `XCEngine/Scripting/IScriptRuntime.h` + +## 签名 + +```cpp +virtual bool TryGetAvailableScriptClasses( + std::vector& outClasses) const = 0; +``` + +## 作用 + +返回当前后端已经发现、可以绑定给 `ScriptComponent` 的脚本类描述列表。 + +## 返回值语义 + +- 返回 `true`:后端支持类发现,且当前返回的数据可用。 +- 返回 `false`:后端未初始化、当前没有这项能力,或发现流程失败。 + +调用前实现应清空 `outClasses`;`ScriptEngine::TryGetAvailableScriptClasses()` 会在此基础上继续过滤空类名并排序。 + +## 相关文档 + +- [IScriptRuntime](IScriptRuntime.md) +- [ScriptEngine::TryGetAvailableScriptClasses](../ScriptEngine/TryGetAvailableScriptClasses.md) diff --git a/docs/api/XCEngine/Scripting/IScriptRuntime/TryGetClassFieldDefaultValues.md b/docs/api/XCEngine/Scripting/IScriptRuntime/TryGetClassFieldDefaultValues.md new file mode 100644 index 00000000..4be3e1d2 --- /dev/null +++ b/docs/api/XCEngine/Scripting/IScriptRuntime/TryGetClassFieldDefaultValues.md @@ -0,0 +1,40 @@ +# IScriptRuntime::TryGetClassFieldDefaultValues + +**命名空间**: `XCEngine::Scripting` + +**类型**: `method` + +**头文件**: `XCEngine/Scripting/IScriptRuntime.h` + +## 签名 + +```cpp +virtual bool TryGetClassFieldDefaultValues( + const std::string& assemblyName, + const std::string& namespaceName, + const std::string& className, + std::vector& outFields) const = 0; +``` + +## 作用 + +查询某个脚本类在初始状态下的字段默认值。 + +## 设计意义 + +这个接口不是简单返回“每种类型的零值”。当前 `ScriptEngine` 用它来: + +- 构建 `ScriptFieldModel` 的 `defaultValue` +- 在 `ClearScriptFieldOverrides()` 时把活体托管字段重置回类默认值 + +因此更合理的实现应该尽量反映托管类构造后的真实字段状态。 + +## 返回值语义 + +- 返回 `true`:后端确认类存在,并成功返回默认值列表。 +- 返回 `false`:类不存在、后端未初始化,或后端不支持默认值提取。 + +## 相关文档 + +- [TryGetClassFieldMetadata](TryGetClassFieldMetadata.md) +- [ScriptEngine::TryGetScriptFieldModel](../ScriptEngine/TryGetScriptFieldModel.md) diff --git a/docs/api/XCEngine/Scripting/Mono/Mono.md b/docs/api/XCEngine/Scripting/Mono/Mono.md index ac27c3ba..b1f9bf55 100644 --- a/docs/api/XCEngine/Scripting/Mono/Mono.md +++ b/docs/api/XCEngine/Scripting/Mono/Mono.md @@ -8,15 +8,15 @@ ## 概览 -`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。 -- 加载脚本核心程序集与游戏程序集。 -- 发现可用脚本类和公共实例字段。 -- 把 `ScriptComponent`、`GameObject` 与托管对象实例绑定起来。 -- 在生命周期调用后把托管字段同步回本地存储。 +- 解析 `MonoScriptRuntime::Settings`,推导程序集目录、脚本核心程序集和游戏程序集路径。 +- 加载 `XCEngine.ScriptCore.dll` 与 `GameScripts.dll`。 +- 发现应用程序集中的非抽象 `MonoBehaviour` 子类,并缓存支持的公共实例字段与生命周期方法。 +- 为 `ScriptComponent` 创建托管实例,注入 `GameObject`/`ScriptComponent` 上下文,并在生命周期后同步字段。 ## 为什么单独分目录 @@ -25,11 +25,21 @@ - 可以清楚区分“脚本系统公共契约”和“具体后端实现”。 - 以后如果接入别的脚本后端,这里天然就是平行扩展点。 +## 程序集来源 + +当前 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` 及其默认字段值。 + ## 当前实现边界 - 当前只实现了 Mono 后端,没有并列的 IL2CPP、Lua 或自研 VM 后端。 -- 目录里只有一个 public header,说明当前重点仍然是把单条托管脚本链路先跑顺。 -- 内部调用注册已覆盖 `GameObject`、`Transform`、`Camera`、`Light`、`MeshFilter`、`MeshRenderer` 和基础日志/时间桥接,但远不是完整编辑器级 API 面。 +- 目录里只有一个 public header,说明当前重点仍然是把一条 Mono 托管链路跑通。 +- 类发现只扫描当前应用程序集里的非抽象 `MonoBehaviour` 子类,不会把工具类或抽象基类暴露给脚本绑定 UI。 +- 字段发现只接受受支持类型的 `public` 非 `static` 实例字段。 +- internal call 目前已覆盖 `GameObject`、`Transform`、`Camera`、`Light`、`MeshFilter`、`MeshRenderer`、日志和时间桥接,但远不是完整编辑器级脚本 API 面。 ## 头文件 @@ -38,6 +48,7 @@ ## 相关指南 - [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) ## 相关文档 diff --git a/docs/api/XCEngine/Scripting/Mono/MonoScriptRuntime/MonoScriptRuntime.md b/docs/api/XCEngine/Scripting/Mono/MonoScriptRuntime/MonoScriptRuntime.md index 518cedd9..afe79be2 100644 --- a/docs/api/XCEngine/Scripting/Mono/MonoScriptRuntime/MonoScriptRuntime.md +++ b/docs/api/XCEngine/Scripting/Mono/MonoScriptRuntime/MonoScriptRuntime.md @@ -6,7 +6,7 @@ **头文件**: `XCEngine/Scripting/Mono/MonoScriptRuntime.h` -**描述**: 基于 Mono 的脚本运行时实现,负责程序集加载、类发现、实例创建、internal call 桥接和生命周期调用。 +**描述**: 基于 Mono 的脚本运行时实现,负责程序集解析与加载、类发现、实例创建、字段桥接、默认值读取和生命周期调用。 ## 概览 @@ -14,10 +14,28 @@ - 初始化 root domain 和 app domain。 - 加载脚本核心程序集与游戏程序集。 -- 发现继承 `MonoBehaviour` 的可用脚本类。 -- 构建类元数据缓存。 +- 发现继承 `MonoBehaviour` 的可用脚本类,并缓存生命周期方法与字段元数据。 +- 读取类字段默认值。 - 创建和销毁脚本实例。 - 通过 internal call 让托管脚本访问原生 `GameObject`、`Transform`、`Camera` 等能力。 +- 把本地字段覆盖写入托管实例,并在生命周期之后同步回 `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` 就是基于项目脚本程序集目录这样初始化的。 ## 生命周期 @@ -26,26 +44,22 @@ - [Shutdown](Shutdown.md) 会销毁 app domain、清空类缓存与实例缓存。 - 析构会自动调用 `Shutdown()`。 -## 常用访问器 - -- `IsInitialized()` -- `GetSettings()` -- `GetLastError()` - -这些访问器主要用于测试、诊断和工具层。当前文档对 `GetLastError()` 单独补页,因为它直接关系到失败排查。 - ## 设计要点 - `ScriptEngine` 不直接碰 Mono API;所有后端细节收敛在这个类里。 -- 类缓存和实例缓存都基于稳定键,便于场景重建和脚本回绑。 -- internal call 注册集中在本实现里,说明当前托管 API 面是围绕 Mono 后端组织的,而不是独立脚本 ABI。 +- 类缓存和实例缓存都基于稳定键,便于场景重建、类切换和实例回绑。 +- `TryGetClassFieldDefaultValues()` 会创建一个临时托管对象并读取字段值,因此脚本字段默认值可以反映 C# 初始化表达式,而不只是原生零值。 +- `CreateScriptInstance()` 会先注入上下文 UUID,再把本地存储里同名且类型匹配的字段写入托管实例。 +- `SyncManagedFieldsToStorage()` 只回写本地已有字段,保证运行时不会把临时字段偷偷持久化到场景。 ## 当前实现边界 - 当前只发现应用程序集中的非抽象 `MonoBehaviour` 子类。 - 支持的公共字段类型只覆盖 `float / double / bool / int32 / uint64 / string / Vector2 / Vector3 / Vector4 / GameObject`。 -- `SyncManagedFieldsToStorage()` 只会回写已经存在于 `ScriptFieldStorage` 中的字段;运行时临时字段不会自动持久化。 -- `OnRuntimeStop()` 只清理当前活动场景与实例,不会做完整 `Shutdown()`。 +- `TryGetAvailableScriptClasses()`、`GetScriptClassNames()` 和字段元数据查询都要求运行时已经初始化完成。 +- `SyncManagedFieldsToStorage()` 只会回写已经存在于 `ScriptFieldStorage` 中且类型仍匹配的字段;运行时临时字段不会自动持久化。 +- `OnRuntimeStop()` 只清理当前活动场景与实例,不会做完整 `Shutdown()`;程序集和类缓存会保留到显式 `Shutdown()` 或析构。 +- 目前没有程序集热重载、增量编译监听或多场景并发运行支持。 ## 公开方法 @@ -58,7 +72,11 @@ | [GetLastError](GetLastError.md) | 读取最近一次错误描述。 | | [IsClassAvailable](IsClassAvailable.md) | 查询脚本类是否已发现。 | | [GetScriptClassNames](GetScriptClassNames.md) | 返回已发现脚本类名列表。 | +| `IsInitialized()` | 判断运行时是否已完成初始化。 | +| `GetSettings()` | 返回已解析的运行时设置。 | +| [TryGetAvailableScriptClasses](TryGetAvailableScriptClasses.md) | 返回完整脚本类描述列表。 | | [TryGetClassFieldMetadata](TryGetClassFieldMetadata.md) | 读取脚本类字段元数据。 | +| [TryGetClassFieldDefaultValues](TryGetClassFieldDefaultValues.md) | 读取脚本类字段默认值。 | | [HasManagedInstance](HasManagedInstance.md) | 判断某脚本组件是否已有托管实例。 | | [GetManagedInstanceCount](GetManagedInstanceCount.md) | 返回当前托管实例数。 | | [GetManagedInstanceObject](GetManagedInstanceObject.md) | 读取托管对象裸指针。 | @@ -77,6 +95,7 @@ - `engine/src/Scripting/Mono/MonoScriptRuntime.cpp` - `tests/scripting/test_mono_script_runtime.cpp` +- `tests/scripting/test_project_script_assembly.cpp` ## 相关文档 @@ -84,3 +103,4 @@ - [IScriptRuntime](../../IScriptRuntime/IScriptRuntime.md) - [ScriptEngine](../../ScriptEngine/ScriptEngine.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) diff --git a/docs/api/XCEngine/Scripting/Mono/MonoScriptRuntime/TryGetAvailableScriptClasses.md b/docs/api/XCEngine/Scripting/Mono/MonoScriptRuntime/TryGetAvailableScriptClasses.md new file mode 100644 index 00000000..65489fbd --- /dev/null +++ b/docs/api/XCEngine/Scripting/Mono/MonoScriptRuntime/TryGetAvailableScriptClasses.md @@ -0,0 +1,32 @@ +# MonoScriptRuntime::TryGetAvailableScriptClasses + +**命名空间**: `XCEngine::Scripting` + +**类型**: `method` + +**头文件**: `XCEngine/Scripting/Mono/MonoScriptRuntime.h` + +## 签名 + +```cpp +bool TryGetAvailableScriptClasses( + std::vector& outClasses) const override; +``` + +## 当前实现流程 + +1. 清空 `outClasses`。 +2. 若运行时尚未初始化,直接返回 `false`。 +3. 遍历 `m_classes` 缓存,把每个条目转换成 `ScriptClassDescriptor`。 +4. 按 `assemblyName -> namespaceName -> className` 排序后返回。 + +## 返回内容边界 + +- 只来自当前已发现的应用程序集脚本类缓存。 +- 不包含抽象类。 +- 不包含非 `MonoBehaviour` 子类。 + +## 相关文档 + +- [MonoScriptRuntime](MonoScriptRuntime.md) +- [ScriptEngine::TryGetAvailableScriptClasses](../../ScriptEngine/TryGetAvailableScriptClasses.md) diff --git a/docs/api/XCEngine/Scripting/Mono/MonoScriptRuntime/TryGetClassFieldDefaultValues.md b/docs/api/XCEngine/Scripting/Mono/MonoScriptRuntime/TryGetClassFieldDefaultValues.md new file mode 100644 index 00000000..6f6e92c5 --- /dev/null +++ b/docs/api/XCEngine/Scripting/Mono/MonoScriptRuntime/TryGetClassFieldDefaultValues.md @@ -0,0 +1,48 @@ +# MonoScriptRuntime::TryGetClassFieldDefaultValues + +**命名空间**: `XCEngine::Scripting` + +**类型**: `method` + +**头文件**: `XCEngine/Scripting/Mono/MonoScriptRuntime.h` + +## 签名 + +```cpp +bool TryGetClassFieldDefaultValues( + const std::string& assemblyName, + const std::string& namespaceName, + const std::string& className, + std::vector& outFields) const override; +``` + +## 当前实现流程 + +1. 清空 `outFields`。 +2. 用 `(assemblyName, namespaceName, className)` 查找缓存的类元数据。 +3. 切换到当前 app domain。 +4. 创建一个临时托管对象并执行默认构造。 +5. 遍历缓存字段,逐个读取字段值并写入 `ScriptFieldDefaultValue`。 +6. 按 `fieldName` 排序后返回。 + +## 为什么它重要 + +这里返回的是托管类真实初始化后的字段值,所以它能反映: + +- C# 字段初始化表达式 +- 默认构造后留下的初始状态 + +`ScriptEngine::TryGetScriptFieldModel()` 和 `ClearScriptFieldOverrides()` 都依赖这条数据。 + +## 失败路径 + +- 类找不到 +- Mono 无法创建临时对象 +- 任意字段读取失败 + +这些情况都会返回 `false`,并清空输出。 + +## 相关文档 + +- [TryGetClassFieldMetadata](TryGetClassFieldMetadata.md) +- [Project Script Assembly And Field Sync](../../../../_guides/Scripting/Project-Script-Assembly-And-Field-Sync.md) diff --git a/docs/api/XCEngine/Scripting/NullScriptRuntime/NullScriptRuntime.md b/docs/api/XCEngine/Scripting/NullScriptRuntime/NullScriptRuntime.md index 5e5795d5..94dddcd5 100644 --- a/docs/api/XCEngine/Scripting/NullScriptRuntime/NullScriptRuntime.md +++ b/docs/api/XCEngine/Scripting/NullScriptRuntime/NullScriptRuntime.md @@ -10,18 +10,20 @@ ## 概览 -`NullScriptRuntime` 的作用不是执行脚本,而是让整个脚本系统在“没有真实脚本后端”的情况下仍然保持接口闭合: +`NullScriptRuntime` 的职责不是执行脚本,而是在“没有真实托管环境”时维持脚本系统接口闭合: -- `ScriptEngine` 仍然可以安全持有一个运行时对象。 -- 场景和脚本组件的原生数据层仍然可以工作。 -- 测试或工具链不需要为“没有运行时时指针为空”额外兜底。 +- `ScriptEngine` 默认就持有它,因此不会出现运行时指针为空的问题。 +- `ScriptComponent`、`ScriptFieldStorage`、场景序列化和字段编辑流程都仍然可以工作。 +- 编辑器或测试可以先走完整的脚本数据链路,再按需接入真实 Mono 后端。 -这是一种很典型的 Null Object 模式,在商业引擎底层模块中很常见。 +这是标准的 Null Object 模式,但它的“成功”语义必须按真实实现理解。 ## 当前实现行为 - 运行时启停是 no-op。 +- 可用脚本类查询总是失败,并清空输出数组。 - 元数据查询总是失败,并清空输出数组。 +- 默认值查询总是失败,并清空输出数组。 - 托管写字段总是返回 `true`,相当于“我接受这个请求,但没有实际后端可写”。 - 托管读字段总是返回 `false`。 - 同步字段是 no-op。 @@ -43,7 +45,9 @@ |------|------| | [OnRuntimeStart](OnRuntimeStart.md) | 空实现。 | | [OnRuntimeStop](OnRuntimeStop.md) | 空实现。 | +| [TryGetAvailableScriptClasses](TryGetAvailableScriptClasses.md) | 始终失败并清空输出。 | | [TryGetClassFieldMetadata](TryGetClassFieldMetadata.md) | 始终失败并清空输出。 | +| [TryGetClassFieldDefaultValues](TryGetClassFieldDefaultValues.md) | 始终失败并清空输出。 | | [TrySetManagedFieldValue](TrySetManagedFieldValue.md) | 始终返回 `true`。 | | [TryGetManagedFieldValue](TryGetManagedFieldValue.md) | 始终返回 `false`。 | | [SyncManagedFieldsToStorage](SyncManagedFieldsToStorage.md) | 空实现。 | diff --git a/docs/api/XCEngine/Scripting/NullScriptRuntime/TryGetAvailableScriptClasses.md b/docs/api/XCEngine/Scripting/NullScriptRuntime/TryGetAvailableScriptClasses.md new file mode 100644 index 00000000..83be0c16 --- /dev/null +++ b/docs/api/XCEngine/Scripting/NullScriptRuntime/TryGetAvailableScriptClasses.md @@ -0,0 +1,28 @@ +# NullScriptRuntime::TryGetAvailableScriptClasses + +**命名空间**: `XCEngine::Scripting` + +**类型**: `method` + +**头文件**: `XCEngine/Scripting/NullScriptRuntime.h` + +## 签名 + +```cpp +bool TryGetAvailableScriptClasses( + std::vector& outClasses) const override; +``` + +## 当前实现行为 + +- 清空 `outClasses` +- 直接返回 `false` + +## 含义 + +空运行时不会伪造脚本类列表。需要可绑定脚本类时,调用方必须接入真实后端,如 `MonoScriptRuntime`。 + +## 相关文档 + +- [NullScriptRuntime](NullScriptRuntime.md) +- [ScriptEngine::TryGetAvailableScriptClasses](../ScriptEngine/TryGetAvailableScriptClasses.md) diff --git a/docs/api/XCEngine/Scripting/NullScriptRuntime/TryGetClassFieldDefaultValues.md b/docs/api/XCEngine/Scripting/NullScriptRuntime/TryGetClassFieldDefaultValues.md new file mode 100644 index 00000000..4b92e0c9 --- /dev/null +++ b/docs/api/XCEngine/Scripting/NullScriptRuntime/TryGetClassFieldDefaultValues.md @@ -0,0 +1,32 @@ +# NullScriptRuntime::TryGetClassFieldDefaultValues + +**命名空间**: `XCEngine::Scripting` + +**类型**: `method` + +**头文件**: `XCEngine/Scripting/NullScriptRuntime.h` + +## 签名 + +```cpp +bool TryGetClassFieldDefaultValues( + const std::string& assemblyName, + const std::string& namespaceName, + const std::string& className, + std::vector& outFields) const override; +``` + +## 当前实现行为 + +- 忽略输入参数 +- 清空 `outFields` +- 直接返回 `false` + +## 含义 + +空运行时不会提供类默认值,因此 `ScriptEngine` 在这种模式下会回退到按字段类型生成默认值。 + +## 相关文档 + +- [NullScriptRuntime](NullScriptRuntime.md) +- [IScriptRuntime::TryGetClassFieldDefaultValues](../IScriptRuntime/TryGetClassFieldDefaultValues.md) diff --git a/docs/api/XCEngine/Scripting/ScriptComponent/ClearScriptClass.md b/docs/api/XCEngine/Scripting/ScriptComponent/ClearScriptClass.md new file mode 100644 index 00000000..cae95108 --- /dev/null +++ b/docs/api/XCEngine/Scripting/ScriptComponent/ClearScriptClass.md @@ -0,0 +1,32 @@ +# ScriptComponent::ClearScriptClass + +**命名空间**: `XCEngine::Scripting` + +**类型**: `method` + +**头文件**: `XCEngine/Scripting/ScriptComponent.h` + +## 签名 + +```cpp +void ClearScriptClass(); +``` + +## 作用 + +清空当前脚本组件的命名空间和类名绑定。 + +## 当前实现行为 + +- 内部等价于调用 `SetScriptClass(m_assemblyName, "", "")`。 +- 当前 `assemblyName` 会被保留。 +- 如果运行时正在运行且该组件原本已有脚本类,`ScriptEngine` 会收到类变化通知,并销毁旧跟踪实例。 + +## 设计含义 + +“清空绑定”不是单纯改两个字符串。对运行时来说,它意味着这个组件不再有可执行脚本类,应该停止继续调度生命周期。 + +## 相关文档 + +- [SetScriptClass](SetScriptClass.md) +- [ScriptEngine::OnScriptComponentClassChanged](../ScriptEngine/OnScriptComponentClassChanged.md) diff --git a/docs/api/XCEngine/Scripting/ScriptComponent/ScriptComponent.md b/docs/api/XCEngine/Scripting/ScriptComponent/ScriptComponent.md index dd0b45ea..85c4169b 100644 --- a/docs/api/XCEngine/Scripting/ScriptComponent/ScriptComponent.md +++ b/docs/api/XCEngine/Scripting/ScriptComponent/ScriptComponent.md @@ -6,22 +6,25 @@ **头文件**: `XCEngine/Scripting/ScriptComponent.h` -**描述**: 挂在 `GameObject` 上的脚本绑定组件,负责保存脚本类标识、组件 UUID 和字段存储。 +**描述**: 挂在 `GameObject` 上的脚本绑定组件,负责保存脚本类标识、组件 UUID 和字段覆盖存储。 ## 概览 -`ScriptComponent` 是脚本系统的数据入口。它本身不执行脚本逻辑,而是保存三类信息: +`ScriptComponent` 是脚本系统的数据入口。它本身不执行托管代码,而是保存三类关键信息: - 这个组件绑定的是哪个程序集、命名空间和类。 - 这个脚本组件自己的稳定 UUID。 - 这份脚本实例的可持久化字段缓存 `ScriptFieldStorage`。 -这和 Unity 场景里挂着的 `MonoBehaviour` 序列化槽位很接近,但当前实现更明确地把“数据层”和“运行时实例层”拆开了。 +这和 Unity 场景里挂着的 `MonoBehaviour` 槽位很接近,但当前实现更明确地区分了“原生数据层”和“运行时实例层”。 ## 生命周期 - 构造时会生成一个非零随机 `scriptComponentUUID`。 - 默认程序集名是 `GameScripts`。 +- 首次绑定脚本类时,会通知 `ScriptEngine::OnScriptComponentEnabled()`。 +- 已绑定脚本类发生变化时,会通知 `ScriptEngine::OnScriptComponentClassChanged()`,触发当前运行时实例停机并按新类重建。 +- `ClearScriptClass()` 会保留当前 `assemblyName`,只清空命名空间和类名。 - 启用、禁用、销毁回调会直接转发给 `ScriptEngine`。 - 序列化/反序列化会持久化 UUID、脚本类绑定和字段存储内容。 @@ -32,9 +35,9 @@ ## 当前实现边界 -- `SetScriptClass()` 只有在“之前没有脚本类,现在有了”这个转换点,才会主动通知 `ScriptEngine::OnScriptComponentEnabled()`。 -- 单纯修改已有脚本类名,不会自动触发一轮完整的重绑定流程。 +- 只有 `SetScriptClass()` / `ClearScriptClass()` 会通知 `ScriptEngine`;`SetAssemblyName()`、`SetNamespaceName()`、`SetClassName()` 是纯字段写入,不会自动触发重绑定。 - 反序列化使用引擎私有的分号分隔文本格式,不是通用 JSON/YAML。 +- `SetFieldStorage()` 直接整体覆盖本地字段缓存,不会自动把活体托管实例同步到同一状态。 ## 常用访问器 @@ -43,7 +46,7 @@ - `GetClassName()` / `SetClassName()` - `GetScriptComponentUUID()` -这些访问器大多是简单内联函数,因此当前文档重点放在会改变生命周期或序列化语义的核心方法上。 +这些访问器大多是简单内联函数,因此文档重点放在会影响运行时重建、字段语义或序列化行为的方法上。 ## 公开方法 @@ -51,6 +54,7 @@ |------|------| | [Constructor](Constructor.md) | 创建组件并生成 UUID。 | | [SetScriptClass](SetScriptClass.md) | 设置脚本类绑定。 | +| [ClearScriptClass](ClearScriptClass.md) | 清空脚本类绑定。 | | [HasScriptClass](HasScriptClass.md) | 判断当前是否已经绑定脚本类。 | | [GetFullClassName](GetFullClassName.md) | 返回带命名空间的完整类名。 | | [GetFieldStorage](GetFieldStorage.md) | 访问持久化字段缓存。 | @@ -64,6 +68,7 @@ - `engine/src/Scripting/ScriptComponent.cpp` - `tests/scripting/test_script_component.cpp` +- `tests/scripting/test_script_engine.cpp` ## 相关文档 diff --git a/docs/api/XCEngine/Scripting/ScriptComponent/SetScriptClass.md b/docs/api/XCEngine/Scripting/ScriptComponent/SetScriptClass.md index 352067eb..b90eda92 100644 --- a/docs/api/XCEngine/Scripting/ScriptComponent/SetScriptClass.md +++ b/docs/api/XCEngine/Scripting/ScriptComponent/SetScriptClass.md @@ -27,13 +27,17 @@ void SetScriptClass( - 两个重载都会先记录“之前是否已经有脚本类”。 - 然后覆盖程序集名、命名空间和类名。 -- 只有在“之前没有脚本类,设置后有脚本类”时,才会主动调用 `ScriptEngine::Get().OnScriptComponentEnabled(this)`。 +- 如果“之前没有脚本类,设置后有脚本类”,会调用 `ScriptEngine::Get().OnScriptComponentEnabled(this)`。 +- 如果之前已经绑定脚本类,并且程序集名 / 命名空间 / 类名发生变化,会调用 `ScriptEngine::Get().OnScriptComponentClassChanged(this)`。 ## 设计含义 -当前实现把“首次绑定脚本类”视作一个启用事件,但并没有把“换到另一个脚本类”也当成完整重建流程。这是一个当前版本的真实边界,用户不应该误以为修改类名会自动完成热切换。 +- 当前实现把“首次绑定脚本类”视作启用事件。 +- 已绑定类发生变化时,`ScriptEngine` 会停掉旧实例并按新类重新跟踪,这已经是当前实现的一部分。 +- 但 `SetAssemblyName()` / `SetNamespaceName()` / `SetClassName()` 这些原始 setter 不会触发同样的流程;需要真正重绑定时应走 `SetScriptClass()`。 ## 相关文档 +- [ClearScriptClass](ClearScriptClass.md) - [HasScriptClass](HasScriptClass.md) -- [OnEnable](OnEnable.md) +- [ScriptEngine::OnScriptComponentClassChanged](../ScriptEngine/OnScriptComponentClassChanged.md) diff --git a/docs/api/XCEngine/Scripting/ScriptEngine/ApplyScriptFieldWrites.md b/docs/api/XCEngine/Scripting/ScriptEngine/ApplyScriptFieldWrites.md new file mode 100644 index 00000000..0feb1fcd --- /dev/null +++ b/docs/api/XCEngine/Scripting/ScriptEngine/ApplyScriptFieldWrites.md @@ -0,0 +1,45 @@ +# ScriptEngine::ApplyScriptFieldWrites + +**命名空间**: `XCEngine::Scripting` + +**类型**: `method` + +**头文件**: `XCEngine/Scripting/ScriptEngine.h` + +## 签名 + +```cpp +bool ApplyScriptFieldWrites( + ScriptComponent* component, + const std::vector& requests, + std::vector& outResults); +``` + +## 作用 + +批量写脚本字段,并为每个请求返回独立状态。 + +## 当前实现流程 + +1. 先调用 `TryGetScriptFieldModel()` 建立字段模型。 +2. 针对每个请求逐项校验: + - 字段名不能为空 + - 值必须和声明类型兼容 + - 字段必须存在于当前模型 + - 如果类元数据可用,不能给 `StoredOnly` 遗留字段写值 + - 类型必须与模型字段类型一致 +3. 对已声明字段走 `TrySetScriptFieldValue()`,保证活体实例和本地存储同步更新。 +4. 对“类元数据缺失但本地仍有字段”的场景,允许直接写 `ScriptFieldStorage`。 +5. 把每条结果写入 `outResults`。 + +## 返回值语义 + +- 只有所有请求状态都是 `Applied` 时,返回 `true` +- 只要出现一项失败或诊断状态,就返回 `false` + +但无论整体布尔值如何,`outResults` 都是逐项结果的权威来源。 + +## 相关文档 + +- [ClearScriptFieldOverrides](ClearScriptFieldOverrides.md) +- [ScriptField](../ScriptField/ScriptField.md) diff --git a/docs/api/XCEngine/Scripting/ScriptEngine/ClearScriptFieldOverrides.md b/docs/api/XCEngine/Scripting/ScriptEngine/ClearScriptFieldOverrides.md new file mode 100644 index 00000000..964631be --- /dev/null +++ b/docs/api/XCEngine/Scripting/ScriptEngine/ClearScriptFieldOverrides.md @@ -0,0 +1,40 @@ +# ScriptEngine::ClearScriptFieldOverrides + +**命名空间**: `XCEngine::Scripting` + +**类型**: `method` + +**头文件**: `XCEngine/Scripting/ScriptEngine.h` + +## 签名 + +```cpp +bool ClearScriptFieldOverrides( + ScriptComponent* component, + const std::vector& requests, + std::vector& outResults); +``` + +## 作用 + +批量清理字段覆盖,并在可能时把活体托管字段恢复到类默认值。 + +## 当前实现流程 + +1. 先构建 `ScriptFieldModel`,再收集类默认值表。 +2. 对每个请求校验字段名是否为空、字段是否存在于模型。 +3. 如果字段属于当前脚本类且活体实例存在: + - 调用运行时 `TrySetManagedFieldValue()` + - 把托管值重置为该字段的类默认值 +4. 如果本地存储中有同名覆盖项,则移除该存储项。 +5. 如果既没有活体值可重置,也没有存储覆盖可删除,结果为 `NoValueToClear`。 + +## 返回值语义 + +- 只有所有请求都成功清理时,返回 `true` +- 任一请求出现 `UnknownField`、`EmptyFieldName`、`NoValueToClear` 或 `ApplyFailed`,整体返回 `false` + +## 相关文档 + +- [ApplyScriptFieldWrites](ApplyScriptFieldWrites.md) +- [ScriptEngine::TryGetScriptFieldModel](TryGetScriptFieldModel.md) diff --git a/docs/api/XCEngine/Scripting/ScriptEngine/OnScriptComponentClassChanged.md b/docs/api/XCEngine/Scripting/ScriptEngine/OnScriptComponentClassChanged.md new file mode 100644 index 00000000..8158a967 --- /dev/null +++ b/docs/api/XCEngine/Scripting/ScriptEngine/OnScriptComponentClassChanged.md @@ -0,0 +1,37 @@ +# ScriptEngine::OnScriptComponentClassChanged + +**命名空间**: `XCEngine::Scripting` + +**类型**: `method` + +**头文件**: `XCEngine/Scripting/ScriptEngine.h` + +## 签名 + +```cpp +void OnScriptComponentClassChanged(ScriptComponent* component); +``` + +## 作用 + +处理脚本组件在运行时中的类绑定变化。 + +## 当前实现流程 + +1. 忽略空指针。 +2. 如果运行时未启动,直接返回。 +3. 若该组件当前已被跟踪,则先执行 `StopTrackingScript(..., false)`: + - 如有实例,会触发 `OnDisable -> OnDestroy -> DestroyScriptInstance` + - 然后从跟踪表移除 +4. 如果组件已经没有脚本类,流程结束。 +5. 否则按新类重新 `TrackScriptComponent()`。 +6. 若新状态满足 `ShouldScriptRun()`,立即 `EnsureScriptReady(..., true)`,从而创建新实例并触发 `Awake / OnEnable`。 + +## 使用场景 + +`ScriptComponent::SetScriptClass()` 在已绑定类发生变化时会调用这里;`ClearScriptClass()` 也会走同一条停机路径。 + +## 相关文档 + +- [OnScriptComponentEnabled](OnScriptComponentEnabled.md) +- [ScriptComponent::SetScriptClass](../ScriptComponent/SetScriptClass.md) diff --git a/docs/api/XCEngine/Scripting/ScriptEngine/ScriptEngine.md b/docs/api/XCEngine/Scripting/ScriptEngine/ScriptEngine.md index 92a2050a..7d87ed50 100644 --- a/docs/api/XCEngine/Scripting/ScriptEngine/ScriptEngine.md +++ b/docs/api/XCEngine/Scripting/ScriptEngine/ScriptEngine.md @@ -6,7 +6,7 @@ **头文件**: `XCEngine/Scripting/ScriptEngine.h` -**描述**: 当前脚本系统的总调度器,负责运行时启停、脚本实例追踪、生命周期调用与字段模型拼装。 +**描述**: 当前脚本系统的总调度器,负责运行时启停、脚本实例追踪、生命周期调用、脚本类发现和字段模型拼装。 ## 概览 @@ -19,9 +19,11 @@ 当前它的核心职责包括: - 在运行时开始时收集场景中的脚本组件并建立追踪表。 +- 订阅 `Scene::OnGameObjectCreated()`,把运行中创建的新对象也纳入脚本追踪。 - 根据对象激活状态、组件启用状态和脚本类绑定状态决定脚本是否应当运行。 - 按顺序创建实例、调用 `Awake / OnEnable / Start / Update...`。 -- 在运行时字段、本地字段缓存和类元数据之间建立一致的读取模型。 +- 在运行时字段、本地字段缓存、类元数据和类默认值之间建立一致的读取模型。 +- 为编辑器/Inspector 提供脚本类列表、字段模型、批量写入和清除覆盖能力。 ## 生命周期 @@ -43,6 +45,17 @@ 这说明当前生命周期状态是显式状态机,而不是每次调用都从托管世界反查。 +## 脚本类与字段模型 + +除了生命周期调度,`ScriptEngine` 还负责把运行时暴露成编辑器可消费的数据接口: + +- `TryGetAvailableScriptClasses()` 返回排序后的 `ScriptClassDescriptor` 列表,并可按程序集过滤。 +- `TryGetScriptFieldModel()` 会把类声明字段、运行时默认值、存储覆盖值和活体托管值融合成 `ScriptFieldModel`。 +- `ApplyScriptFieldWrites()` 会逐项返回 `Applied / UnknownField / TypeMismatch / StoredOnlyField` 等状态。 +- `ClearScriptFieldOverrides()` 会把声明字段重置回类默认值,并删除本地存储中的覆盖项。 + +这也是当前 Inspector 和脚本字段编辑工具最该依赖的 API 面。 + ## 线程语义 - 当前实现没有锁。 @@ -53,8 +66,9 @@ - 只跟踪当前运行场景里的脚本组件。 - `Start` 生命周期会在第一次 `OnUpdate()` 前补发一次,而不是在 `OnRuntimeStart()` 里立即调用。 -- `TrySetScriptFieldValue()` 只有在后端能返回类字段元数据时,才会强校验字段名和类型。 -- `TryGetScriptFieldModel()` 会把类元数据、运行时值和本地存储值融合成一份快照模型,这对调试和编辑器非常重要。 +- `TrySetScriptFieldValue()` 只有在后端能返回类字段元数据时,才会强校验字段名和类型;否则会退回到纯本地存储写入。 +- `TryGetScriptFieldModel()` 会优先使用运行时返回的类默认值,而不是简单地把每种类型置零。 +- `TryGetScriptFieldSnapshots()` 在模型成功但字段为空时会返回 `false`,调用方不能把“返回 `false`”简单等同于“接口失败”。 ## 常用访问器 @@ -78,11 +92,15 @@ | [OnScriptComponentEnabled](OnScriptComponentEnabled.md) | 处理脚本组件启用事件。 | | [OnScriptComponentDisabled](OnScriptComponentDisabled.md) | 处理脚本组件禁用事件。 | | [OnScriptComponentDestroyed](OnScriptComponentDestroyed.md) | 处理脚本组件销毁事件。 | +| [OnScriptComponentClassChanged](OnScriptComponentClassChanged.md) | 处理脚本类绑定变化,销毁旧实例并按新类重建跟踪。 | | [HasTrackedScriptComponent](HasTrackedScriptComponent.md) | 查询某组件是否被跟踪。 | | [HasRuntimeInstance](HasRuntimeInstance.md) | 查询某组件是否已有运行时实例。 | | [GetTrackedScriptCount](GetTrackedScriptCount.md) | 返回当前跟踪脚本数。 | +| [TryGetAvailableScriptClasses](TryGetAvailableScriptClasses.md) | 返回可绑定脚本类列表。 | | [TrySetScriptFieldValue](TrySetScriptFieldValue.md) | 写脚本字段。 | | [TryGetScriptFieldValue](TryGetScriptFieldValue.md) | 读脚本字段。 | +| [ApplyScriptFieldWrites](ApplyScriptFieldWrites.md) | 批量写脚本字段并返回逐项状态。 | +| [ClearScriptFieldOverrides](ClearScriptFieldOverrides.md) | 批量清理字段覆盖并恢复默认值。 | | [TryGetScriptFieldModel](TryGetScriptFieldModel.md) | 构建完整字段模型。 | | [TryGetScriptFieldSnapshots](TryGetScriptFieldSnapshots.md) | 直接返回字段快照数组。 | @@ -91,6 +109,7 @@ - `engine/src/Scripting/ScriptEngine.cpp` - `tests/scripting/test_script_engine.cpp` - `tests/Scene/test_scene_runtime.cpp` +- `tests/scripting/test_mono_script_runtime.cpp` ## 相关文档 diff --git a/docs/api/XCEngine/Scripting/ScriptEngine/TryGetAvailableScriptClasses.md b/docs/api/XCEngine/Scripting/ScriptEngine/TryGetAvailableScriptClasses.md new file mode 100644 index 00000000..ba7412ca --- /dev/null +++ b/docs/api/XCEngine/Scripting/ScriptEngine/TryGetAvailableScriptClasses.md @@ -0,0 +1,37 @@ +# ScriptEngine::TryGetAvailableScriptClasses + +**命名空间**: `XCEngine::Scripting` + +**类型**: `method` + +**头文件**: `XCEngine/Scripting/ScriptEngine.h` + +## 签名 + +```cpp +bool TryGetAvailableScriptClasses( + std::vector& outClasses, + const std::string& assemblyName = std::string()) const; +``` + +## 作用 + +返回当前运行时可绑定的脚本类列表,并可按程序集过滤。 + +## 当前实现流程 + +1. 清空 `outClasses`。 +2. 调用运行时 `TryGetAvailableScriptClasses()`。 +3. 若传入了 `assemblyName`,只保留匹配该程序集的类。 +4. 过滤掉 `className` 为空的无效描述。 +5. 按 `assemblyName -> namespaceName -> className` 排序。 + +## 返回值语义 + +- 返回 `true`:运行时支持类发现,排序/过滤后的结果可用。 +- 返回 `false`:运行时不支持或当前不能返回类列表。 + +## 相关文档 + +- [ScriptEngine](ScriptEngine.md) +- [IScriptRuntime::TryGetAvailableScriptClasses](../IScriptRuntime/TryGetAvailableScriptClasses.md) diff --git a/docs/api/XCEngine/Scripting/ScriptEngine/TryGetScriptFieldModel.md b/docs/api/XCEngine/Scripting/ScriptEngine/TryGetScriptFieldModel.md index 6e35f9cd..18f8da4d 100644 --- a/docs/api/XCEngine/Scripting/ScriptEngine/TryGetScriptFieldModel.md +++ b/docs/api/XCEngine/Scripting/ScriptEngine/TryGetScriptFieldModel.md @@ -30,7 +30,7 @@ bool TryGetScriptFieldModel( 对于运行时可见的每个字段: -- 先用字段类型生成默认值。 +- 先尝试用运行时 `TryGetClassFieldDefaultValues()` 返回的类默认值;拿不到时才回退到按字段类型生成零值/空值。 - 如果本地存储里有同名字段,则记录 `storedType / storedValue`。 - 若类型不匹配,标记 `TypeMismatch`。 - 若托管实例可读该字段,则当前值来源记为 `ManagedValue`。 @@ -52,7 +52,7 @@ bool TryGetScriptFieldModel( 这套模型不是“为了文档好看”,而是当前脚本系统里最接近商业引擎 Inspector 数据模型的一层。它让工具或调试界面能回答这些关键问题: - 这个字段类里声明了吗? -- 当前显示的是托管值还是存储值? +- 当前显示的是类默认值、存储值还是托管值? - 本地存储是否已经和类定义不匹配? ## 相关文档 diff --git a/docs/api/XCEngine/Scripting/ScriptField/ScriptField.md b/docs/api/XCEngine/Scripting/ScriptField/ScriptField.md index b3b9c9a5..2128d345 100644 --- a/docs/api/XCEngine/Scripting/ScriptField/ScriptField.md +++ b/docs/api/XCEngine/Scripting/ScriptField/ScriptField.md @@ -6,7 +6,7 @@ **头文件**: `XCEngine/Scripting/ScriptField.h` -**描述**: 定义脚本字段类型系统、字段快照模型以及序列化/反序列化辅助函数。 +**描述**: 定义脚本字段类型系统、字段快照模型、批量写入状态以及序列化/反序列化辅助函数。 ## 概览 @@ -46,15 +46,58 @@ - `Math::Vector4` - `GameObjectReference` +### ScriptFieldMetadata / ScriptFieldDefaultValue + +- `ScriptFieldMetadata` 表示类里声明了一个什么字段。 +- `ScriptFieldDefaultValue` 表示该字段在托管类初始状态下的默认值。 + +`MonoScriptRuntime::TryGetClassFieldDefaultValues()` 会直接构造托管对象并读取这一层数据,因此它可以反映 C# 字段初始化表达式。 + ### ScriptFieldSnapshot / ScriptFieldModel -它们用于字段面板、字段模型比对和“已声明字段 vs 已存储字段”诊断。`ScriptEngine::TryGetScriptFieldModel()` 会用到这组结构。 +它们用于字段面板、字段模型比对和“已声明字段 vs 已存储字段”诊断。`ScriptEngine::TryGetScriptFieldModel()` 会把默认值、存储值和活体托管值融合到这组结构里。 + +相关状态枚举: + +- `ScriptFieldClassStatus` + `Unassigned / Available / Missing` +- `ScriptFieldValueSource` + `None / DefaultValue / StoredValue / ManagedValue` +- `ScriptFieldIssue` + `None / StoredOnly / TypeMismatch` + +### ScriptFieldWriteRequest / ScriptFieldWriteResult + +这组结构用于 `ScriptEngine::ApplyScriptFieldWrites()` 的批量写接口。 + +`ScriptFieldWriteStatus` 当前包括: + +- `Applied` +- `EmptyFieldName` +- `UnknownField` +- `InvalidValue` +- `TypeMismatch` +- `StoredOnlyField` +- `ApplyFailed` + +### ScriptFieldClearRequest / ScriptFieldClearResult + +这组结构用于 `ScriptEngine::ClearScriptFieldOverrides()` 的批量清理接口。 + +`ScriptFieldClearStatus` 当前包括: + +- `Applied` +- `EmptyFieldName` +- `UnknownField` +- `NoValueToClear` +- `ApplyFailed` ## 设计要点 - 字段类型集合是显式白名单,而不是任意模板反射。 - 文本序列化逻辑集中在这个头对应的实现里,保证 `ScriptFieldStorage` 和脚本组件共用同一套规则。 -- `StoredOnly`、`TypeMismatch` 这类问题状态被直接编码进快照模型,便于编辑器或调试工具解释当前字段状态。 +- `StoredOnly`、`TypeMismatch`、`DefaultValue / StoredValue / ManagedValue` 这些状态被直接编码进快照模型,便于编辑器或调试工具解释当前字段状态。 +- 批量写入/清理结果也被做成显式状态枚举,避免 UI 只能拿到一个粗糙的布尔值。 ## 公开函数 diff --git a/docs/api/XCEngine/Scripting/ScriptFieldStorage/ScriptFieldStorage.md b/docs/api/XCEngine/Scripting/ScriptFieldStorage/ScriptFieldStorage.md index fa1c4578..ba1e6783 100644 --- a/docs/api/XCEngine/Scripting/ScriptFieldStorage/ScriptFieldStorage.md +++ b/docs/api/XCEngine/Scripting/ScriptFieldStorage/ScriptFieldStorage.md @@ -6,11 +6,17 @@ **头文件**: `XCEngine/Scripting/ScriptFieldStorage.h` -**描述**: 以字段名为键保存脚本字段值的本地缓存容器。 +**描述**: 以字段名为键保存脚本字段覆盖值的本地缓存容器。 ## 概览 -`ScriptFieldStorage` 是当前脚本系统的数据缓冲层。它保存的不是“任意运行时变量”,而是需要在原生侧被识别、查询、序列化和可能回写的一组字段。 +`ScriptFieldStorage` 是当前脚本系统的数据缓冲层。它保存的不是“任意运行时变量”,而是需要在原生侧被识别、查询、序列化和可能回写的一组字段覆盖值。 + +在当前实现里它承担三种职责: + +- 场景序列化时持久化脚本字段覆盖值。 +- 运行前为 Inspector/工具提供可编辑的本地字段落点。 +- 运行中接收 `MonoScriptRuntime::SyncManagedFieldsToStorage()` 的回写。 ## 设计要点 @@ -23,6 +29,7 @@ - 不是线程安全容器。 - 不做类型自动转换。 +- 不负责解释字段是否仍被脚本类声明;这部分语义由 `ScriptEngine::TryGetScriptFieldModel()` 结合类元数据来判定。 - 遇到非法反序列化行时会跳过,而不是抛异常中断整个恢复流程。 ## 公开方法 diff --git a/docs/api/XCEngine/Scripting/Scripting.md b/docs/api/XCEngine/Scripting/Scripting.md index fe398dc1..69c84bab 100644 --- a/docs/api/XCEngine/Scripting/Scripting.md +++ b/docs/api/XCEngine/Scripting/Scripting.md @@ -4,39 +4,56 @@ **类型**: `module` -**描述**: 提供脚本组件数据模型、运行时调度器、字段序列化系统以及托管脚本后端抽象。 +**描述**: 提供脚本组件数据模型、字段覆盖存储、运行时调度器以及可替换的托管脚本后端抽象。 ## 概览 -`XCEngine::Scripting` 当前已经形成了一条比较清晰的运行链路: +当前脚本模块已经形成一条比较明确的“数据层 -> 调度层 -> 后端层”链路: -1. [ScriptComponent](ScriptComponent/ScriptComponent.md) 挂在 `GameObject` 上,保存脚本类绑定和序列化字段。 -2. [ScriptEngine](ScriptEngine/ScriptEngine.md) 在场景运行时追踪这些组件,管理实例创建、生命周期调用和字段同步。 -3. [IScriptRuntime](IScriptRuntime/IScriptRuntime.md) 定义脚本后端必须实现的统一桥接接口。 -4. [NullScriptRuntime](NullScriptRuntime/NullScriptRuntime.md) 提供无托管环境时的空实现兜底。 -5. [Mono](Mono/Mono.md) 子目录里的 [MonoScriptRuntime](Mono/MonoScriptRuntime/MonoScriptRuntime.md) 则是当前真正可运行的托管后端。 +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) 负责在没有真实托管后端时兜底,让脚本数据层和编辑流程仍然可工作。 +5. [Mono](Mono/Mono.md) 子目录中的 [MonoScriptRuntime](Mono/MonoScriptRuntime/MonoScriptRuntime.md) 是当前唯一的真实托管后端,负责加载 `XCEngine.ScriptCore.dll` 与 `GameScripts.dll`、发现脚本类、创建托管实例并完成字段桥接。 -如果从 Unity 的经验去理解,这一层大致对应: +如果从引擎分层角度理解: -- `ScriptComponent` 类似挂在对象上的脚本实例槽位。 -- `ScriptEngine` 类似引擎内部的脚本生命周期调度器。 -- `MonoScriptRuntime` 则像 C++ 引擎层和托管 `MonoBehaviour` 世界之间的桥梁。 - -但当前实现仍然比成熟商业引擎轻很多,文档必须明确区分“已经成立的真实行为”和“未来可能扩展的方向”。 +- `ScriptComponent + ScriptFieldStorage` 是可序列化的数据层。 +- `ScriptEngine` 是场景运行时的调度层。 +- `IScriptRuntime`/`MonoScriptRuntime` 是托管执行后端。 ## 设计要点 -- 把“脚本绑定数据”和“具体托管后端”拆开,可以让场景序列化不依赖 Mono。 -- `ScriptFieldStorage` 作为本地缓存,使脚本字段在无运行时、运行时和场景序列化三种状态间都能有统一落点。 -- `IScriptRuntime` 让引擎主流程只依赖抽象接口,方便以后接入别的脚本后端。 -- `ScriptEngine` 集中处理生命周期顺序,这比让各组件自己直接碰运行时更容易保证一致性。 +- 场景序列化只依赖 `ScriptComponent` 和 `ScriptFieldStorage`,不依赖 Mono 是否已经初始化。 +- 字段模型分成“类声明默认值、存储覆盖值、活体托管值”三层,便于 Inspector 和调试工具说明字段来源。 +- `ScriptEngine` 把生命周期顺序、运行中生成对象追踪、类切换重建都集中在一处。 +- `IScriptRuntime` 让主流程只依赖契约,后续可以接别的托管后端,而不是把 Mono API 直接扩散到引擎各处。 + +## 当前运行链路 + +1. `SceneRuntime` 启动场景时调用 `ScriptEngine::OnRuntimeStart(scene)`。 +2. `ScriptEngine` 先通知当前运行时启动,再递归收集场景里现有的 `ScriptComponent`,同时订阅 `Scene::OnGameObjectCreated()` 以追踪运行中创建的新对象。 +3. 对满足运行条件的组件,运行时创建托管实例;`MonoScriptRuntime` 会先把 `ScriptFieldStorage` 中同名且类型匹配的字段写入实例。 +4. `ScriptEngine` 统一按 `Awake -> OnEnable -> Start -> FixedUpdate/Update/LateUpdate` 驱动生命周期。 +5. 每次生命周期调用后,运行时执行 `SyncManagedFieldsToStorage()`;当前 Mono 实现只会回写“本地已存在且类型仍匹配”的字段。 +6. 编辑器或调试工具可通过 `ScriptEngine::TryGetScriptFieldModel()` / `TryGetScriptFieldSnapshots()` 看到字段默认值、覆盖值、活体值以及 `StoredOnly` / `TypeMismatch` 等诊断状态。 + +## 项目程序集来源 + +当 `XCENGINE_ENABLE_MONO_SCRIPTING` 打开时,`managed/CMakeLists.txt` 会构建两类程序集: + +- 引擎脚本核心程序集:`XCEngine.ScriptCore.dll` +- 游戏脚本程序集:`GameScripts.dll` + +除了测试用的 `build/managed` 输出,CMake 还会把项目 `project/Assets/**/*.cs` 编译到 `project/Library/ScriptAssemblies/GameScripts.dll`。`MonoScriptRuntime::Settings` 可以直接指向该目录,因此项目资产脚本和默认字段值已经进入当前文档的真实行为范围。 ## 当前实现边界 - 当前公开支持的脚本字段类型是有限集合:标量、字符串、`Vector2/3/4` 和 `GameObject` 引用。 - 生命周期覆盖 `Awake / OnEnable / Start / FixedUpdate / Update / LateUpdate / OnDisable / OnDestroy`。 -- `NullScriptRuntime` 只是桥接占位,不会真正执行脚本代码。 -- `MonoScriptRuntime` 当前围绕单个活动场景工作,还没有做域热重载、程序集增量刷新或完整编辑器脚本生态。 +- `NullScriptRuntime` 只是桥接占位,不会真正执行脚本代码,也不会返回脚本类列表或字段默认值。 +- `MonoScriptRuntime` 目前只发现应用程序集中的非抽象 `MonoBehaviour` 子类,不做热重载、域增量刷新或完整编辑器脚本生态。 +- 字段同步目前不会自动把“运行中新增但本地没有声明”的字段持久化下来。 ## 头文件 @@ -51,6 +68,7 @@ ## 相关指南 - [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`,以及默认值、存储覆盖和活体字段如何相互覆盖。 ## 相关文档 diff --git a/docs/api/_guides/Scripting/Project-Script-Assembly-And-Field-Sync.md b/docs/api/_guides/Scripting/Project-Script-Assembly-And-Field-Sync.md new file mode 100644 index 00000000..c3ef16b7 --- /dev/null +++ b/docs/api/_guides/Scripting/Project-Script-Assembly-And-Field-Sync.md @@ -0,0 +1,123 @@ +# Project Script Assembly And Field Sync + +## 这份指南解决什么问题 + +脚本模块这轮更新之后,已经不只是“Mono 能不能跑起来”的问题,还包括两条更具体的链路: + +1. `project/Assets/**/*.cs` 怎么进入当前运行时能发现的 `GameScripts.dll` +2. 字段默认值、场景存储覆盖值、活体托管值到底是怎么互相覆盖和同步的 + +这份指南就是把这两条链路放在一起说明。 + +## 项目脚本程序集是怎么生成的 + +`managed/CMakeLists.txt` 当前会构建两组托管输出: + +- 通用脚本核心程序集:`XCEngine.ScriptCore.dll` +- 游戏脚本程序集:`GameScripts.dll` + +同时它还会扫描 `project/Assets/**/*.cs`,并把这些项目资产脚本编译到: + +- `project/Library/ScriptAssemblies/XCEngine.ScriptCore.dll` +- `project/Library/ScriptAssemblies/GameScripts.dll` +- `project/Library/ScriptAssemblies/mscorlib.dll` + +如果项目目录下暂时没有任何 `.cs` 文件,CMake 会生成一个占位源文件,保证 `GameScripts.dll` 仍然存在。 + +## 运行时如何接入这份项目程序集 + +`MonoScriptRuntime::Settings` 可以只指定: + +- `assemblyDirectory` +- `corlibDirectory` +- 或更显式的 `coreAssemblyPath` / `appAssemblyPath` + +`ResolveSettings()` 会根据这些字段补全剩余路径。当前 `tests/scripting/test_project_script_assembly.cpp` 的做法就是把 `assemblyDirectory` 指到 `project/Library/ScriptAssemblies`,再验证运行时能发现项目资产脚本。 + +已验证的真实行为包括: + +- 运行时能发现 `ProjectScripts.ProjectScriptProbe` +- 能返回它的字段元数据 +- 能返回它的默认字段值: + - `EnabledOnBoot = true` + - `Label = "ProjectScriptProbe"` + - `Speed = 2.5f` + +项目样例脚本当前位于 [ProjectScriptProbe.cs](../../../../project/Assets/Scripts/ProjectScriptProbe.cs)。 + +## 字段值的三层来源 + +当前脚本字段至少可能来自三层: + +1. 类默认值 + 来自 `MonoScriptRuntime::TryGetClassFieldDefaultValues()`,反映 C# 初始化后的真实默认状态。 +2. 存储覆盖值 + 来自 `ScriptComponent::GetFieldStorage()`,会进入场景序列化。 +3. 活体托管值 + 来自当前运行中的托管实例。 + +`ScriptEngine::TryGetScriptFieldModel()` 会把这三层合并成 `ScriptFieldModel`: + +- `defaultValue` 表示类默认值 +- `storedValue` 表示场景/本地缓存中的覆盖值 +- `value` + `valueSource` 表示当前真正应该展示给 UI 的值 + +## 创建实例时字段怎么进入托管世界 + +`MonoScriptRuntime::CreateScriptInstance()` 当前会按这条顺序工作: + +1. 查类元数据 +2. 创建托管对象 +3. 注入 `gameObjectUUID` 和 `scriptComponentUUID` +4. 遍历 `ScriptFieldStorage` +5. 只把“字段名存在且类型匹配”的覆盖值写进托管实例 + +这意味着: + +- 场景里保存的覆盖值可以覆盖类默认值 +- 存储里已经遗留、但脚本类里不存在的字段,不会被写入托管实例 +- 类型不匹配的字段也不会被偷偷应用 + +## 生命周期后为什么还要回写 + +`ScriptEngine` 每次调用生命周期方法后,都会紧接着调用运行时的 `SyncManagedFieldsToStorage()`。 + +Mono 当前只会回写: + +- 本地已经存在于 `ScriptFieldStorage` 的字段 +- 且字段在类元数据里仍然存在 +- 且存储类型与类声明类型匹配 + +这样设计的好处是: + +- 不会把运行时临时字段自动污染到场景数据 +- 类型漂移会显式表现成 `TypeMismatch` +- 存储层和托管层的职责边界更清楚 + +## 批量编辑和清除覆盖怎么工作 + +当前推荐给编辑器用的不是直接操作 `ScriptFieldStorage`,而是: + +- [ScriptEngine::ApplyScriptFieldWrites](../../XCEngine/Scripting/ScriptEngine/ApplyScriptFieldWrites.md) +- [ScriptEngine::ClearScriptFieldOverrides](../../XCEngine/Scripting/ScriptEngine/ClearScriptFieldOverrides.md) + +原因很直接: + +- 批量写会同时校验类元数据、活体实例和本地存储 +- 清除覆盖会把活体托管字段恢复到类默认值,而不是只删一份本地缓存 + +如果脚本类已经丢失,系统仍允许对现有存储字段做有限编辑或删除,但会用 `StoredOnly` / `Missing` 这类状态明确告诉你当前已经脱离类声明。 + +## 当前限制 + +- 只支持受支持类型的 `public` 非 `static` 实例字段 +- 只发现应用程序集里的非抽象 `MonoBehaviour` 子类 +- 没有热重载或程序集增量刷新 +- 不会自动把“运行时新增但本地不存在”的字段持久化 + +## 推荐阅读 + +1. [Scripting](../../XCEngine/Scripting/Scripting.md) +2. [MonoScriptRuntime](../../XCEngine/Scripting/Mono/MonoScriptRuntime/MonoScriptRuntime.md) +3. [ScriptEngine](../../XCEngine/Scripting/ScriptEngine/ScriptEngine.md) +4. [ScriptField](../../XCEngine/Scripting/ScriptField/ScriptField.md) diff --git a/docs/api/_guides/Scripting/Scripting-Runtime-And-Field-Model.md b/docs/api/_guides/Scripting/Scripting-Runtime-And-Field-Model.md index e6e98cff..bd5c93e3 100644 --- a/docs/api/_guides/Scripting/Scripting-Runtime-And-Field-Model.md +++ b/docs/api/_guides/Scripting/Scripting-Runtime-And-Field-Model.md @@ -10,7 +10,7 @@ - `IScriptRuntime` - `MonoScriptRuntime` -这份指南的目标,是把它们放回同一条真实运行链路里解释清楚。 +这份指南的目标,是把它们放回同一条真实运行链路里解释清楚,并明确“类默认值、存储覆盖值、活体托管值”这三层数据是怎么叠加的。 ## 一句话理解当前架构 @@ -48,12 +48,25 @@ 这和 Unity Inspector 里的“脚本字段序列化层”在理念上很接近,但当前实现更轻量,也更直接。 +## 运行时里字段值到底有几层 + +当前字段值至少可能来自三层: + +1. 类默认值 + 来自运行时 `TryGetClassFieldDefaultValues()`;Mono 实现会真实构造一个托管对象并读取字段初始化结果。 +2. 存储覆盖值 + 来自 `ScriptFieldStorage`;它会在场景序列化、运行前编辑和生命周期回写之间持续存在。 +3. 活体托管值 + 来自当前运行中的托管实例;`ScriptEngine::TryGetScriptFieldModel()` 和 `TryGetScriptFieldValue()` 会优先读这一层。 + +`ScriptFieldSnapshot` 里的 `valueSource` 就是在说明当前展示值到底来自哪一层。 + ## 当前生命周期是怎么串起来的 真实顺序如下: 1. `SceneRuntime::Start()` 调用 `ScriptEngine::OnRuntimeStart(scene)`。 -2. `ScriptEngine` 收集场景里的 `ScriptComponent`。 +2. `ScriptEngine` 收集场景里的 `ScriptComponent`,并订阅 `Scene::OnGameObjectCreated()`,保证运行中生成的新对象也会被继续追踪。 3. 对满足运行条件的组件,调用运行时 `CreateScriptInstance()`。 4. 然后按顺序补发 `Awake`、`OnEnable`。 5. 第一次 `OnUpdate()` 前,会先补发一次 `Start`。 @@ -85,7 +98,7 @@ - 初始化 Mono 域并加载核心/游戏程序集。 - 发现继承 `MonoBehaviour` 的非抽象脚本类。 -- 枚举支持类型的公共实例字段。 +- 枚举支持类型的公共实例字段,并读取字段默认值。 - 创建托管实例,并注入 `GameObject` / `ScriptComponent` 上下文。 - 通过 internal call 桥接基础引擎 API。 - 生命周期调用后把已存储字段同步回本地缓存。 @@ -112,6 +125,16 @@ 这是一种更偏工程安全的选择。 +## 类切换为什么要走 `SetScriptClass()` + +当前运行时重绑定逻辑并不是“任意改三个字符串都能触发”。 + +- `ScriptComponent::SetScriptClass()` / `ClearScriptClass()` 会通知 `ScriptEngine`。 +- 运行时中的 `ScriptEngine::OnScriptComponentClassChanged()` 会停掉旧实例,再按新类重新创建和跟踪。 +- `SetAssemblyName()` / `SetNamespaceName()` / `SetClassName()` 只是原始字段 setter,不会触发这条流程。 + +也就是说,真正要做脚本类切换时,必须走显式重绑定 API。 + ## 推荐阅读顺序 如果你第一次接触这个模块,建议按下面顺序看: @@ -123,6 +146,7 @@ 5. [ScriptEngine](../../XCEngine/Scripting/ScriptEngine/ScriptEngine.md) 6. [IScriptRuntime](../../XCEngine/Scripting/IScriptRuntime/IScriptRuntime.md) 7. [MonoScriptRuntime](../../XCEngine/Scripting/Mono/MonoScriptRuntime/MonoScriptRuntime.md) +8. [Project Script Assembly And Field Sync](Project-Script-Assembly-And-Field-Sync.md) ## 相关 API diff --git a/docs/plan/API文档并行更新任务池_2026-04-02.md b/docs/plan/API文档并行更新任务池_2026-04-02.md index da298653..deb03670 100644 --- a/docs/plan/API文档并行更新任务池_2026-04-02.md +++ b/docs/plan/API文档并行更新任务池_2026-04-02.md @@ -62,8 +62,8 @@ ## T05 Scripting 模块内容重构 -- 状态: `TODO` -- 认领人: `未认领` +- 状态: `DONE` +- 认领人: `Codex` - 优先级: `P1` - 写入范围: `docs/api/XCEngine/Scripting/**`、`docs/api/_guides/Scripting/**` - 主要源码依据: `engine/include/XCEngine/Scripting/IScriptRuntime.h`、`Mono/MonoScriptRuntime.h`、`NullScriptRuntime.h`、`ScriptComponent.h`、`ScriptEngine.h` 及对应 `.cpp`;`tests/scripting/**`