跳至主要内容
SheetForge

功能与限制

本页列出了 SheetForge 不做的、目前还做不到的,或者与你预期不同的一切行为,并附上原因、变通方法,以及未来是否有改进空间。

每个条目的格式为:是什么 / 为什么 / 变通方法(在有意义的情况下附上未来空间)。


1. 平台与依赖

Addressables 是必需的——没有它管线会被锁定

  • 是什么: 按地址加载就是运行时路径,AssetRef@Group 类型也需要 com.unity.addressables,因此必须安装该包才能使用本资源。资源本身在没有它的情况下也能编译——所有使用 Addressables 的代码都被包裹在一个 SHEETFORGE_ADDRESSABLES 版本 define 之后,只有在包存在时才会启用。
  • 缺失时的行为: 整条管线(导入·导出·推送·创作写回)会处于锁定,而非降级状态——运行任何入口点都会显示一条安装提示并停止。不存在任何部分或静默的替代路径(不会有"跳过资源键验证"这样的回退)。由于 Editor 在没有该包时也能编译,它永远不会进入 Safe Mode:入门窗口会正常打开,其 Addressables 行会显示 ✗,并带有一个打开 Package Manager按钮。无依赖的 SheetForge.Setup 引导程序仍然保留,作为应对其他、不相关的编译失败情形的引导安全网。
  • 在 Data Studio 中,没有该包时: 资源单元格的 ⊙ 选择器按钮及其拖放目标都会被禁用,原因会以工具提示的形式呈现;手动输入地址依然可行。
  • 为什么不做 Resources/Addressables 双轨抽象: 刻意没有构建——被判定为过度工程。
  • 变通方法: 安装 Addressables(Asset Store 的导入提示会在编译之前处理好这件事;如果你跳过了它,能够编译的 Editor 会引导你完成安装)。
  • 验证: 两条分支都经过了实测——移除版本 define 后(模拟"Addressables 缺失"的情况),产品与测试程序集能以 0 个错误编译通过;恢复后则是 0 个错误、0 个警告。另外还进行了端到端的独立验证:将本资源导入一个未安装 Addressables 的全新项目,结果项目正常编译,安装引导窗口也按设计出现。

Unity Localization 是可选的——只有 StringTable 同步会等待它

  • 是什么: 本地化表(@loc)、LocRef 引用、键常量、覆盖率报告、导出、推送、xlsx 以及 web 应用,在不安装 com.unity.localization 的情况下也都能正常工作。唯一会等待的是 StringTable 同步这个出口:它会显示一条安装提示(每个会话一次)然后停下。所有触及该包的代码都被包裹在一个 SHEETFORGE_LOCALIZATION 版本 define 之后,因此没有该包时,每个程序集、每一行生成代码依然能编译通过——生成的字段永远是一个普通的 LocRef 结构体,绝不是包内的类型。
  • 缺失时的行为: 其他任何地方都不会降级,也不会被静默跳过——工作表依然是完整的工作表;只有同步这个出口被锁定,并会显示原因。
  • 支持的版本: 1.5 或更高。
  • 变通方法: 当你想要那些表格时再安装该包;在那之前创作的一切,都会在下一次完成的导入中同步。见本地化表格

没有一键式程序化安装

  • 是什么: 安全网窗口只会引导你,它本身不会安装该包。同样的规则也适用于 Unity Localization 的安装提示。
  • 为什么: Asset Store 的上架规则限制了以程序方式修改包;引导窗口是安全、合规的选择。

SHEETFORGE 产品检测宏不会被自动移除

  • 是什么: Editor 程序集会在每个构建目标上自我注册一个 SHEETFORGE 脚本编译宏,以便其他资产能够在编译期检测到 SheetForge 已安装(见插件开发 ▸ 从其他资产中检测 SheetForge)。这项注册是幂等的(只有在缺失时才会添加——一旦存在就不会引发反复重新编译)。
  • 限制: 如果你之后删除了本资源,该宏会保留下来——原本会察觉到它被删除的代码也随之一起消失了。
  • 变通方法:Project Settings ▸ Player ▸ Scripting Define Symbols 中手动移除(按平台分别处理)。我们刻意不为了清理一个符号而让一个后台监视器持续运行。这与 SHEETFORGE_ADDRESSABLES 是两回事——后者是一个内部版本宏,只反映 Addressables 包是否存在。

2. Google表格 数据源

ExportUrl 模式是只读的

  • 是什么: 在 ExportUrl 模式下,推送、反映、结构编辑和删除全部被禁用。
  • 为什么: 这是未经认证的、链接共享的导出路径——本质上就是只读的。推送始终需要 SheetsApi 凭据,并在任何网络请求之前就会强制检查。
  • 变通方法: 任何写回操作都请使用 SheetsApi 模式(服务账号——见Google表格设置)。

ExportUrl 需要 gid 映射表

  • 是什么: 空的 gid 映射表会导致导入失败;重复的 gid 会被拒绝。
  • 为什么: 没有 gid 的导出 URL 会静默地只返回第一个标签页——这是产品拒绝踏入的一个静默数据损坏陷阱。SheetsApi 会自动发现标签页。
  • 变通方法: 为每个标签页注册其 #gid= 值,或者改用 SheetsApi。

推送按键删除行,且只删除已验证的行

  • 是什么: 本地删除的一条记录会在推送时——在发送前的重新获取确认它的键仍位于你导入时所见的那一行之后——从实时工作表中移除。已经不存在的行算作已完成(幂等的重新推送);如果在别的行上发现了该键,就会附带通知被跳过,绝不会按位置删除。删除会列在批准摘要中自己的一个区块里,并在每个标签页内由下往上、最后发送。
  • 为什么: 按键与实时工作表比对,正是让删除在一个可能已经漂移的工作表上保持安全的方法;任何比对无法确认的内容都会被原样保留。
  • 限制: 不具备行删除能力的来源(一个从未获得该能力的自定义提供方)会回退到旧的行为——删除会被报告,实时工作表中的那一行则留给你自己移除。

推送会跳过冲突的单元格(设计使然)

  • 是什么: 自你上次导入以来被第三方编辑过的单元格、键发生了歧义性移动的行、缺失的行,或重复的实时键,都会被跳过并给出警告——而不是被覆盖。
  • 为什么: 这正是安全网在起作用:被发送的单元格都是有效的;跳过是为了保护别人的更改,并防止写入错误的行。
  • 变通方法: 查看报告中"已应用/已跳过"的计数;先重新导入以进行协调,然后再次推送。(冲突解决 UI 会是一个独立的功能——目前没有计划。)

推送需要一个键列

  • 是什么: 一个发生了更改、却没有 RecordId 键列的标签页无法被推送——这是一个计划错误,会阻断整个 Push(所有标签页均零发送;不存在部分发送)。
  • 为什么: 推送会在实时工作表中按键重新定位行;没有键,防止写错行的机制就无法成立。
  • 变通方法: 新增一个键列,或者导出到文件后手动粘贴。

