跳至主要内容
SheetForge

表格语法

一份 SheetForge 工作表是自描述的:列 A 保留给标记,实际数据从列 B 开始。行是通过其标记来识别的,而不是通过其位置,因此你可以在任何位置插入注释行,都不会破坏任何东西。

若要改造一份已有的工作表:在你的数据前插入一列标记列,并新增三行标记行。已有的数据列保持不变。

标记(列 A)

列 A含义
#注释行——完全被忽略,往返时会原样保留。
@name字段名行(每列一个名称)。
@type字段类型行。
@desc描述行——代码生成会将其烘焙进 XML 文档注释和检查器工具提示中。
@overlap(可选) 按列设置的重复策略——true(允许,默认值)/ false(强制值唯一)。
@style(可选) 工作表显示元数据——为该工作表设置一个分组标签和一种颜色。见下文。
@enum(可选) 将整张工作表标记为枚举定义表,而非一张数据表。见下文。
@loc(可选) 将整张工作表标记为本地化表——它的单元格为每一列命名对应的语言代码。见下文。
@yourMarker(可选,由插件注册) 一个自定义结构标记——见下文。
(空)数据行。
  • @name@type@desc必需的@overlap@style 以及任何自定义标记都是可选的。
  • 标记行可以以任意顺序出现,只要它们都位于数据行之上即可。
  • 未知的 @marker 会是一个错误,并附带最接近的匹配建议("你是不是想输入 @desc?")。已注册的自定义标记也会加入建议候选池。
  • 某一列没有 @name/@type 表头却存在数据,会是一个错误(孤立数据防护——绝不允许静默的数据丢失)。

示例(列显示为 A | B | C | D):

#        | Item definitions — hand-edited by design team
@name    | codeName      | displayName | price
@type    | RecordId      | string      | int=10
@desc    | unique key    | shown in UI | shop price (gold)
         | item.sword    | Sword       | 120
         | item.potion   | Potion      |

item.potion 这一行为空的 price 单元格会具化为显式默认值 10。)

类型系统

每种类型都是自描述的——读者仅凭 @type 单元格就能看出一列存放的是什么。

记法含义
int float bool string内置标量类型。
Enum<DamageType>一个 C# enum——既可以在一张枚举定义表中定义(见下文,无需代码),也可以由插件注册(EnumRegistry)。成员名会被验证,拼写错误会得到最接近的匹配建议。
List<T>一个列表——元素分隔符为 ;,元素会被去除首尾空白,空元素是错误,空单元格表示空列表。
RecordId该标签页的键列——一个字符串形式的自我标识符(例如 item.sword)。始终是必填标量。推荐的列名:codeName
IntId该标签页的次级整数键——每个标签页最多一个,必填标量,用于运行时/存档/后端 id。推荐的列名:id。一个标签页可以以 RecordIdIntId,或两者同时作为键。
RecordId@EffectsEffects 标签页中一条记录的引用——会进行完整性验证(目标标签页存在、拥有键列、该 id 可解析;拼写错误会给出建议)。
IntId@Effects通过整数键引用 Effects 标签页中的一条记录——与 RecordId@Effects 完全对等:以同样的方式进行完整性验证(目标标签页存在、拥有 IntId 列、该 id 可解析),未命中时会给出最接近的整数建议。取值会通过 int.ToString 规范化,因此手写的 007 会被解析为 7
AssetRef@Icons对 Addressables 组 Icons 中一个资源的引用——会对照目录验证其是否存在。子资源(贴图中的一个精灵、字体中的一个材质)以 parent[sub] 的形式寻址——这是 Addressables 为子对象条目分配的地址,例如 atlas[sword]——并以这个键完成验证、烘焙(SubObjectName)与导出。
AssetRef@Icons<Sprite>同样的引用,但限制为一种资源类型:只有当该地址上的资源——或它的某个子资源——能够被加载为 Sprite 时,这个地址才算通过。这个类型名可以是项目已知的任意 UnityEngine.Object 派生资源类型(引擎自带的或你自己的类型均可):当恰好只有一个类型匹配时写短名称,否则写全名MyGame.ItemData)。代码生成会输出 AssetReferenceT<Sprite>;不带 <…>AssetRef@Icons 依旧不受限制;AssetRef<Sprite>@Icons 这种写法会被拒绝,并给出正确拼写的建议。见下文的类型化资源引用
LocRef@Strings对本地化表 Strings 中一个本地化键的引用——完整性验证方式与 RecordId@Tab 相同(是否存在、最接近匹配建议、重命名传播、选择器、下拉菜单),并会内联预览该条目源语言的文本。目标标签页必须带有 @loc(否则报 LocRefTargetNotLocalizationSheet),不带 @Target 的裸 LocRef 会被拒绝。List<LocRef@Strings>LocRef@Strings? 可以照常组合。代码生成会输出一个普通的 LocRef 结构体——见本地化表格
Color · AnimationCurve · Gradient内置的视觉值类型。每一种都有一种紧凑的文本形式(见下文),Data Studio 与 web 应用会用原生的颜色、曲线或渐变编辑器来编辑它,而不是用原始文本;代码生成会输出 UnityEngine.Color / AnimationCurve / Gradient 字段。
Modifier (示例)一个插件注册的自定义单元格类型(见插件开发)——例如示例中的 stat:op:value 迷你语法。仅凭注册,CustomType@Target 这种写法也同样可行。当插件选择启用 IReferencingCellType 时,该列的行为会与 RecordId@Target 完全一致——同样会被验证、给出建议、随重命名更新、被绘制并可被选取。
Pair<T> (示例)一个插件注册的包装类型——一种通用的值形态 MyWrapper<T>,把若干个内部 T 值打包进一个单元格中(例如 Pair<int> = 1~2)。内部类型会被递归解析,因此 Pair<RecordId@Effects>Pair<Enum<DamageType>>,以及嵌套的 Box<Pair<int>> 都可以正常工作。见插件开发

