# 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. `SceneRuntime::Start()` 调用 `ScriptEngine::OnRuntimeStart(scene)`。 2. `ScriptEngine` 收集场景里的 `ScriptComponent`。 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,而不是偷偷帮你转换。 这是一种更偏工程安全的选择。 ## 推荐阅读顺序 如果你第一次接触这个模块,建议按下面顺序看: 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) ## 相关 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)