3. xlsx 数据源

  • xlsx 来源的标签页被排除在标签页重命名之外——这是对多工作表工作簿的一种保护。请在工作簿中重命名,然后重新导入。

  • 键重命名传播如果会触及 xlsx 来源的标签页,会阻塞整个批次——xlsx 路径无法安全地进行外科手术式的精准单元格更新,并且绝不允许部分反映。请直接编辑该标签页,然后重新导入。

  • 无法表示的单元格会被拒绝——没有缓存值的公式单元格、错误单元格,以及单元格内的制表符/换行符。内置的 OOXML 读取器刻意保持精简(零第三方代码)。请将公式具化为值;用 ; 表示列表。

  • 只处理取值——公式、日期与格式都会被如实解读 — 一个公式单元格给出的是它缓存的取值(绝不会重新计算),一个日期格式的单元格会被当作 yyyy-MM-dd 显示文本读取,其他数字格式、合并单元格和图表都不会被导入。网页应用的导入对话框会在一条"How this workbook was read"说明中点名实际发生过的情况;在编辑器中,同一策略会针对每个单元格静默生效(上面提到的那些拒绝依然会按单元格报告)。

  • 部分导出的下拉规则无法被承载 — 由于导出结果是一个工作簿,引用列的下拉列表会被写成一个指向目标工作表键列的真正区间,与 Google 规则的含义相同。仍有三种情况会被省略,并统一在一条 DropdownNotSupportedByFormat 警告中列出名字:

    • 一个包含逗号的列表成员(内联分隔符会将其拆开);
    • 一个超出该格式 255 字符上限(含引号)的内联列表;
    • 一个目标标签页不在该工作簿中的区间。

    无论如何,取值本身都会完整导出。见数据源、导出与推送

4. 创作——Data Studio

没有键列的标签页无法获得新记录,其取值编辑也会失去锚点

  • 是什么: 一个没有 RecordId 键列的标签页可以正常导入和显示,结构编辑也完全可用——新增、删除、重命名、重排序列与标记,以及工作表级别的重命名与删除都没问题。它唯一无法获得的是一条新记录,因为一条没有键的记录既无法被命名,也无法被引用:

    • 新增行控件会被禁用;
    • 引用选择器会拒绝在那里创建("……没有键列,因此无法在那里创建新记录");
    • 试图向其中写入的画布或检查器操作也不会有任何效果。

    取值单元格是可编辑的——但由于没有键可以用来定位这一行,这次编辑就只能以这一行的位置为暂存依据。

  • 为什么: 通常情况下,每一次暂存的编辑都是以逻辑方式被寻址的,即 (tab, record key, field),并会在真正写入之前针对工作表重新解析。这正是让一次编辑能挺过重新导入、行重排序,或者别人在它上方插入新行的原因。没有键列就没有这样的地址,因此这次编辑会改为钉死在一个行号上通过——脱离了这层安全网。

    所以,如果工作表的行在你写回之前发生了移动(一次重新导入,或者有人直接编辑了来源),一次按位置锚定的编辑就可能落到错误的行上。请把这类操作分成小批次暂存并反映。

  • 变通方法: 新增一个 RecordId 列(结构编辑本身是可用的,所以你可以在同一个窗口内完成),反映之后,该标签页就会重新获得逻辑锚点,变得完全可创作。无键的标签页依然完全适合被导入——这是一项创作层面的限制,而不是架构层面的限制。

列重命名 / @type 更改会破坏引用它的游戏代码;反映之后不可逆

  • 是什么: 生成字段的名称/类型会发生变化;引用它的游戏代码必须手动更新。Ctrl+Z 只在反映之前有效。
  • 为什么: 强类型——该字段是生成架构的一部分。编译中断会被自动链的安全中止机制捕获,并给出一条可执行的提示语句。列的取值会被完整保留(只有标记单元格会变化)。
  • 变通方法: 确认对话框会先给出警告;更新你的代码,让下一次导入继续完成即可。

标签页重命名会破坏引用它的游戏代码;反映之后不可逆

机制与上面相同——生成的类名会发生变化(FooDatabaseBarDatabase);重新导入会自动处理所有资源侧的清理工作(旧类、SO、地址)。

支持互换(swap)与循环式的标签页重命名

  • 是什么: Alpha→Beta + Beta→Alpha(一次互换),以及更长的循环(A→B→C→A),都可以在一个批次内暂存并反映——无论先暂存哪一半都可以,标签页栏会立即显示交换后的名称(所见即所得,可撤销)。UI 层的把关使用的是最终名称集合的唯一性(只有真正的冲突——两个重命名指向同一个名称——才会被拒绝);反映阶段会严格执行这一点。
  • 引用跟随的是数据(标签页身份),而不是名称: 在 A↔B 互换之后,RecordId@A 会被原子化地重写为 RecordId@B(单次处理——绝不会被重复应用),因此它会持续指向同一份数据,即便这份数据已经移动到了 B。
  • 本地: 一次互换会在一次写入中交换两个文件的内容;如果链条中有名称在不同扩展名之间被复用,会删除过期的旧扩展名文件(基于路径的删除防护),因此重新导入永远不会看到重复的标签页。
  • Google: 标题更改会按拓扑顺序执行,并用一个临时标题打破任何循环(A→tmp, B→A, tmp→B),因此实时工作表永远不会出现瞬时的重复标题。如果标题更改在序列执行到一半时失败,遗留在临时名称下的标签页会被报告出来,并附带恢复指引。

仅存在于 Google 上的边界情况:互相引用的互换标签页不会被重新指向

  • 是什么: 当两个被互换的标签页互相引用时(标签页 A 有一个 RecordId@B 列,标签页 B 有一个 RecordId@A 列),Google 路径会通过原地的标题更改来保留它们的内容,而不会重写它们自己@type 单元格——因此这种相互自引用在 Google 上不会被重新指向。
  • 为什么: Google 是通过更改标题来重命名一个标签页的(设计上内容不受影响);重写被重命名标签页自己的网格会破坏这一点。本地来源会重写被重命名标签页的投影,因此本地能完全处理这个问题。来自第三个标签页的引用,在两条路径上都会被重新指向。
  • 变通方法: 在 Google 上,让这个相互引用改为经由第三个标签页中转,或者通过一个中间名称来分步反映这次互换。

