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