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)

View File

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