暂存取值的 SO 叠加层已经没有对应按钮了

  • 是什么:"在 SO 上预览暂存取值"这个叠加层(EphemeralSoApply)过去是由一个工作台按钮驱动的,而那个窗口已经不存在了。这个类型依然作为公共 API 保留给需要它的工具使用;SO 面板的测试编辑开关覆盖了日常尝试运行时数值的场景。
  • 如果你确实调用它,会有什么限制: 该叠加层会拒绝应用——并给出如实的徽标——(a) 待处理/新增的列,以及 (b) 解析失败的单元格。新增的是支持的。它复用的是真实的解析+烘焙路径,因此凡是它无法如实计算的内容,它会选择拒绝应用,而不是伪造结果。
  • 变通方法: 它从来就只是一个预览;真正的更改请正常反映即可。重新导入总会还原出真实情况。

当所在行被外部重命名、删除或发生键冲突时,暂存的编辑会被隔离

  • 是什么: 一次暂存编辑,如果其所在行在暂存和反映之间被外部重命名、外部删除,或发生了键冲突,就会被排除在本次反映之外,并打上"隔离"徽标。
  • 为什么: 它的逻辑地址无法被重新解析——但它既不会被静默丢弃,也不会被允许阻塞整个会话。
  • 变通方法: 单独丢弃它(在确认之后),然后重新暂存。

键重命名传播只覆盖 baseline 中的单元格

  • 是什么: 你在同一批次中刚刚输入的、引用旧键的文本不会被自动重写。
  • 为什么: 静默重写用户刚输入的内容是被禁止的;预检会转而捕获这个悬空引用。
  • 变通方法: 自己修复暂存中的引用,或者先反映重命名操作。

以下条目全部关于 Data Studio——这唯一的一个创作窗口。更早的工作台窗口已经被移除;它独有的三项功能已经提前迁移到了 Studio 与设置面板中——见工作台发生了什么

一张验证失败的工作表会被打开供编辑——但导出、推送和构建依然被阻止

  • 是什么: 只要来源被完整读取,即便验证失败,它的工作表也依然会被保存为 baseline,因此 Studio 可以打开它们,让你就地修复错误。代码生成和烘焙不会运行,直到错误数量归零,并且导出、推送到实时工作表,以及玩家构建,在工作表处于该状态期间全部会被拒绝,且各自会说明原因。
  • 为什么: 这三个出口都会把最后一次成功烘焙的取值与较新的工作表合并在一起。此刻运行其中任何一个,都会把过期的取值拼接到别人已经修正过的单元格上——这是一次静默的回滚。阻塞出口,正是让入口能够保持开放的原因。
  • 反映一次修复只会询问一次: 在一张处于隔离状态的工作表上,写回会显示一次额外的确认,因为预检在那里无法充当硬性关卡(工作表本身已经存在错误)。发现的一切都会作为警告记录在那次反映的报告中,随后的自动重新导入会重新验证整张工作表。健康的工作表不受影响——预检依然会拒绝写入。
  • 变通方法: 修复每一个被报告的错误,然后重新拉取。这个阻塞只会在一个地方自动解除——一次完整跑完烘焙的运行。

Data Studio 的排序与过滤仅影响显示——并且在激活时会禁用行重排序

  • 是什么: Studio 的按工作表排序(任意列、升序/降序、按项目持久化)和文本过滤,都只会改变显示顺序。行号槽依然保留真实的工作表行号,两者都不会影响暂存、反映、推送或导出。当排序或过滤处于激活状态时,行的 ▲▼ 重排序工具会被禁用,并带有提示说明。
  • 为什么: 在视图处于排序或过滤状态时按"可见的相邻行"来重排序,会把行悄悄移动到用户看不见的行旁边。真正的行顺序更改是一种结构操作——请先清除排序/过滤。
  • 说明: "按最新排序"只有在你的工作表中存在一列能编码这个信息(例如 IntId 或类似日期的字符串列)时才存在——工作表本身不存储任何时间戳。

当一次键重命名处于暂存状态时,Data Studio 的 Problems 只是一份草稿

  • 是什么: 当一个键(RecordId)单元格存在暂存编辑时,Problems 面板会带有一个 draft 徽标,其中的未解析引用条目可能是假警报。
  • 为什么: 内存中的预览不会应用键重命名传播——那要到反映时才会跨所有标签页执行。与其隐藏诊断信息或伪造传播结果,窗口选择告诉你:在重命名被写入之前,这份列表只是一份草稿。
  • 变通方法: 反映这次重命名(传播会带着自己的确认一起运行),然后再阅读刷新后的列表。

表格在超过 200 行时会做行虚拟化——有两处边界值得了解

  • 是什么: 超过 200 行之后,表格只会为当前可见窗口构建行元素(外加十二行的预渲染余量),并在上下两端用占位块撑住真实的总高度,这样滚动条就不会撒谎。滚动越过一个窗口边界时,还存活的行会被复用,只有新进入视野的行才会被构建。浏览器端的网格在同一个阈值上做的是同样的事。

    有两种情况依然会构建全部内容。行数等于或少于 200 行时,每一行都会像以前一样一字不差地被构建。一张根本无法查询到视口高度的表格也是如此——比如站在某个窗口之外、布局永远不会抵达的那种情况——因为在那里,如实的兜底方案就是"全部构建"。一张尚未完成布局的大型表格则会改为等待一帧,因此它从第一次绘制开始就是窗口化的,而不是先全部构建再整个丢弃。

  • 你正在编辑的那一行会保持存活,即便它滚出了视野,光标、焦点和你已经输入的内容都会保留下来。这种"保活"有一个距离上限,超过之后,正在打开的编辑器会提交并失焦,而不会被无限期地一直携带下去。发生这种情况时不会丢失任何东西——取值早已存在于暂存会话中。

  • 只有元素的创建是窗口化的。 列宽采样、搜索、排序、坐标计算,以及暂存叠加层,依然会考虑每一行,因为这些操作中的任何一个,如果只看屏幕上的内容,都会给出不同的答案。因此切换到一张非常大的工作表,所做的工作依然与其规模成正比——不再发生的,只是构建成千上万个部件。

  • 在浏览器中,窗口化模式会自己测量列宽,而不是交给布局引擎去做。 自动布局的宽度会根据窗口内恰好可见的那些行来计算,因此列宽会在你滚动时抖动。在窗口化模式下,宽度来自对全部行的一次数据驱动估算,然后被固定下来。完整渲染模式(≤ 200 行)依然使用自动布局,没有变化。