<>@ 含义不同,可以共存:<> = 种类/包装(内置的 List,或插件提供的 MyWrapper<T>),@ = 目标。因此 List<RecordId@Effects> 是一个引用列表,而 Pair<RecordId@Effects> 打包了两个引用——两者都指向 Effects 标签页。整数键的组合方式相同:List<IntId@Effects> 是一个整数键引用列表。

包装类型(MyWrapper<T>

插件可以注册一个包装类型(wrapper)——一种拥有外层语法(分隔符、元数)并将内部类型委托给 Core 处理的通用值形态。该包装类型可以与任意内部类型组合。其内部的引用依然会被验证、在键重命名时被传播、在标签页重命名时被重写(完全透传)。

拒绝规则(与 List 保持一致):

记法是否允许?原因
Pair<RecordId@Effects> · Pair<Enum<E>> · Box<Pair<int>>允许包装一个标量、引用、enum,或另一个包装类型。
List<Pair<int>>允许一个由组合值构成的列表。包装类型自己的分隔符必须与 ;(列表分隔符)不同——这是插件作者的责任。
Pair<List<int>>不允许列表不能位于包装类型内部List 始终保持扁平、最外层,与 List<List<T>> 规则相同)。
Pair<int>@Effects不允许包装类型是一种值形态;应把 @ 放在内部叶子上(Pair<RecordId@Effects>)。
Pair<int?> · Pair<int=1>不允许可选性/默认值是字段级别的记法,不属于内部类型的一部分。

必填 / 可选 / 默认值

记法含义
float(无标记)必填——空单元格是错误(在入口处就阻止静默污染)。
float?可选——空单元格会具化为类型默认值0),并标记为 IsDefaulted。适用于这四种标量(int / float / bool / string),以及三种视觉类型:Color? → 透明黑色 #00000000AnimationCurve? → 一条没有关键帧的曲线,Gradient? → 白色渐变 `#FFFFFF@0,#FFFFFF@1
RecordId@Effects? · IntId@Effects? · AssetRef@Icons?可选引用——空单元格会具化为一个空引用:"指向虚无",目标标签页/组信息会被保留,并且该单元格标记为 IsDefaulted。这并不是一个损坏的引用——引用完整性校验和资源键校验都会跳过它,画布不会为它画出连线,@overlap 也不会把两个空引用算作重复。凡是确实带有取值的单元格,验证方式与以往完全相同,因此可选列中的拼写错误依然会被捕获。
RecordId@Effects=用显式写法表达同样的意思:一个空的显式默认值等价于上面单独的 ?。而一个非空的默认值(RecordId@Effects=fire)依然会被解析,并依然会接受完整性校验。
int=1显式默认值的可选项——空单元格会具化为 1
List<T>空单元格始终被允许(表示空列表)。

