跳至主要内容
SheetForge

创作内核——构建第二个创作界面

进阶内容。面向想要在 SheetForge 引擎之上构建自己的创作 UI(例如节点图画布)的资源/工具作者。使用 Data Studio 的游戏团队不需要阅读本页。

创作窗口本身并不是引擎。Data Studio——以及在它之外的那个浏览器应用——都只是一个与窗口无关的创作内核的使用方

它们所做的一切都是通过公共类型驱动的:暂存、验证、反映编排、撤销边界、重新导入——第三个创作界面同样可以用这些类型来驱动。已经有两个创作界面在这样做了,这就是这道接缝确实存在、而不只是纸面设想的实际证明。

在着手构建一整个创作界面之前,先确认是否已经有一个扩展点能满足需求。一个插件可以在完全不拥有自己窗口的情况下,为随产品发布的窗口添加动作、面板、徽标和单元格部件——这些内容以数据的形式描述,因此既能在编辑器中渲染,能在浏览器中渲染——见插件开发 §4.16。本页面针对的是你确实想要拥有自己的画布这种情形。

一个消费者模拟测试程序集(SheetForge.Tests.Consumer,不对 Core 或 Editor 使用 InternalsVisibleTo)仅凭公共 API 就端到端地实现了一个虚拟的创作界面。如果它所需要的某个成员是 internal 的,该程序集就无法编译(CS0122),因此它就是下文所描述的这套界面的可执行规范。

三对象引擎

┌─────────────────────┐     ┌──────────────────────────┐     ┌───────────────┐
│  AuthoringSession   │────▶│   AuthoringDispatcher    │────▶│ BaselineStore │
│  (staging state)    │     │   .Reflect()             │     │ (round-trip   │
│                     │     │   (the full cycle)       │     │  snapshots)   │
└─────────────────────┘     └────────────┬─────────────┘     └───────────────┘
                                         │ binds
                            ┌────────────▼─────────────┐
                            │ AuthoringDispatchCallbacks│
                            │ (view concerns — YOUR UI) │
                            └──────────────────────────┘

AuthoringSession——暂存状态

一个 [Serializable] 的普通类(刻意不做成 ScriptableObject):把它保存在你 EditorWindow 的一个 [SerializeField] 字段中,你就能免费获得 Unity 原生的 Undo 快照能力,以及跨域重载的存活能力——这正是这些内置窗口的 Ctrl+Z 背后所依赖的同一套机制。

它拥有全部暂存状态:

  • 单元格编辑(Edits)、新增行(NewRows)、结构操作(StructOps);
  • 按标签页的重排序(Reorders)、标签页重命名(TabRenames);
  • baseline 锚点、隔离的编辑。

在这些状态之上,它还暴露了相应的变更/查询 API:

  • SetStaged(...)——暂存一次单元格编辑。编辑携带的是一个逻辑地址(标签页 · RecordId · 字段);物理行序号只是一个在反映之前才重新解析的派生缓存。
  • ResolveBaselineEdits(provider)——将所有编辑重新锚定到当前 baseline 上。可解析的编辑会继续推进;三种无法解析的情形(外部重命名 / 外部删除 / 键冲突)会被移入 IsolatedEdits——从反映中排除,如实地打上徽标,绝不静默丢弃,也绝不阻塞会话。
  • baseline 读取接口:TabNamesTryGetBaselineTable(tab, out SheetTable)——无需你自己动手解析,就能获得带类型的架构访问(TypeToken、@desc@overlap)。
  • EffectiveStructOps() / PendingStructCount()——组合后的、规范的结构操作视图。
  • 重映射钩子(RemapFieldName / RemapRecordId / RemapTab)让暂存状态在各种重命名之间保持一致。
  • LastProjectionResult 缓存最近一次的投影结果。

AuthoringDispatcher——反映编排

var dispatcher = new AuthoringDispatcher(session, callbacks, baselineStore);
dispatcher.Reflect();   // the entire cycle, one call

Reflect() 会依次执行整个周期:

  • 预检验证
  • 按来源分别反映——本地是精准写入,Google 是安全重写,自定义提供方则是你自己的目标
  • 保留状态的清理
  • ClearUndo 确认边界
  • 带报告的自动重新导入

此外还有:

  • BuildProjectionResult()——把当前暂存状态无副作用地投影为一个 ImportResult(相当于"假设已经反映"的验证)。可用于实时错误徽标。
  • 公开的 Session / Callbacks / Baselines——自定义来源提供方会用它们来组装自己的反映目标。

AuthoringDispatchCallbacks——你的 UI 的契约