画布只能在其滚动范围内平移,而标记循环的虚线连线在变长时会变得更粗略

  • 是什么:

    • Ctrl/Cmd + 鼠标滚轮可以缩放记录画布,缩放范围在 25 % 到 200 % 之间,并保持光标所在的点位不动——以中心为锚点的缩放会把你正在看的卡片滑出屏幕。画布头部显示的百分比同时也是一个可以恢复到 100 % 的按钮。单纯滚动滚轮依然是滚动画面。
    • 按住鼠标中键——或者对于没有中键的设备用 Alt + 左键——拖动即可平移,按住期间光标会标示出抓取状态;左键拖动被留给了选中与链接,因此它不能同时表示"移动视图"。
    • 这个窗格是一个滚动视图,因此平移范围就是滚动范围:它会在内容边缘处停下,而不会漂移到空白区域,当内容小于视口时甚至根本不会移动。这不是一个无限画布。
    • 标记循环的虚线连线,会限制自己绘制的虚线段数量上限,并在路径很长时把虚线周期翻倍,因此一个很长的循环读起来会更粗略,而不是更精细。
  • 为什么: Unity 按每次绘制调用分配网格顶点,存在一个硬性的 65,535 上限,超出这个上限会让绘制内容完全消失,而三角化的开销却照样要付出。虚线段数量上限正是刻意设计出来,让单次 Stroke 调用保持在这个预算之内。

    背景的圆点网格过去也曾面临同样的悬崖式风险,现在已经不再如此:它现在是一小块重复平铺的背景贴图,顶点开销为零,无论画布变得多大,重绘时间都保持恒定。(以路径方式绘制的一个圆点,实测开销是 28 个顶点,而不是它的四个角——这正是绘制回退方案 1,800 个圆点预算背后的算术依据,也是这块贴图会成为正式发布方案的原因。)

  • 变通方法: 网格本身不需要任何变通方法。面对一大片相邻节点时,缩小视图、收窄方向分段,或者把某个相邻节点打开作为新的终点节点,而不要试图把一切都塞进一屏。

一张还没有表格的工作表会被跳过,而不是被导入

  • 是什么: 一个既没有三项必需标记中的任何一项、没有任何数据行的标签页——一张全新的、只带有注释或一行 @style 的工作表——会被以一条 EmptyTabSkipped 警告跳过,而不是因为缺失三个必需标记而导致导入失败。它已经生成的代码、烘焙资源和地址都会被保留,而不会像标签页被删除那样被清理掉。导出和推送也会以同样的方式对称地跳过它,因为三者问的是同一个判定条件。
  • 为什么: 一张尚未完成的工作表,不应该能够阻止其他所有标签页正常导入,而且作者通常都是先创建工作表,之后才写表头行。
  • 边界: 一张写了一半的工作表(存在任何一个必需标记)不会被跳过——它会如实地报错失败,因为静默跳过会掩盖已经完成的真实工作。同样地,一张从列 A 而不是列 B 开始录入类型的工作表,也会被正常交给解析器处理,因此它真正的诊断信息("列 A 是标记列,数据从 B 开始")依然会出现。

由插件 C# 代码注册的枚举,无法从工作表中获得新成员

  • 是什么: 一个 T 由插件通过 enums.Register<T>() 注册的 Enum<T>,归代码所有。一张枚举定义表不能再声称拥有这个名字(DuplicateEnumName),并且这样一列的单元格下拉菜单上,也根本不会出现 "Add a new member…" 这一行。
  • 为什么: 工作表只对它自己定义的内容具有权威性。往一张已经不再决定编译后类型的工作表里写入一个成员,只会产生一个永远不会出现在代码里的成员——这是产品无法兑现的承诺。这一行的缺席,就是 UI 表达这一点的方式,而不是提供一个注定会失败的操作。
  • 变通方法: 如果这个枚举应该归工作表所有,就把它迁移进一张枚举定义表;否则就在你插件的 C# 代码里添加该成员,然后重新编译。
  • 结构编辑遵循同一条归属界线: 一张枚举定义表的结构在两个宿主环境中都是完全可创作的——新增、重命名、删除、重排列,以及编辑基础类型和说明——但这一切都无法触碰一个代码拥有的枚举名,一张定义表也不能声称拥有这样一个名字。拒绝时会点名原因。

枚举定义表:成员行只能追加,且没有排序功能

  • 是什么: 枚举工作表的结构在编辑器和 web 应用中都是原地创作的,但成员行永远只能追加——一个空缺永远不会被回填——并且这个视图不提供任何排序或过滤功能。
  • 为什么: 一个成员的位置就是它的整数取值。填补一个空缺或重新排列成员,会悄悄改写已经烘焙进资源、存放在存档里的数值。相比之下,列的顺序不承载任何意义,这正是重排列列永远被允许的原因。
  • 变通方法: 如果想显式地固定一个取值,请使用 Name=value 语法;其余场合的展示顺序是消费方自己的事,与工作表无关。

由工作表定义的枚举,总是会生成到设置文件夹中

  • 是什么: 生成的标签页类型会在它们已有的那个文件夹中原地重新生成,但枚举文件(SheetForgeEnums.cs)没有任何标签页可以作为锚点,因此它总是会被写入设置中指定的生成代码文件夹。如果某个标签页的生成代码存放在它自己的包文件夹中,却使用了一个由工作表定义的枚举,那个包程序集就会因为 CS0246 而编译失败。
  • 为什么: 所有由工作表定义的枚举都汇总在同一个文件里,因为枚举是一项项目级别的产出,而不是按标签页产出的——因此不存在某个单一的标签页可以让它跟随。
  • 变通方法: 把两个生成文件夹放进同一个程序集,或者改为从插件代码注册那个枚举。这个失败会是一个可见的编译错误,并点名缺失的类型,绝不会是静默的损坏。

类型化资源引用依据已加载的类型解析——短名称仅在唯一时可用,预定义程序集中的类型不可用

  • 是什么: AssetRef@Group<Type> 接受项目能加载的任意 UnityEngine.Object 派生资源类型,无论是引擎自带的还是你自己的,都没有白名单限制。有三种情况会被拒绝、而不是被凭猜测处理:一个被多个已加载类型共享的短名称AmbiguousAssetType——取决于安装了哪些包,TextAsset 就可能是这样一个例子)必须写成全名(UnityEngine.TextAsset);一个未知的名字是 UnknownAssetType,并附带最接近的匹配建议;而一个位于预定义程序集中的类型(Assembly-CSharp 及其同类——任何没有程序集定义文件的脚本文件夹)则是 AssetTypeNotReferenceable
  • 为什么: 生成的配套程序集本身就是一个程序集定义,而一个程序集定义无法引用预定义程序集——对这样的 T 而言,AssetReferenceT<T> 根本无法编译通过。如果对一个有歧义的名字擅自选一个来解析,就会把这一列悄悄绑定到错误的类型上。
  • 变通方法: 把该类型移入一个程序集定义,或者去掉 <…> 限制、改用不受限制的 AssetRef@Group。组件和仅编辑器类型永远不在候选之列。
  • 另外: 浏览器不会解析类型名(它没有项目可供解析):web 应用会解析 <Type> 并在列的工具提示中显示它,但不会产生这三种类型名诊断中的任何一种,也不提供选择器或拖放。代码生成绝不会原样输出一个未解析的名字——一个它无法解析的名字会回退为 AssetReference,并附带一条 AssetTypeUnresolvedFallback 警告。