? 不被接受的地方,原因始终相同:Core 无法凭空捏造出一个值,所以这些类型都需要一个显式的 =default。这涵盖了:

  • Enum<T>?
  • 插件自定义类型——Modifier?,包括 Modifier@Tab?
  • 包装类型——Pair<int>?

键列被排除在外则是出于另一个原因:空的键会滋生重复。因此 RecordId?(不带键的自我标识符形式)与 IntId? 同样会被拒绝。

其他被刻意拒绝的记法:

  • int?=1RecordId@Effects?=fire——?= 都表示"可选",二选一即可。
  • List<T>?——列表本身就允许为空。
  • List<List<T>>——不支持嵌套列表。
  • Pair<int?>——可选性是字段级别的记法,不属于内部类型的一部分。

取值规则

  • bool:仅 true / false,输入时不区分大小写;规范形式为小写。
  • 数字:小数点始终使用 .(与语言环境无关)。逗号小数、NaNInfinity 会在入口处被拒绝。
  • 浮点数往返:导出时会渲染为最短的可往返格式,因此 1.0 可能会以 1 的形式回来——但数值被精确保留(语义化往返)。
  • 标记与 enum 的比较均为 Ordinal(序数)比较(不会有语言环境带来的意外)。

类型化资源引用(AssetRef@Group<Type>

AssetRef@Icons 接受该组内的任意地址。AssetRef@Icons<Sprite> 把它收窄到一种资源类型,这个收窄会在三个环节被检查:验证、代码生成,以及创作层面。

  • 哪些名字能被解析。 类型可以是项目能加载的任意 UnityEngine.Object 派生资源类型——引擎类型(SpriteTexture2DAudioClip,或是像 Texture 这样的抽象基类)与你自己的 ScriptableObject 一视同仁;没有白名单限制。组件和仅编辑器类型不在候选之列。当恰好只有一个类型携带该名字时写短名称,否则写包含命名空间的全名。有歧义的名字(AmbiguousAssetType,附带列出的每一个候选项)和未知的名字(UnknownAssetType,附带最接近的匹配建议)都会在 @type 行上、按列报告一次。
  • 什么样的地址算通过。 当该地址上的资源本身,或它的任意一个子资源,能够被加载为该类型时,这个地址就满足限制——因此一张以 Sprite 模式导入的贴图能通过 <Sprite>,而一张普通贴图则会在逐个单元格上被报告为 AssetTypeMismatch。子资源本身可以用 parent[sub] 寻址,而这个键只会针对它自己的类型进行检查。
  • 生成代码无法引用的类型会被拒绝。 一个位于预定义程序集(Assembly-CSharp 及其同类——任何没有程序集定义文件的脚本文件夹)中的类型虽然能被找到,但会被报告为 AssetTypeNotReferenceable,因为生成的配套程序集无法引用这些预定义程序集,AssetReferenceT<T> 也就无法编译通过。请把该类型移入一个程序集定义,或者去掉 <…>
  • 代码生成会输出什么。 对已解析的类型输出 AssetReferenceT<global::UnityEngine.Sprite>,对不受限制的列输出 AssetReference。配套的程序集定义会自动引用该类型所在的程序集,并且解析出的全名会被计入架构指纹,因此重新映射这个名字会重新生成代码。
  • 可以像其他任意类型一样组合AssetRef@Icons<Sprite>?List<AssetRef@Icons<Sprite>>,以及像 Pair<AssetRef@Icons<Sprite>> 这样的包装类型都能正常工作;而 AssetRef@Icons<>(空)、AssetRef@Ic<ons(组名中出现尖括号)和 RecordId@Skills<X>(这项限制仅适用于 AssetRef)都是语法错误。
  • Data Studio 的列表单上有一个**类型…**按钮,它会列出候选类型,并为你重写 @type 单元格——见 Data Studio

视觉值类型(ColorAnimationCurveGradient

有三种内置类型所承载的值,用原始文本是无法直接读懂的。它们的文本形式经过专门设计:一个人可以手写一个简短的版本,而每一个工具——无论是编辑器、导出、推送,还是 web 应用——写入的始终是规范、完整的形式,一个值经过工作表 → Unity → 工作表的往返也不会有任何损失。

这三种类型共用同一套分隔符,其层级比列表分隔符低一级:在一个值内部,**条目(item)之间用 , 分隔,一个条目内的字段(field)**之间用 : 分隔,分区(section)之间用 | 分隔,而一个关键帧的时间则用 @ 附加。List<> 的元素依旧用 ; 分隔,且这三种记法本身永远不会包含 ;——因此 List<AnimationCurve> = 0:0,1:1;0:1,1:0 能够被干净地拆分。数字在任何地方都用 . 作为小数点(语言环境的逗号只会导致字段数量出错,绝不会悄悄产生一个错误的取值),分隔符周围的空白会被去除,并且对每一个可接受的输入,往返关系 parse(render(parse(x))) == parse(x) 都成立。

类型可接受的输入规范形式
Color#RGB#RGBA#RRGGBB#RRGGBBAA(不区分大小写,# 必需)颜色不透明时为大写的 #RRGGBB,否则为 #RRGGBBAA——例如 #FF8800#FF880080
AnimationCurve`key,key,…[pre:post],其中一个关键帧可以是 t:vt:v:in:outt:v:in:out:inW:outW:wm,或 t:v:in:out:inW:outW:wm:tm`(2, 4, 7 或 8 个字段——3, 5 和 6 个字段都是错误)
Gradient`colorKeys[alphaKeys[

颜色:这个值以四个字节存储。不支持 HDR(通道取值高于 1)——烘焙后的颜色在导出时会被钳制到 0…1 区间内。类型默认值是透明黑色 #00000000

曲线wm 是加权切线标志(0 表示无 · 1 表示入切线 · 2 表示出切线 · 3 表示两者皆是),tm 是切线模式Left/Right,其后可以再跟一个可选的 /broken——每一侧都是 FreeAutoLinearConstantClampedAuto 之一,与 Unity 曲线编辑器所用的名字完全相同。更短的形式会补全其余部分:一个 2 字段的关键帧会把与相邻关键帧的斜率当作切线(Linear/Linear),权重为 0.33333334 且不加权;一个 4 字段的关键帧会保留你写的切线(Free/Free);一个 7 字段的关键帧会加上权重。切线字段可以是 Infinity-Infinity(表示一个 Constant 阶跃);时间、值和权重都必须是有限数,关键帧时间必须互不相同(关键帧会在导入时按时间排序,因此你输入它们的先后顺序不影响结果),且关键帧的数量没有上限。模式优先于数值:对任意不是 Free 的一侧,切线取值都会在导入时依据其模式重新计算——与 Unity 自身执行的计算完全相同——因此一个手写的、与其模式相矛盾的数字会被替换掉,工作表、各编辑器与游戏本身呈现的都会是同一条曲线。包裹模式有 ClampForeverLoopPingPongDefaultOnce 会被当作 ClampForever 的别名接受(Unity 会将其归一化),且永远不会被写回。没有任何关键帧的曲线没有文本形式:它只会以可选列的空单元格形式存在,导出时会被渲染为一个空单元格。

渐变:颜色关键帧不携带 alpha(在颜色分区中写 #RRGGBBAA 是错误的——alpha 有它自己的分区);在一个分区内,@t 时间要么全部出现,要么全部缺席,缺席时关键帧会被均匀分布(n = 10n ≥ 2i/(n−1));缺少 alpha 分区意味着 1@0,1@1,缺少模式意味着 Blend。模式有 BlendFixed(阶梯)和 PerceptualBlend;可选的颜色空间(GammaLinear)只会影响 PerceptualBlend 的插值方式。时间与 alpha 的取值范围都是 0…1;时间在导入时会被量化为 16 位,与 Unity 存储它们的方式完全一致,因此你看到的值就是引擎持有的值。一个只有单个关键帧的渐变,经过 Unity 往返后会变成两个完全相同的关键帧——画面不会改变,只有关键帧数量会增加。

列表List<Color> = #F00;#0F0List<Gradient> = #F00,#00F;#0F0,#000——列表分隔符保持不变。

Data Studio 会把这些单元格显示为原生的颜色、曲线与渐变字段,web 应用则显示为带完整编辑器的预览——见 Data StudioSheetForge Web。两者写入的都是规范形式;简写形式是给人手写用的。

键与唯一性

  • RecordId(不带 @)是键列:每个标签页最多一个。
    • 零个键列也是合法的——直到另一个标签页引用这个标签页为止(TargetTabHasNoKey)。
    • 两个或更多则是错误(MultipleKeyColumns)。
    • 重复的键值(DuplicateRecordId)以及空的键单元格都是错误。
  • IntId 是一个次级整数键:其唯一性是独立强制执行的,其他标签页可以通过 IntId@Tab 引用它。
    • 整数键引用会获得与 RecordId@Tab 完全相同的完整性验证、最接近匹配建议、重命名传播,以及图/画布支持。
  • 一个标签页可以只以 RecordId 为键、只以 IntId 为键,或两者同时作为键,这三种情况在任何地方的行为都是对称的。
    • 当一个标签页同时带有两者时,RecordId 是显示/身份取值,整数键会伴随显示在旁边。
    • 另一个标签页可以用任意一种方式指向同一条记录:通过其字符串键 RecordId@ThisTab,或通过其整数键 IntId@ThisTab
  • @overlap:普通列默认允许重复值。在某列的 @overlap 单元格中填入 false,即可强制实施基于值的唯一性。
    • 1.01 视为相同的值;当两个列表的所有元素及其顺序都相同时,视为重复。
    • 两个空引用彼此永远不会被视为重复(而一个标量的空默认值仍然是一个普通取值)。
    • 键列永远是唯一的;在键列上写 @overlap true 会是一个矛盾错误。

工作表显示元数据(@style

@style 让一张工作表能够声明自己属于哪个分组、是什么颜色,从而让分组与配色存在于工作表本身,而不只是存在于编辑器里。它是唯一一个描述整张工作表而非其各列的标记。它的单元格因此不与列对齐——它们是一份从列 B 开始的自由 key=value 列表。

@style   | title=Combat  | color=#4D8FF0
@name    | codeName      | displayName | power
@type    | RecordId      | string      | int
@desc    | unique key    | shown in UI | attack power
         | skill.fire    | Fireball    | 12
效果
title任意文本共享同一个 title 的工作表会在 Data Studio 侧边栏中被归拢到该标题之下。分区按首次出现的顺序排列,工作表在各自分区内保留原有顺序;没有 title 的工作表则留在默认分区。
color#RRGGBB(六位十六进制数字)为该工作表在所有出现之处着色:侧边栏中的圆点、画布上的节点边框,以及每一个指向这张工作表的端口与连线。
  • 两个键都是可选的,且顺序不重要;可以只写一个、都写,或者都不写。空单元格会被忽略(用来占位的空单元格没有问题)。
  • 校验错误都会被报告为 MarkerCellInvalid,并给出单元格坐标和具体的修复方法:
    • 未知的键(附带最接近的匹配建议);
    • 重复的键;
    • 缺失的值;
    • 不是 #RRGGBB 格式的颜色。
  • 三位简写(#4AF)和颜色名称都被刻意拒绝,以确保该取值只有一种记法可以往返。
  • 仅用于显示:代码生成、烘焙和架构指纹都不会读取 @style。更改一张工作表的颜色不会重新生成代码,也不会重新烘焙 ScriptableObject。
  • 往返安全@style 行会像注释行一样被保留。新增、删除、移动和重命名列都不会影响它,因为它的单元格并不归属于任何列。编辑它需要通过 Group & color 表单(在 Data Studio 侧边栏中右键点击某张工作表),该表单会以规范形式重写该行。
  • 一张只有 @style(以及注释)的工作表会被视为"尚无表格":导入会带着警告跳过它,而不会因为缺失三个必需标记而失败。一旦你添加了 @name/@type/@desc,它就会被正常解析。见功能与限制
  • style 是一个保留的标记名——插件如果尝试注册它会被拒绝,而像 @styl 这样的拼写错误会得到 @style 作为建议。

枚举定义表(@enum

一个 Enum<T> 列需要一个 T。你可以从插件 C# 代码中注册一个(EnumRegistry),但你也完全可以直接把它写在工作表里——不需要代码,也不需要插件。以下两种情况之一成立时,一张工作表就会被当作枚举定义来读取:

  • 它带有一行 @enum 标记(该标签页可以取任意名字),或者
  • 该标签页的名字恰好是 Enum(区分大小写),并且没有 @type 行。

第二条规则要求 @type 必须刻意缺席:一张数据表总会带有它,所以一张恰好被命名为 Enum 的既有数据表,仍然会被当作数据表。一张同时带有 @enum@type 的工作表是自相矛盾的,会被报告为 EnumSheetMarkerConflict,而不会被凭猜测处理。

@desc 不参与这项判定——它在两种工作表上都是合法的,在枚举定义表上,它描述的是该列所对应的枚举(见下文)。

一张枚举定义表没有表格——没有架构、没有键列、没有记录。一列就是一个枚举@name 单元格保存枚举的名字,它下方的每一行(列 A 留空)都是一个成员。

@enum    | byte       |
@desc    | Damage kind| Elemental affinity
@name    | DamageType | Element
         | Physical   | Fire
         | Magical=10 | Ice
         | True       | Lightning

这张工作表定义了两个枚举,此后 Enum<DamageType> / Enum<Element> 就能在任何 @type 单元格中被解析——列的记法本身没有变化。上面展示的三项附加内容都是可选的;一张仅有 @name 行加成员的工作表,依然是一张完整的枚举定义表。

你不必自己手动敲出这个骨架:Create sheet 提供了一个 Enum definitions 模板,帮你把工作表排布好,这是两种内置模板之一(见 Data Studio ▸ Sheet create / delete)。

  • 顺序即取值,Name=value 可以固定它。 一个成员单元格要么是一个纯名称,要么是 Name=value(带一个显式整数)——与 C# 的 enum 规则完全一致:未标号的成员取值为前一个成员的值加一,第一个成员为 0
    • Normal / Rare=10 / Epic 会编译为 0 / 10 / 11。代码生成只会在你写了 = value 的地方才输出它。
    • 数据单元格与下拉菜单始终使用名称Rare,绝不会是 Rare=10)。
    • 如果取值不是一个纯整数,或者超出了底层类型的取值范围(包括通过自动递增产生的越界),会报 InvalidEnumMemberValue
    • 这也是 Data Studio 从不重新排列成员、也从不回填空缺的原因:挪动一个成员会悄无声息地改变已经烘焙进资源、存进存档文件里的取值。
  • @desc 描述该枚举。 一列的 @desc 单元格会成为生成代码中该枚举的 XML <summary>(IDE 中的工具提示),与数据表字段 @desc 的用意相同。空单元格 = 无描述;该标记行本身是可选的。
  • @enum 单元格用于指定底层类型。 @enum 行在某一列的单元格中,可以指定该枚举的 C# 底层类型——bytesbyteshortushortintuintlongulong 之一。
    • 空单元格(或者在名为 Enum 的标签页上完全没有 @enum 行)表示 int。其他任何取值都会报 InvalidEnumUnderlyingType
    • 代码生成会输出 public enum Grade : byte { … }
    • 对于 ulong,超出 long.MaxValue 的显式取值无法从工作表中支持——请改为从插件 C# 代码注册这类枚举。
  • 空单元格会被跳过,不会被当作成员读取,因此各列的长度可以不同,中间出现的空缺也会被直接跳过。
  • 注释行(#)在工作表的任何位置都会被忽略。一张表里有多个枚举、一个项目里有多张枚举表,都没有问题。所有枚举的名字在它们之间必须唯一,如果某个名字已经由插件从 C# 代码注册过,则以代码为准——工作表中的定义会被拒绝,报错为 DuplicateEnumName
  • 名字与成员必须能作为合法的 C# 标识符:ASCII 字母、数字与 _,不能以数字开头,也不能是保留关键字(InvalidEnumIdentifier)。
    • 非 ASCII 字符被刻意拒绝,因为外观相似的 Unicode 标识符会产生一个谁也分不清彼此的类型。
    • 一个已声明但下方没有任何成员的名字会报 EnumSheetEmptyColumn
    • 如果某一列里有任意一个成员失败,整个枚举都会被丢弃,而不是半成品式地注册。
  • 导入会生成什么。 整个项目只有一个 SheetForgeEnums.cs——枚举是项目级别的产出,而不是按标签页产出的。它会被写入设置中指定的生成代码文件夹,命名空间与生成的标签页类型相同。首次导入会创建该类型、编译它,并在域重载之后完成烘焙,不需要额外点击任何东西。
  • 无需打开工作表即可添加成员Data StudioEnum<T> 单元格下拉菜单上带有 "Add a new member…",点击它会把新成员作为一步撤销操作暂存到枚举定义表中。从插件 C# 代码注册的枚举不会提供这一行——因为它归代码所有。
  • 枚举定义表没有记录,因此它们永远不会被烘焙进 ScriptableObject,Export/Push 也不会改动它们的文本;导入会把它们与被跳过的标签页分开报告。
  • 关于这里的两个边界——插件注册的枚举无法从工作表中被扩展,以及生成的枚举文件总是落在设置文件夹中——见功能与限制

本地化表(@loc

一行 @loc 标记会把这张工作表变成一张本地化表:行是键,列是语言,每个语言列的 @loc 单元格为其命名语言代码。

  • RecordId 键列是必需的——键值就是本地化键。
  • 第一个语言列是源语言
  • 语言列是字符串列。推荐写成 string?:这样一个空单元格代表的是覆盖率缺口,而不是错误。
  • 有两个按名称保留的可选列:smart(布尔值)与 comment(字符串)。
@loc     |            | en          | ko    |
@name    | codeName   | en          | ko    | comment
@type    | RecordId   | string?     | string? | string?
@desc    | key        | source text |       |
         | ui.ok      | OK          | 확인  | Confirm button

对于编辑、导出、推送、xlsx 和 web 应用来说,这张表依然是一张普通表格。变化的是输出:一个本地化标签页不会产生记录类,也不会产生 Database SO,取而代之的是按标签页生成的键常量——以及在安装了 Unity Localization 包的情况下——StringTable 同步。同一张工作表上同时出现 @enum@loc 是一个冲突错误。

完整的故事——LocRef 引用、铸造(minting)、桥接、翻译工作流——都在本地化表格中。

自定义结构标记(插件注册)

@overlap按列标记的内置示例:一个标记行的单元格按列携带一个值,逐列进行验证。插件可以用同样的方式注册自己的标记——例如一个 @curve 标记,用于标注每个数值列的插值方式。

该值会被存储为与领域无关的元数据(FieldSchema.MarkerValues),供验证器、边贡献者和创作窗口的列表头工具提示读取。Core 本身从不解释该值——验证被委托给标记定义本身。

  • 已注册的自定义标记会被完全按照 @overlap 的方式接受:可以出现在数据之上的任意位置,重复会被拒绝,出现在数据下方的标记是错误。
  • 每个标记只拥有它自己的按列取值验证(包括空单元格意味着什么)——它不会接管整行的解析。数据的"形状"仍然依靠归一化来表达(引用、List<T>type 列)。
  • 自定义标记用于列级元数据,而不是新的数据形状。注册示例见插件开发 §4.5
  • @style 是唯一一个不按列工作的内置标记(它描述的是整张工作表),因此它不是应该效仿的模型——@overlap 才是。

组合复杂数据:先归一化

表达复杂结构的推荐方式是引用组装("组装,而不是脚本化"):

  • 原子数据以行的形式存在于各自的标签页中。
  • 组合关系用引用列表表示:List<RecordId@Effects>
  • 一个 type 列(一个 enum)把数据行关联到代码中的一个原子——你的运行时对它做 switch 来分发行为。不需要内嵌的脚本语言。

迷你语法(例如 attack:add:10 这样的自定义单元格类型)适用于小型元组——Core 提供了 ;: 这两种约定;不要过度使用它们。

对于真正一次性的过程式逻辑,可以像引用一张图片一样引用一个脚本资源:List<AssetRef@Scripts>。SheetForge 会验证该引用并烘焙其可寻址地址;执行该脚本则是你的游戏自己的职责。

特殊的数据"形状":即便是看起来棘手的数据(例如数值曲线等),也能被干净地归一化(List<float>、引用组装)。自定义结构标记增加的是列级元数据(按列验证),而不是新的数据形状——先归一化数据,只有在确实需要非常冗长的逐列注释时,才使用自定义标记。见插件开发

相关页面

  • 核心概念——这些单元格在解析之后会发生什么
  • Data Studio——无需打开工作表即可编辑列/类型
  • 本地化表格——@loc 表格形状与 LocRef 引用的完整说明
  • 插件开发——注册 enum 与自定义单元格类型
  • 功能与限制——关于 <> 包装类型与自定义标记语法的边界