调度器会为每一个视图相关的关注点调用一组共 13 个通用委托:ResolveSettings、确认对话框(ConfirmKeyRenamesConfirmTabRenames 等)、RenderReport(一个 Action<ImportReport>——可为 null,纯粹是观察性的)、PushApproverTriggerReimportClearUndoRebuild 等等。

另有 14 个专属于内置 Local/Google 来源的对话框委托,存放在一个独立的可选 BuiltInSourceDialogs 组合包中——外部创作界面或提供方从不需要绑定它们。

这些内置窗口绑定的是"会弹出对话框"的默认实现;你的画布可以绑定自己的实现(或者什么都不做)。引擎本身从不绘制任何 UI。

图形素材

对于一种"节点 = 记录,边 = 引用 ∪ 声明"的投影:

  • ReferenceScanner(Core)——用于枚举所有表中引用出现位置的唯一真实来源:标量、列表元素、显式默认值。它与引用验证器所使用的枚举完全相同,因此你的图和验证结果天然保持一致。Scan(tables) / ScanTable / ScanField / IsReferenceField
  • IEdgeContributor / EdgeSpec / EdgeContributorRegistry(Core)——领域插件用它们来声明扫描器看不到的边(自定义类型值内部、type 列关联、带 payload 记录的记录边)。可以通过 Editor 的 PluginRegistry.BuildEdgeContributors 收集它们。
  • ReferenceIndex / RecordEdge(Core)——Data Studio 自己的画布所运行于其上的那份组装完成的快照:Build(...) 会把扫描到的引用与贡献者提供的边合并一次,此后 OutEdges / InEdges / InCount 都能以每条记录 O(1) 的复杂度作答。完整的成员列表见 API 参考
  • IRecordCanvasAugmenter / CanvasAugmentBuilder(Core)——按标签页的覆盖契约,如果你希望领域包能以它们扩展 Studio 画布的同样方式(虚拟节点、额外的边、层级与显示提示)来扩展你自己的画布,就可以使用它。
  • ProjectionErrorMapper(Editor,纯函数)——把投影错误的物理坐标(标签页/行/字段)映射为逻辑地址(标签页/RecordId/字段),这样你就可以把错误徽标钉在节点上,而不是行号上。

支撑组件

类型你的创作界面如何使用它
ImportEvents两条事件总线,都是公共契约ImportCompletedImportCompletedArgsTabs · BakeFolder)会在自动链一路跑完烘焙之后触发,订阅者此时可以读取烘焙出的资源。BaselineUpdatedBaselineUpdatedArgsTabs · Quarantined)则会在每一次工作表快照被保存时触发——包括一次验证失败的运行——如果你的创作界面想要展示失败的工作表并让人们去修复它们,订阅的就是这一条总线。如果你的视图既展示工作表又展示烘焙取值,就把两者都订阅上;并在 OnDisable 中对称地取消订阅。
IPipelineObserver如果需要知道这件事的是一个插件而不是一个窗口,这是更轻量的路径:注册一个观察者,在每次导入周期结束时收到一份不可变的 PipelineRunView,完全不依赖编辑器——它在浏览器宿主中同样有效。见插件开发 §4.17
RecordIdMinter为新记录建议 id——前缀检测 + 避免冲突的唯一化处理。这是一个建议性质的 API,刻意不做成自动编号。
EphemeralSoApply把暂存取值临时预览到烘焙的 SO 上(重新导入会还原)。只应用可计算的子集;对待处理的列和解析失败会如实返回跳过原因。产品自带的界面中已经没有任何东西会驱动它了,因此想要这个预览功能的创作界面,需要自己提供对应的按钮。
KeyRenamePlanner规划键重命名(3 阶段:提取 / 传播 / 手术式改写),与这个内置窗口所用的方式相同。标签页重命名确认改为经由公开的 ConfirmTabRenames 回调完成。
SourceProviderRegistry以设置 UI 相同的方式解析当前生效的来源提供方。

内核强制执行的基本规则(你也会一并继承)

  • 工作表保持权威性——你的创作界面负责暂存和反映;它绝不会写入 SO。
  • 先验证,再反映——如果预检失败,Reflect() 什么都不会写入。
  • 不会静默丢失——无法解析的编辑会带着原因被隔离;确认操作都会经过你的回调。
  • 撤销原生集成——把 session 保存在一个已序列化的字段中,并在你的窗口上注册撤销快照;ClearUndo 标记反映边界。
  • 与领域无关——内核不包含任何领域词汇(有守护测试保证)。你的领域是通过插件契约进入的,而不是通过修改内核。

相关页面

  • API 参考——这里提到的一切的具体签名
  • 插件开发——你的领域在内核之外还会用到的契约
  • Data Studio——你的创作界面所复现或取代的行为