资源选择器、拖放与暂存的注册——哪些是自动的,哪些不是

  • 是什么: 拖放或选取一个资源会立即把地址写入单元格,并为这次反映暂存相应的 Addressables 更改(新增 · 移动 · 创建组);这项更改只会在工作表写入成功之后才会运行——或者,当暂存的内容里只有注册时,它们会独立运行,随后进行自动重新导入;一次因为标签页由工作簿承载而无法写入的反映,会让它们继续保持暂存状态。一项已经没有任何东西引用的注册,会在单元格被重新输入时被丢弃;任何挺过这一步、活到反映时刻的注册,都会因为已不再有引用而被跳过;一个已经在组中的资源会保留它现有的地址;一个位于另一个组中的资源,只有在一次点名了引用它的其他单元格的确认之后,才会被移动。自动生成的地址是不带扩展名的文件名,一个已被该组中另一个资源占用的地址会被拒绝,而不会被重新命名。新建的组会获得默认的 BundledAssetGroupSchemaContentUpdateGroupSchema。已应用和被跳过的条目都会连同各自的原因被记录到控制台;对于本地文件夹来源,反映的完成对话框会重复显示这行汇总(Addressables: N registered, M skipped)。
  • 为什么: 工作表才是权威——对于一次没有触及工作表的反映,项目绝不能发生变化;而一个没有任何单元格指向的条目,会是一个工作表无法解释的孤儿条目。
  • 变通方法: 如果一项注册被跳过了,下一次重新导入会把该单元格报告为 UnknownAssetKey;修复原因后再次反映即可。注册操作始终需要 Addressables 包。

子资源以 parent[sub] 的形式寻址,以 Sprite 模式导入的贴图能通过 <Sprite>

  • 是什么: 一个子对象条目(贴图中的一个精灵、字体中的一个材质)会按 Addressables 为它命名的方式寻址——parent[sub]——而这个键只会针对子对象自己的类型进行检查。父项地址既满足它自己的类型,满足它所包含的每一个子资源的类型,这正是一张以 Sprite 模式导入的贴图能够通过 <Sprite> 列的原因。拖放一个子资源,会为父项暂存注册,并把 parent[sub] 写入单元格。
  • 边界: 一个子对象条目必须已经存在于 Addressables 目录中,parent[sub] 才能通过验证;选择器会把它所知道的子键列在各自父项之后。

颜色不支持 HDR、曲线切线跟随其模式、渐变会被量化——这是设计使然

  • 是什么: Color 是四个字节——超过 1 的通道(HDR)会在导出时被钳制到 0…1 区间。一个 AnimationCurve 的切线,只要它那一侧是 AutoLinearConstantClampedAuto,就会在导入时依据模式重新计算,因此一个手写的、与其模式相矛盾的数字会被替换掉(与 Unity 应用该模式时执行的是同一套计算);Once 会被读作 ClampForever,且永远不会被写回;一条没有关键帧的曲线没有文本形式,只会以可选列的空单元格形式存在。Gradient 的关键帧时间在导入时会被量化为 16 位(与 Unity 存储它们的方式完全一致),一个只有单个关键帧的渐变,从 Unity 那里回来后会变成两个完全相同的关键帧,颜色空间只有在被设置过时才会被写出。
  • 为什么: 工作表所展示的值必须就是引擎所持有的值,因此 Unity 原本会在之后执行的归一化,被提前在入口处一次性完成,这样每一个界面——工作表、编辑器字段、web 预览、烘焙后的资源——展示的都是同一条曲线、同一个渐变。
  • 变通方法: 如果你希望切线数值被原样采用,请使用 Free/Free;HDR 强度请存放在一个单独的 float 列中。

纸片编辑器与原生字段只为这三种视觉类型而存在

  • 是什么: Data Studio 会把 ColorAnimationCurveGradient 标量显示为 Unity 自己的字段,把它们的 List<> 显示为纸片编辑器;web 应用则用它自己的编辑器显示预览,并使用纸片列表。其他任何列表列——List<int>List<Enum<…>>、包装类型列表——在两个宿主环境中都仍然是规范文本,一个包含这三者之一的包装类型(Pair<Color>)也同样是文本。
  • 为什么: 只有这三种类型的每个元素都对应一幅画面;对其余类型而言,单独一行规范文本已经是最精确的表达方式,而且包装类型的外层记法归它所属的插件所有。
  • 余地: 一个存储这三种取值之一的插件类型,可以通过声明匹配的 StudioCellEditorHint 形态,来采用同样的编辑器(见插件开发 §4.16)。

归属着色只区分"工作表 vs 代码"这一种维度

  • 是什么: 侧边栏/图例恰好只区分两种来源——真实的工作表标签页,以及代码注册表的虚拟标签页。不存在第三种"生成"分类,也没有按列的归属着色。
  • 为什么: 来源是由标签页本身推导出来的,窗口对此已经确定无疑;而按列分类则需要另一个全新的扩展点才能做到真实可靠,而且从来没有消费方提出过这个需求。
  • 这与工作表自身的颜色不是一回事: @style 允许一张工作表为自己指定颜色,那种颜色是作者自行选择的显示元数据——它不能说明这张工作表来自哪里。这两种着色读取自不同的位置,绝不会混合在一起。

引用下拉菜单回答的是"成员是谁",而不是"顺序如何"

  • 是什么: 一个 RecordId@Tab 单元格现在带有一个可搜索的下拉菜单(List<> 则是一份清单),但列表单元格的下拉菜单只能新增和移除元素——无法移动某一个元素。列表的重排序要在画布上完成,那里每个元素都有自己的一行。
  • 为什么: 下拉菜单回答的是"这里面有什么";而"哪个位置"则需要一个能把内容排成一行的界面,画布本身就已经是这样的界面。在两个地方都做同一件事,就意味着要同时维护两份答案。
  • 另外: 这个下拉菜单只会附加到普通的、非包装类型的引用列上。包装类型单元格的文本承载的是它自己的记法,把一个裸键粘贴进去会破坏这个取值——深入到包装类型内部属于已注册单元格部件的职责(见插件开发)。

All 搜索在编辑器中读取的是已烘焙数据,在浏览器中读取的是实时会话

  • 是什么: Data Studio 的 All 条目搜索的是已烘焙的数据库,因此需要先成功完成一次导入,并会在一次导入完成或活动设置发生变化时自动刷新。Web 应用的 All 条目搜索的是会话当前展示的取值,包括暂存的编辑在内。匹配方式、结果顺序和 50 行分页在两端是同一份代码;两端都可以双击一条结果(或选中它并按下 Enter)来打开对应工作表并选中匹配到的单元格。
  • 为什么: 浏览器没有已烘焙的 ScriptableObject;它拥有的是实时会话——用屏幕上正显示的取值来回答,正是浏览器会话存在的意义。编辑器则继续读取它已经拥有的、已烘焙的真实数据。
  • 边界: 在编辑器中,一次已暂存但尚未反映的编辑,在一次导入运行之前不会被 All 找到;而一条来自会话尚未加载的工作表的结果不会跳转——列表下方会有提示说明原因。在浏览器中,一次暂存的编辑会被立即找到,且每一条结果都可以跳转。

