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

193 lines
7.2 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](../../XCEngine/Scripting/Mono/MonoScriptRuntime/MonoScriptRuntime.md)。
这种设计和商业引擎常见的脚本体系很接近。它的核心收益不是“更抽象”,而是把三个原本会互相缠绕的问题拆开:
- 场景序列化
- 运行时生命周期
- 托管语言桥接
## 为什么 `ScriptComponent` 不直接等于“脚本实例”
`ScriptComponent` 更像“脚本绑定描述 + 字段持久化容器”,而不是托管对象本身。
这是因为:
- 场景在未运行时也要保存脚本类绑定
- Inspector 和序列化层在未启动 Mono 时也要工作
- 运行时实例应当可以被创建、销毁和重建,而不污染场景层数据结构
这和 Unity 的理念一致:序列化壳与运行时实例不是同一个对象。
## 字段到底有哪几层
当前字段值至少可能来自三层:
1. 类默认值
- 来自 `MonoScriptRuntime::TryGetClassFieldDefaultValues()`
- 它反映的是 C# 初始化后的真实默认值
2. 存储覆盖值
- 来自 `ScriptFieldStorage`
- 它是场景/本地持久化层
3. 活体托管值
- 来自当前正在运行的托管实例
这三层的意义不同:
- 类默认值回答“如果没有任何场景覆盖,这个字段原本该是什么”
- 存储覆盖值回答“场景目前记住了什么”
- 活体托管值回答“本次运行到现在,这个字段被脚本改成了什么”
## 当前哪些字段会进入字段模型
当前 Mono 后端的真实规则不是“所有字段都进来”,而是经过一层非常明确的筛选:
- 排除:
- `static`
- `const / literal`
- `readonly / init-only`
- 接受:
- `public` 实例字段
- `[SerializeField] private` 字段
- 最后还要满足当前支持的字段类型
这意味着:
- `public` 不是进入序列化/字段模型的唯一方式
- `private` 也不是天然不行,关键在于它是否被显式声明为可序列化字段
## 为什么支持 `[SerializeField] private`
这是当前文档里必须讲清楚的一条设计原则。
如果只允许 `public` 字段进入字段模型,会有两个直接问题:
- 脚本作者为了让字段能进 Inspector / 场景存储,只能把本来应该私有的实现细节也暴露成公开 API
- 类的封装边界会被序列化需求反向破坏
支持 `[SerializeField] private` 的好处是:
- 你可以保留封装,把类对外 API 面控制住
- 你仍然能让字段参与场景存储和 Inspector
- 字段重构时更安全,不会因为“想序列化”就把内部状态长期公开
这正是 Unity 一类商业引擎长期采用这套规则的核心原因。
## 为什么未标注的 private 字段仍然要忽略
因为不是所有 private 字段都应该持久化。
很多 private 字段只是:
- 临时缓存
- 运行时计数器
- 中间态标记
- 纯内部实现细节
如果把它们也自动纳入字段模型,会导致:
- 场景数据被临时状态污染
- Inspector 暴露出不该改的内部细节
- 运行时偶发状态被错误回写到持久化层
因此当前策略很明确:
- 想进入字段模型,就显式标 `[SerializeField]`
- 不标,就保持私有实现细节身份
## 为什么 `readonly` 也被排除
当前字段同步模型要求:
- 运行前把存储覆盖值写回托管实例
- 运行后把托管值同步回 `ScriptFieldStorage`
`readonly` 字段不适合承担这个双向同步职责,因此当前实现选择直接排除它。这是偏工程安全的取舍,不是功能缺失。
## 一个最直接的例子:`FieldMetadataProbe`
`managed/GameScripts/FieldMetadataProbe.cs` 当前几乎就是规则样本:
- `Health` / `Speed` / `Label` / `SpawnPoint` / `State` / `Target`
- 会进入字段元数据
- `HiddenFlag`
- 因为是 `[SerializeField] private`,也会进入字段元数据
- `IgnoredPrivateCounter`
- 因为是未标注的 private不会进入
- `SharedCounter`
- 因为是 `static`,不会进入
- `UnsupportedRotation`
- 因为 `Quaternion` 当前不在支持类型里,不会进入
这条规则已经被 `ClassFieldMetadataListsSupportedPublicInstanceFields` 和相关默认值测试直接覆盖。
## 一个更完整的运行时例子:`SerializeFieldProbe`
`managed/GameScripts/SerializeFieldProbe.cs` 展示了 `[SerializeField] private` 字段是如何真正走完整个同步链的。
当前测试场景做了这几步:
1.`ScriptFieldStorage` 里预先写入:
- `HiddenCounter = 42`
- `HiddenEnabled = false`
2. 启动运行时并创建托管实例
3. `CreateScriptInstance()` 把这两个存储覆盖值写回 private 字段
4. 脚本在 `Start()` 里读取并修改它们
5. `SyncManagedFieldsToStorage()` 再把更新后的值同步回存储
6. 场景序列化并重新加载后,值仍然能 round-trip 保持
同一测试还验证了一个边界:
- `IgnoredPrivateCounter` 这种未标注 private 字段不会被写入 `ScriptFieldStorage`
这正是当前字段模型“支持 `[SerializeField] private`,但保持内部状态边界”的关键体现。
## 为什么这种同步策略是保守但合理的
当前实现并不是“看到托管字段就自动持久化”,而是更保守地只同步已经进入字段模型的字段。
它的好处很现实:
- 不会把运行时临时字段偷偷带进场景数据
- 不会因为类里多了一个内部变量,就让序列化格式悄悄膨胀
- 字段迁移或类型变化时,边界更可控
这是更偏商业引擎工程安全的取舍。
## 推荐阅读顺序
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)