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

5.8 KiB
Raw Blame History

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. 然后按顺序补发 AwakeOnEnable
  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
  2. ScriptComponent
  3. ScriptField
  4. ScriptFieldStorage
  5. ScriptEngine
  6. IScriptRuntime
  7. MonoScriptRuntime

相关 API