5. 性能

  • 常规路径是线性且快速的:50,000 行 × 20 列 ≈ 628 ms(实时编辑器,Mono;无头模式下为 144 ms);50 个标签页 × 2,000 行、含 180k 个引用单元格 ≈ 294 ms。典型的项目规模完全不是问题。
  • 内存占用是线性的,但装箱开销较大:每个单元格保留 ≈ 59 字节(导入期间峰值 ≈ 138)。按 Google表格 的单元格上限(~10M 个单元格)外推,对应 ~6.3 s 导入耗时、~590 MB 保留内存、~1.4 GB 峰值内存——在极端规模下,请留意低配置/32 位环境。(面向列的 IR 是一项已被认领、尚未处理的待办事项。)
  • 创作用的表格在超过 200 行后会做行虚拟化,编辑器和浏览器两端都是如此,因此打开一张大型工作表不会再为每一行都构建一个部件。不会被窗口化的是那些只看屏幕内容就会给出不同答案的逐行逻辑——列宽采样、搜索、排序、坐标计算、暂存叠加层。细节和两处边界见§4
  • 错误路径同样是线性的:即便引用大批量破坏,最接近匹配建议的计算依然保持有界——按字段设定的建议预算,加上经过长度预筛选、可提前终止的编辑距离算法,使其在损坏引用数量上大致保持线性(4,000 条损坏引用时 ≈ 45 ms,无头模式;同等规模下有效数据的耗时 ≈ 2.7 ms)。重命名一个被引用的标签页,本来就不会导致引用大批量破坏:重命名操作会重写引用方的 @type 单元格。

6. 演示场景

  • 示例是可选导入的——这两个演示示例及其场景默认并不存在。它们以 Unity 包的形式发布(Assets/SheetForge/Examples/SheetForgePluginDemo.unitypackageSheetForgeCoreDemo.unitypackage);导入其中一个(双击,或入门窗口中的 导入 Plugin/Core Demo 按钮)即可还原出 Assets/SheetForge.PluginDemo/…Assets/SheetForge.CoreDemo/…。在此之前,示例根本不存在于你的项目中——它们仅以那些包的形式发布——因此它们永远不会与你的项目冲突——核心产品在没有它们的情况下也完全自给自足。
  • 需要先执行一次导入(每台机器各一次)——它所加载的 Addressables 地址是一个不提交到版本库的缓存。在此之前,它会显示一条引导信息。
  • 演示不需要设置命名空间——已提交的示例类型使用默认的 SheetForge.Generated 命名空间,并带有 Example* 类名前缀,因此重新导入一个演示会原地重新生成它们,无需设置 generatedNamespace

7. 插件扩展——已上线的接口缝隙(及其边界),以及仍然保留的部分

十六种扩展契约已经上线,每一种都以零 Core 改动的方式加入——完整清单见插件开发。全部十六种都由发现机制找到(Unity 的 TypeCache;浏览器端则是对已上传程序集的扫描),不需要程序集引用,也不需要编辑任何清单文件。其中十一种 Core 契约会拿到一个注册表,用来注册它们所新增的内容;而五种 Editor 契约——图形部件、检查器操作、单元格编辑器提供方、面板提供方和来源提供方——则只是被发现之后照原样使用。

已上线的接口缝隙中,有五个的边界值得在这里说明清楚,而不是留给你自己去发现:

引用型自定义单元格类型——为你自己的记法提供完整的 RecordId@Tab 对等能力

  • 是什么: 一个已注册的单元格解析器,如果同时实现了 IReferencingCellType(且其取值实现了 IRefBearingValue),就等于告诉了 Core 该如何读取和重写藏在它自己记法内部的那个键。这样一列就会获得内置引用所拥有的一切:

    • 带最接近匹配建议的完整性检查;
    • 保留载荷的键重命名传播(attack:add:10power:add:10);
    • 图边与端口、 选择器、反向引用计数;
    • 孤立检测,以及导出的下拉规则。

    发现机制是对已注册解析器的一次类型转换——不存在新的注册渠道,一个没有实现它的自定义类型则完全不受影响,一个字节都不会改变。见插件开发 §4.4a

  • 边界: 载荷中不能包含 ;——Core 会在你的解析器看到文本之前,就先把一个列表单元格拆分成多个元素,因此取值内部的一个分号会被拆碎成两个元素(这与包装类型所受的约束相同)。并且 @target 必须指向一个真实的工作表标签页:代码注册表的虚拟标签页会像 RecordId@Tab 一样被 UnknownTargetTab 拒绝。正是这条限制,才让 Core 自身的未解析引用报告和重命名传播机制得以原样适用。

<> 包装类型(MyWrapper<T>)——附拒绝规则

  • 是什么: 插件通过 ICellWrapperType 注册一种通用值形态(例如 Pair<int> = 1~2);Core 会递归解析内部类型(见表格语法)。

  • 拒绝规则:

    • Pair<List<T>> 会被拒绝——列表不能位于包装类型内部,List 始终保持扁平、最外层。
    • Pair<int>@Tab 会被拒绝——应把 @ 放在内部叶子上:Pair<RecordId@Tab>
    • Pair<int?> / Pair<int=1> 会被拒绝——可选性/默认值是字段级别的,不属于内部类型的一部分。

    List<Pair<T>> 被允许的,但包装类型自己的分隔符必须与 ;(列表分隔符)不同——这是 Core 无法强制保证、需要插件作者自行负责的事项。

自定义结构标记(@yourMarker)——仅限按列元数据

  • 是什么: 插件通过 IStructuralMarkerDefinition / ISheetForgeMarkerPlugin 注册一个 @marker 行,是对 @overlap 按列验证机制的泛化。该值会被存储为 FieldSchema.MarkerValues 元数据。
  • 边界: 一个标记只拥有它自己的按列取值验证——它不会接管整行数据形状的解析(归一化仍然是表达数据形状的方式)。并且代码生成不会烘焙标记值:与 @overlap 一样,它们只是验证/展示用的元数据,对架构指纹不可见——因此没有任何与标记相关的内容会进入生成代码或烘焙出的 SO。

