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