Files
XCEngine/docs/api/_guides/Scripting/Scripting-Runtime-And-Field-Model.md

157 lines
7.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Scripting Runtime And Field Model
## 这份指南解决什么问题
只看单个 API 页,很难一下子理解当前脚本系统为什么同时存在:
- `ScriptComponent`
- `ScriptFieldStorage`
- `ScriptEngine`
- `IScriptRuntime`
- `MonoScriptRuntime`
这份指南的目标,是把它们放回同一条真实运行链路里解释清楚,并明确“类默认值、存储覆盖值、活体托管值”这三层数据是怎么叠加的。
## 一句话理解当前架构
当前脚本系统采用的是“原生场景数据层 + 运行时调度层 + 可替换托管后端”三段式设计:
1. 场景和对象层只保存脚本绑定与字段缓存。
2. `ScriptEngine` 负责生命周期和实例追踪。
3. 具体脚本代码执行交给 `IScriptRuntime` 的实现,当前实现是 `MonoScriptRuntime`
这种分层和商业引擎里常见的脚本体系很接近。它的核心收益,不是“代码显得更抽象”,而是让三个原本会互相缠死的问题被拆开:
- 场景序列化
- 运行时生命周期
- 托管语言桥接
## 为什么 `ScriptComponent` 不直接等于“脚本实例”
很多用户第一反应会是:既然组件名叫 `ScriptComponent`,为什么它不自己持有托管对象?
原因很现实:
- 场景在未运行时也需要保存脚本绑定信息。
- 场景加载、复制、序列化、反序列化时,不应该强依赖 Mono 已经初始化。
- 编辑器或工具链可能只想改字段,不想真正启动托管运行时。
所以当前 `ScriptComponent` 更像“脚本实例描述与持久化容器”,而不是托管对象本身。真正的托管实例是在运行期由 `ScriptEngine + IScriptRuntime` 动态创建的。
## 为什么还要有 `ScriptFieldStorage`
`ScriptFieldStorage` 的存在,是为了让脚本字段有一个不依赖托管运行时的落点。它解决了三类问题:
- 场景序列化时,字段值需要写进文本数据。
- 运行前,用户仍然需要能编辑脚本字段默认值。
- 运行中,托管对象的字段变化需要有机会回写到本地缓存。
这和 Unity Inspector 里的“脚本字段序列化层”在理念上很接近,但当前实现更轻量,也更直接。
## 运行时里字段值到底有几层
当前字段值至少可能来自三层:
1. 类默认值
来自运行时 `TryGetClassFieldDefaultValues()`Mono 实现会真实构造一个托管对象并读取字段初始化结果。
2. 存储覆盖值
来自 `ScriptFieldStorage`;它会在场景序列化、运行前编辑和生命周期回写之间持续存在。
3. 活体托管值
来自当前运行中的托管实例;`ScriptEngine::TryGetScriptFieldModel()``TryGetScriptFieldValue()` 会优先读这一层。
`ScriptFieldSnapshot` 里的 `valueSource` 就是在说明当前展示值到底来自哪一层。
## 当前生命周期是怎么串起来的
真实顺序如下:
1. `SceneRuntime::Start()` 调用 `ScriptEngine::OnRuntimeStart(scene)`
2. `ScriptEngine` 收集场景里的 `ScriptComponent`,并订阅 `Scene::OnGameObjectCreated()`,保证运行中生成的新对象也会被继续追踪。
3. 对满足运行条件的组件,调用运行时 `CreateScriptInstance()`
4. 然后按顺序补发 `Awake``OnEnable`
5. 第一次 `OnUpdate()` 前,会先补发一次 `Start`
6. 每个生命周期调用后,运行时会把托管字段同步回 `ScriptFieldStorage`
7. 停止运行时,按 `OnDisable -> OnDestroy -> DestroyScriptInstance` 回收。
这里最关键的一点,是当前引擎把生命周期顺序集中收口在 `ScriptEngine`,而不是让 Mono 运行时自己决定。这样未来即便更换脚本后端,生命周期语义仍然可以保持一致。
## `IScriptRuntime` 的真正意义
`IScriptRuntime` 不是为了“为了抽象而抽象”。它解决的是一个很实际的问题:
- 引擎主流程需要操心“什么时候创建实例、什么时候调用生命周期、字段怎么读写”。
- 但它不应该知道 Mono API、GCHandle、程序集加载这些实现细节。
因此 `IScriptRuntime` 只暴露引擎真正关心的契约:
- 运行时启停
- 类字段元数据查询
- 托管字段读写
- 实例创建/销毁
- 生命周期方法调用
这样 `ScriptEngine` 才能成为“脚本系统总调度器”,而不是某个具体运行时的包装类。
## 当前 Mono 方案做到了哪一步
`MonoScriptRuntime` 当前已经具备以下实际能力:
- 初始化 Mono 域并加载核心/游戏程序集。
- 发现继承 `MonoBehaviour` 的非抽象脚本类。
- 枚举支持类型的公共实例字段,并读取字段默认值。
- 创建托管实例,并注入 `GameObject` / `ScriptComponent` 上下文。
- 通过 internal call 桥接基础引擎 API。
- 生命周期调用后把已存储字段同步回本地缓存。
但也要明确它还没有做到什么:
- 没有热重载和程序集增量刷新。
- 没有完整的脚本调试器集成。
- 没有更细的域隔离和编辑器运行时分层。
- `SyncManagedFieldsToStorage()` 当前只回写“本地已经存在的字段”,不会自动持久化运行时临时新增字段。
## 为什么字段同步策略这么保守
当前字段同步只回写 `ScriptFieldStorage` 里已经存在且类型匹配的字段。这种设计看上去保守,但它有明确好处:
- 不会因为运行时临时字段把场景数据层污染得越来越不可控。
- 可以保住“场景里声明了哪些持久化字段”这条边界。
- 对编辑器和序列化格式更安全。
代价也很明显:
- 运行中动态产生的临时计数器、缓存值,不会自动进场景。
- 如果字段类型改了,系统会把它标成 mismatch而不是偷偷帮你转换。
这是一种更偏工程安全的选择。
## 类切换为什么要走 `SetScriptClass()`
当前运行时重绑定逻辑并不是“任意改三个字符串都能触发”。
- `ScriptComponent::SetScriptClass()` / `ClearScriptClass()` 会通知 `ScriptEngine`
- 运行时中的 `ScriptEngine::OnScriptComponentClassChanged()` 会停掉旧实例,再按新类重新创建和跟踪。
- `SetAssemblyName()` / `SetNamespaceName()` / `SetClassName()` 只是原始字段 setter不会触发这条流程。
也就是说,真正要做脚本类切换时,必须走显式重绑定 API。
## 推荐阅读顺序
如果你第一次接触这个模块,建议按下面顺序看:
1. [Scripting](../../XCEngine/Scripting/Scripting.md)
2. [ScriptComponent](../../XCEngine/Scripting/ScriptComponent/ScriptComponent.md)
3. [ScriptField](../../XCEngine/Scripting/ScriptField/ScriptField.md)
4. [ScriptFieldStorage](../../XCEngine/Scripting/ScriptFieldStorage/ScriptFieldStorage.md)
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
- [SceneRuntime](../../XCEngine/Scene/SceneRuntime/SceneRuntime.md)
- [ScriptComponent](../../XCEngine/Scripting/ScriptComponent/ScriptComponent.md)
- [ScriptEngine](../../XCEngine/Scripting/ScriptEngine/ScriptEngine.md)
- [MonoScriptRuntime](../../XCEngine/Scripting/Mono/MonoScriptRuntime/MonoScriptRuntime.md)