色彩预设——我们只为自己绘制的界面上色,而不是 Unity 的原生控件

  • 是什么: 插件通过 ISheetForgeThemePlugin / ThemeRegistry 注册一个色彩预设;它会出现在 Preferences ▸ SheetForge ▸ Theme 中,与内置的 DefaultHigh contrast 预设并列,并且只有当用户选中它时才会生效(注册这个动作本身绝不会强行接管画面)。一个预设只会覆盖它指名的那些插槽——其余每一个插槽都保持产品默认值,因此即便后续新增了插槽,已有的预设依然有效。
  • 边界——混搭的界面外观是预期行为: 主题所覆盖的,是 SheetForge 自己绘制的部分(窗口背景、表头、文本、强调色、网格与暂存相关的颜色)。绘制在这些窗口内部的原生 Unity 控件——按钮外观、字段边框、弹出箭头——依然会跟随编辑器皮肤,Unity 不允许某个包重新为它们上色。因此,如果编辑器正运行着深色皮肤,却选择了 Always light,得到的就会是一个浅色的 SheetForge 界面,配上深色的原生控件。如果想要外观统一,请把编辑器皮肤也一并设置匹配。
  • 边界——仅限颜色: 预设携带的是颜色(每个插槽一个 0xRRGGBB)。间距、字体大小和布局都不可主题化,半透明填充(徽标背景、模态遮罩)则是由某个插槽颜色加一个固定的透明度推算出来的,而不能单独设置。

声明式创作界面——刻意收窄的一套词汇

  • 是什么: ISheetForgeStudioPlugin 让一个包能够以数据的形式描述动作、面板、列徽标和单元格编辑器形态,因此一次注册就能同时被编辑器和浏览器绘制出来。这套词汇是固定的,只会通过追加的方式增长:五种动作放置位置、十三种节点类型、七种单元格编辑器原型(最新的两种 CurveEditorGradientEditor,正是内置的曲线与渐变类型所使用的)。
  • 边界——它不是一个 UI 框架。 任意渲染、复合输入和多步骤流程在这里没有对应的词汇,添加它们就意味着要永远维护一个微型 UI 工具集。这正是 IStudioPanelProvider 存在的意义:用与某个已描述面板相同的 id 注册它,编辑器就会绘制这个富面板,浏览器则绘制那个描述性面板。这里不存在仅限 Web 的逃生舱——浏览器无法加载一个 UIToolkit 类型,假装它可以只会让插件的扩展仅存在于一块屏幕上。
  • 边界——一个动作的能力恰好是四种:暂存一个单元格、把多个单元格作为一步撤销暂存、聚焦一条记录、请求重绘。因此一个插件的动作只是一次普通的暂存编辑,会经过与手动输入完全相同的关卡、预检和推送流程。创作会话本身被刻意地不暴露出来。
  • 对观察者而言,是零触发,绝不会是错误触发: IPipelineObserver 会在一次显式导入周期结束时触发。有两条路径永远无法到达那个终点——一次在管线开始之前就停止的运行(没有活动设置;Addressables 未安装),以及一段被编译错误打断的代码生成→编译流程。如果你需要知道"曾经尝试过一次导入",请把它和编辑器侧的 ImportEvents 事件总线搭配使用。

仍在设计中、尚未构建(目前还没有消费方)

  • 整行数据形状标记(例如用一个标记把一个 2D 矩阵读作单个字段)——刻意没有构建:一个标记拥有的是按列验证,而不是整行解析,而归一化(引用 + type 列 + List<T>)在表达能力上已经是完备的。MarkerRegistry 的注册缝隙本身已经上线;只有这种形状解析的解读方式被保留了下来。
  • 把自定义标记值烘焙进生成代码——在有消费方真正需要把标记元数据当作代码生成的常量/特性使用之前,都不在范围内。
  • 按记录 / 惰性加载的 SO 容器——设计已完成,尚未构建;目前的代码生成只产出按标签页整体加载的 Database SO。

8. 架构演进

  • 导出/推送需要在架构变更之后进行一次全新的烘焙——过期的烘焙结果会因 ExportSchemaMismatch(指纹不匹配)而失败。现在,这个拒绝界面会主动提议替你运行那次导入:只需一次确认即可开始,之后不会自动执行任何导出或推送——等导入完成后,你只需再次按下最初想执行的那个操作即可。
  • 架构变更之后的首次导入内部分两个阶段(代码生成 → 编译 → 烘焙)——全自动,只需一次用户操作;只有编译失败才会使其停止(安全中止,给出可执行的提示语句,尝试次数上限为 3 次)。

9. 本地化范围

报告的细节片段(出问题的值、建议)、底层异常,以及开发者日志,都是本地化报告骨架内部的内嵌英文——运行时插值的内容无法作为语言表的键(这是业内的标准边界)。除此之外,编辑器绘制的一切、报告骨架,以及"为什么"/"怎么办"语句,都在全部 10 种语言中完全本地化。

全部十种语言都已完整翻译。 每种语言表中的每一个键都带有真正的翻译——包括 Data Studio、主题偏好设置、对话框与日志文案在内。十个文件之间的键一致性由测试强制保证,因此不会有任何地方回退成裸键名或破坏占位符。

每种语言里都有少数条目读起来与英文版完全一样,这属于翻译层面的取舍,而不是尚待完成的翻译:它们是符号与仅作占位符使用的字符串(+)、专有名词与格式名称(Google SheetsSHA-256),以及某些语言本身就确实按英文拼写的词(OKAlpha)。

插件自己的标签则完全不存在于 Core 的语言表中:把它们注册进 ISheetForgeStringsPlugin,就能让它们跟随用户的语言;不注册的话,它们就会按原样显示。

10. 编辑器交互

  • Ctrl+Z 的作用范围:一个获得焦点的文本框会优先消费 Ctrl+Z(操作系统的标准行为);一次成功的反映之后,暂存历史会被清空——撤销永远不会触及已经写入工作表的内容(工作表具有权威性)。
  • 在导入/导出/推送期间,语言更改会被锁定(因为它会触发一次菜单再生成式的重新编译)。主题更改不会被锁定——它们从不触发重新编译,因此明暗模式和预设可以随时切换,即便管线正在运行中也不例外。
  • 主题和语言都是按用户存储的(EditorPrefs),而不是按项目存储的——每位队友都各自保留自己的设置,两者都不会出现在版本控制中。选择一个非默认的色彩预设,会在 Assets/SheetForge/Editor/Generated/ 下生成一份样式表(已被 Git 忽略,可自我修复);默认预设则不会写入任何内容,并会删除这份样式表。
  • 生成代码 + 烘焙的 SO + Addressables 组,都是按机器区分、已被 Git 忽略的缓存——每台机器只需运行一次导入;游戏代码按地址加载,绝不通过直接的场景引用。

11. 许可

仓库中附带一份 LICENSE 声明:Unity Asset Store EULA 是具有约束力的协议,并附带一条仓库可见性声明(源码可供参考,也可供已获得许可的购买者查看;未经书面许可,不得在 EULA 之外进行再分发/转售)。第三方代码:没有——包括手写的 OOXML xlsx 读写器在内。

