表格语法
一份 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。一个标签页可以以 RecordId、IntId,或两者同时作为键。 |
RecordId@Effects | 对 Effects 标签页中一条记录的引用——会进行完整性验证(目标标签页存在、拥有键列、该 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? → 透明黑色 #00000000,AnimationCurve? → 一条没有关键帧的曲线,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?=1与RecordId@Effects?=fire——?与=都表示"可选",二选一即可。List<T>?——列表本身就允许为空。List<List<T>>——不支持嵌套列表。Pair<int?>——可选性是字段级别的记法,不属于内部类型的一部分。
取值规则
- bool:仅
true/false,输入时不区分大小写;规范形式为小写。 - 数字:小数点始终使用
.(与语言环境无关)。逗号小数、NaN和Infinity会在入口处被拒绝。 - 浮点数往返:导出时会渲染为最短的可往返格式,因此
1.0可能会以1的形式回来——但数值被精确保留(语义化往返)。 - 标记与 enum 的比较均为 Ordinal(序数)比较(不会有语言环境带来的意外)。
类型化资源引用(AssetRef@Group<Type>)
AssetRef@Icons 接受该组内的任意地址。AssetRef@Icons<Sprite> 把它收窄到一种资源类型,这个收窄会在三个环节被检查:验证、代码生成,以及创作层面。
- 哪些名字能被解析。 类型可以是项目能加载的任意
UnityEngine.Object派生资源类型——引擎类型(Sprite、Texture2D、AudioClip,或是像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。
视觉值类型(Color、AnimationCurve、Gradient)
有三种内置类型所承载的值,用原始文本是无法直接读懂的。它们的文本形式经过专门设计:一个人可以手写一个简短的版本,而每一个工具——无论是编辑器、导出、推送,还是 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:v、t:v:in:out、t: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——每一侧都是 Free、Auto、Linear、Constant、ClampedAuto 之一,与 Unity 曲线编辑器所用的名字完全相同。更短的形式会补全其余部分:一个 2 字段的关键帧会把与相邻关键帧的斜率当作切线(Linear/Linear),权重为 0.33333334 且不加权;一个 4 字段的关键帧会保留你写的切线(Free/Free);一个 7 字段的关键帧会加上权重。切线字段可以是 Infinity 或 -Infinity(表示一个 Constant 阶跃);时间、值和权重都必须是有限数,关键帧时间必须互不相同(关键帧会在导入时按时间排序,因此你输入它们的先后顺序不影响结果),且关键帧的数量没有上限。模式优先于数值:对任意不是 Free 的一侧,切线取值都会在导入时依据其模式重新计算——与 Unity 自身执行的计算完全相同——因此一个手写的、与其模式相矛盾的数字会被替换掉,工作表、各编辑器与游戏本身呈现的都会是同一条曲线。包裹模式有 ClampForever、Loop、PingPong 和 Default;Once 会被当作 ClampForever 的别名接受(Unity 会将其归一化),且永远不会被写回。没有任何关键帧的曲线没有文本形式:它只会以可选列的空单元格形式存在,导出时会被渲染为一个空单元格。
渐变:颜色关键帧不携带 alpha(在颜色分区中写 #RRGGBBAA 是错误的——alpha 有它自己的分区);在一个分区内,@t 时间要么全部出现,要么全部缺席,缺席时关键帧会被均匀分布(n = 1 → 0,n ≥ 2 → i/(n−1));缺少 alpha 分区意味着 1@0,1@1,缺少模式意味着 Blend。模式有 Blend、Fixed(阶梯)和 PerceptualBlend;可选的颜色空间(Gamma 或 Linear)只会影响 PerceptualBlend 的插值方式。时间与 alpha 的取值范围都是 0…1;时间在导入时会被量化为 16 位,与 Unity 存储它们的方式完全一致,因此你看到的值就是引擎持有的值。一个只有单个关键帧的渐变,经过 Unity 往返后会变成两个完全相同的关键帧——画面不会改变,只有关键帧数量会增加。
列表:List<Color> = #F00;#0F0,List<Gradient> = #F00,#00F;#0F0,#000——列表分隔符保持不变。
Data Studio 会把这些单元格显示为原生的颜色、曲线与渐变字段,web 应用则显示为带完整编辑器的预览——见 Data Studio 与 SheetForge Web。两者写入的都是规范形式;简写形式是给人手写用的。
键与唯一性
RecordId(不带@)是键列:每个标签页最多一个。- 零个键列也是合法的——直到另一个标签页引用这个标签页为止(
TargetTabHasNoKey)。 - 两个或更多则是错误(
MultipleKeyColumns)。 - 重复的键值(
DuplicateRecordId)以及空的键单元格都是错误。
- 零个键列也是合法的——直到另一个标签页引用这个标签页为止(
IntId是一个次级整数键:其唯一性是独立强制执行的,其他标签页可以通过IntId@Tab引用它。- 整数键引用会获得与
RecordId@Tab完全相同的完整性验证、最接近匹配建议、重命名传播,以及图/画布支持。
- 整数键引用会获得与
- 一个标签页可以只以
RecordId为键、只以IntId为键,或两者同时作为键,这三种情况在任何地方的行为都是对称的。- 当一个标签页同时带有两者时,
RecordId是显示/身份取值,整数键会伴随显示在旁边。 - 另一个标签页可以用任意一种方式指向同一条记录:通过其字符串键
RecordId@ThisTab,或通过其整数键IntId@ThisTab。
- 当一个标签页同时带有两者时,
@overlap:普通列默认允许重复值。在某列的@overlap单元格中填入false,即可强制实施基于值的唯一性。1.0与1视为相同的值;当两个列表的所有元素及其顺序都相同时,视为重复。- 两个空引用彼此永远不会被视为重复(而一个标量的空默认值仍然是一个普通取值)。
- 键列永远是唯一的;在键列上写
@overlaptrue会是一个矛盾错误。
工作表显示元数据(@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# 底层类型——byte、sbyte、short、ushort、int、uint、long、ulong之一。- 空单元格(或者在名为
Enum的标签页上完全没有@enum行)表示int。其他任何取值都会报InvalidEnumUnderlyingType。 - 代码生成会输出
public enum Grade : byte { … }。 - 对于
ulong,超出long.MaxValue的显式取值无法从工作表中支持——请改为从插件 C# 代码注册这类枚举。
- 空单元格(或者在名为
- 空单元格会被跳过,不会被当作成员读取,因此各列的长度可以不同,中间出现的空缺也会被直接跳过。
- 注释行(
#)在工作表的任何位置都会被忽略。一张表里有多个枚举、一个项目里有多张枚举表,都没有问题。所有枚举的名字在它们之间必须唯一,如果某个名字已经由插件从 C# 代码注册过,则以代码为准——工作表中的定义会被拒绝,报错为DuplicateEnumName。 - 名字与成员必须能作为合法的 C# 标识符:ASCII 字母、数字与
_,不能以数字开头,也不能是保留关键字(InvalidEnumIdentifier)。- 非 ASCII 字符被刻意拒绝,因为外观相似的 Unicode 标识符会产生一个谁也分不清彼此的类型。
- 一个已声明但下方没有任何成员的名字会报
EnumSheetEmptyColumn。 - 如果某一列里有任意一个成员失败,整个枚举都会被丢弃,而不是半成品式地注册。
- 导入会生成什么。 整个项目只有一个
SheetForgeEnums.cs——枚举是项目级别的产出,而不是按标签页产出的。它会被写入设置中指定的生成代码文件夹,命名空间与生成的标签页类型相同。首次导入会创建该类型、编译它,并在域重载之后完成烘焙,不需要额外点击任何东西。 - 无需打开工作表即可添加成员:Data Studio 中
Enum<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 与自定义单元格类型
- 功能与限制——关于
<>包装类型与自定义标记语法的边界