docs: update scripting API docs

This commit is contained in:
2026-04-02 22:23:29 +08:00
parent ec2891b16b
commit 3f9e286637
25 changed files with 776 additions and 76 deletions

View File

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