12. 已验证状态(发布时)

  • 双重测试工具:2,150 个无头 .NET 测试(2,150 个通过)+ 3,021 个 EditMode 测试(3,021 个通过,0 个失败,4 个跳过)。这两个数字是唯一陈述具体计数的地方;其他所有页面都链接到这里。
  • 这四个被跳过的测试都是实时的 Google 往返测试,它只有在环境中存在服务账号凭据时才会运行,本次运行中被跳过了。在具备凭据的情况下,它已经针对一份真实的表格被反复实测——获取 → 推送,包括行移动冲突检测、与语言环境无关的浮点数处理、一次跨标签页的混合批次,以及一次完整的调度器运行,用以断言每次发送只产生一份合并报告、且恰好触发一次自动重新导入。结果:4/4 全部通过。
  • Web 应用有自己的一套关卡,全部通过:类型检查、lint、414 个单元测试、一次 WebAssembly 发布 + 冒烟测试(会加载一个真实的插件 DLL)、一次生产构建、56 个端到端浏览器测试,以及一次跨语言常量校验(六对 Unity↔Web 配对:宿主版本、插件格式、注册表架构版本、OAuth 作用域、宿主程序集名称、行窗口阈值)。
  • 所有已编译的产品 asmdef:0 个错误,0 个警告。(示例 asmdef——每个演示各两个——只有在你导入某个演示包之后才会出现,在此之前不会被编译。)
  • 守护测试全部通过:产品源码中没有任何韩语字面量,内核缝隙中没有任何领域词汇(两者都会跳过 /Samples~/——示例本身就是领域内容),10 种语言的键保持一致。
  • 不使用 IVT 的消费者模拟程序集仅凭公共 API 就能编译通过(编译器强制保证)。其中一个不使用 IVT 的迷你插件探针仅凭公共表面就实现了十六种扩展契约中的十五种ISheetForgePlugin / 验证器 / 边 / 标记 / 模板 / 图形 / 代码注册表 / 主题 / studio UI / 字符串 / 管线 / ISheetSourceProvider / Studio 部件 / Studio 检查器操作 / Studio 单元格部件)——因此即便 Plugin Demo 示例已不再被编译,契约的公开性依然得到了证明。第十六种,IStudioPanelProvider,返回的是一个 VisualElement,因此改由一项编辑器侧测试来验证。同一个探针还实现了可选启用的 capability 接口,包括 IReferencingCellType / IRefBearingValue,并通过公共表面对它们进行了实测(基于类型转换的发现机制、全部五个钩子、残留内容的保留)。

只有在提交 Asset Store 时才能被验证的事项(不在本仓库范围内):.unitypackage 安装提示的实际行为、Portal 依赖声明、发行包中测试程序集的排除情况,以及一次全新项目下 0 警告的复查。

13. 本地化表格(游戏文本)

如实说明本地化表格与 Unity Localization 桥接的边界。(这个包本身是可选的——见 §1。)

桥接是单向的,外部对表格的编辑只会被询问——绝不会被合并

  • 是什么: 同步只沿着工作表 → StringTable 这一个方向进行。桥接会给它所拥有的表格打上标记,并在每次同步时记录一个指纹;此后如果表格被别的东西编辑过——Localization Tables 窗口、Unity 自带的 Google 表格扩展、一次 XLIFF 导入——下一次同步就会停下来询问:是从工作表覆盖,还是带着差异报告中止。
  • 为什么: 两个可写的驾驶舱面对同一份数据,结局就是静默覆盖。工作表才是权威来源,所以另一个驾驶舱必须是显式的,而不是静默的。
  • 变通方法: 让翻译经由工作表流转——翻译工作簿(xlsx 导出 + 仅语言列的部分再导入)正是为此而存在的。

在 Studio 之外重命名一个键,等同于先删除再新增

  • 是什么: 在 Data Studio 中重命名一个键,会原地重命名该表格条目,同时保留 LocalizedString 引用所绑定的内部 id——场景中的引用得以存活。直接在工作表来源中(Google 表格、Excel)重命名这个键,则与删除一个键、再新增另一个键无法区分:桥接会创建一个全新的条目,旧的那个则变成孤儿,场景引用依然指向那个孤儿。
  • 为什么: 文本层面的差异比对,无法在不猜测的情况下分辨"重命名"与"先删后加",而一次错误的猜测会静默地重新绑定引用。
  • 变通方法: 在 Data Studio 中重命名键(两种宿主环境皆可);孤儿报告会捕捉到外部重命名带来的后果。

表格中的孤儿键默认会被保留

  • 是什么: 一个存在于表格中、但已不在工作表里的键会被保留,作为孤儿被报告,只能通过显式的清理操作来移除——或者,如果你选择启用同步时删除这项设置,则会自动移除。不会有任何东西作为副作用被删除。
  • 为什么: 工作表中缺失的一行,可能只是编辑到一半时的失误;因此而毁掉翻译将是不可挽回的。

仅限字符串表——不涉及资源表

  • 是什么: 桥接填充的是 StringTable 集合。Unity Localization 的 AssetTable 这条轴线(本地化的精灵、音频、预制件)不会从工作表同步。这是一项已被认领的待办事项。
  • 变通方法: 用该包自身的工具来管理资源表;桥接不会触碰它们。

不重新实现 XLIFF 与 pseudo-locale

  • 是什么: 被同步的表格就是普通的 Unity Localization 表格,因此该包自身的 XLIFF 导出/导入以及 pseudo-localization 都能原样地在其上工作。SheetForge 不会添加第二套实现。
  • 边界: 那些工具写表格的输出,会被算作一次外部编辑(见上面第一条)。请让工作表保持权威来源的地位,并通过翻译工作簿来传递翻译。

不附带任何语言专属的 Smart Format 辅助工具

  • 是什么: smart 列会把一个条目标记为 Smart String,但 SheetForge 本身不提供任何语法格式化器——例如韩语的助词选择,就被刻意排除在外。
  • 变通方法: 该包的 Smart Format 扩展点依然完全开放,供你自己编写格式化器。

键常量会被清理为 ASCII

  • 是什么: 生成的 {Tab}Keys 常量会把 ASCII 字母、数字与 _ 之外的每一个字符都替换为 _(冲突会附加一个数字后缀),因此一个非 ASCII 的键会得到一个难以阅读的常量名——但键本身在任何地方依然可用。
  • 变通方法: 如果你要使用这些常量,就把键保持为 ASCII(ui.okdialog.intro)。与工作表定义的枚举文件一样,常量文件总是生成到设置文件夹中——同样的程序集说明适用于此。

web 应用负责创作本地化表格;同步是编辑器的职责

  • 是什么: 创作、校验、覆盖率、铸造(minting)、语言视图和翻译工作簿,在浏览器中全部可用。写入 StringTable 则不可以——浏览器没有可供写入的 Unity 项目。
  • 为什么: 这是如实划定的范围,而不是缺失的功能:这些表格存在于项目之中。

相关页面