Skip to content

最佳实践

导表流水线示意

mermaid
flowchart LR
  Excel[Excel / CSV] --> Read[readWorkBook / translateWorkBook]
  Read --> Plugins[插件管线\n(tableRows → tableSchema → tableConvert ...)]
  Plugins --> Schema[Schema AST\n@khgame/schema]
  Plugins --> Convert[Convert 结果\n{ tids, result, collisions }]
  Schema --> Serializers[序列化器\njson / ts / ts-interface / jsonx / go / csharp]
  Convert --> Serializers
  Serializers --> Artifacts[稳定产物\nJSON / TS / 协议等]

自上而下的责任划分:Excel 只承载数据,插件阶段完成清洗/校验,序列化器专注于格式产出;任何问题都能快速定位到所属阶段。

跨表引用(外键关联)

tid 类型说明

tid 是系统内置的数值类型,专门用于表示表格主键(Table ID)。

  • tid 等同于 uint(无符号整数),但语义上明确表示这是一个表格 ID
  • tid 类型的值必须是纯数字(如 40060001),不能是字符串(如 "enemy:shambler"
  • 在类型系统中,tiduint 可以互换使用,但推荐用 tid 表达引用语义

详见:tid-rules 规范

核心原则

表之间的引用必须使用数字 ID(tid),而不是名称或其他字符串标识符。

✅ 正确做法

在需要引用其他表的字段中,使用 tid 类型并填写数字 ID:

# enemies.xlsx
@      @         @       string          ...
sector category  serial  name            ...
40     06        0001    Rift Shambler   ...  ← 生成 tid: 40060001
40     06        0002    Choir Acolyte   ...  ← 生成 tid: 40060002
# waves.xlsx
@      @         @       uint       tid        ...
sector category  serial  timestamp  enemyId    ...
60     08        0001    45         40060001   ...  ← 引用 enemies 表的 40060001
60     08        0002    95         40060002   ...  ← 引用 enemies 表的 40060002

关键点

  • enemyId 字段的类型声明为 tid(而不是 string
  • 数据行填写数字 ID(40060001),而不是名称(Rift Shambler)或自定义标识符(enemy:shambler

❌ 错误做法

# waves.xlsx - 错误示例
@      @         @       uint       string         ...
sector category  serial  timestamp  enemyId        ...
60     08        0001    45         enemy:shambler ...  ← 使用字符串引用
60     08        0002    95         Choir Acolyte  ...  ← 使用名称引用

问题

  • 运行时无法通过字符串找到对应的实体
  • 缺少类型约束,容易出现拼写错误
  • 数据迁移时难以追踪引用关系

多表引用示例

参考 example/game_01_minirpg/ 中的实践:

javascript
// heroes.xlsx
['@', '@', '@', 'uint', 'string', ..., 'tid', 'tid', 'tid', 'tid', 'tid', ...]
['categoryCode', 'subtypeCode', 'sequenceCode', 'sequence', 'name', ...,
 'signatureItem', 'primarySkill', 'supportSkill', 'ultimateSkill', 'unlockStage', ...]
['10', '00', '0001', 1, 'Aerin Frostshield', ...,
 30000004, 20001001, 20002001, 20006001, 40000001, ...]
//  ↑ 引用 items      ↑ 引用 skills                 ↑ 引用 stages
javascript
// stages.xlsx
['@', '@', '@', ..., 'tid', 'tid', 'tid', ..., 'tid', ...]
['categoryCode', 'routeCode', 'sequenceCode', ...,
 'bossSkill', 'bossEnemy', 'unlockHero', ..., 'prerequisiteStage', ...]
['40', '01', '0001', ..., 20005001, 50000001, 10000001, ..., 0, ...]
//                          ↑ skills ↑ enemies ↑ heroes      ↑ 0 表示无前置

tid 类型的特性

  • 自动验证:序列化时可以检测引用的 ID 是否存在(需要加载完整上下文)
  • 类型安全:在 TypeScript 输出中会生成明确的类型引用
  • 易于重构:修改 ID 规则时,所有引用关系都是显式的数字,便于批量替换

可选引用

如果某个引用字段可以为空(例如前置关卡可能不存在),使用 tid?

@      @         @       tid?                ...
sector category  serial  prerequisiteStage   ...
40     01        0001    0                   ...  ← 0 或空表示无前置
40     01        0002    40010001            ...  ← 引用前一关卡

在代码中检查时:

typescript
if (stage.prerequisiteStage && stage.prerequisiteStage !== 0) {
  // 有前置关卡
}

命名约定

为了提高可读性,引用字段建议使用统一的命名后缀:

  • xxxId: 单个引用(如 enemyId, weaponId, bossEnemy
  • xxxIds: 多个引用数组(如 rewardItemIds: tid[]
  • xxxMap: 引用到数值的映射(如 costMap: Map<tid, uint>

调试技巧

当遇到"找不到引用"的问题时:

  1. 检查类型声明:确保引用字段声明为 tid(不是 stringuint
  2. 检查数值格式:确保填写的是完整的数字 ID
  3. 验证 ID 存在性:检查被引用的表中是否真的存在该 ID
  4. 查看序列化输出:在生成的 JSON 中确认 tids 数组包含预期的 ID
bash
# 快速验证 tids
cat out/enemies.json | jq '.tids'
# 验证某个引用是否存在
cat out/waves.json | jq '.result."60080001".enemyId'
cat out/enemies.json | jq '.result."40060001"'

ID 规划

分段组合原则

使用多段 @ 列组合成有意义的 ID:

@      @         @
sector category  serial
10     01        0001    → 生成 tid: 10010001

优势

  • 第一段标识大类(10=英雄,20=技能,30=物品...)
  • 第二段标识子类(01=战士,02=法师...)
  • 第三段为流水号
  • ID 本身包含结构信息,便于人工识别和调试
  • 导出的 JSON/TS/Go/C# 产物都会在记录上附带 _tid 字段,可直接读取。

ID 段位规划建议

根据预期规模合理分配位数:

# 小型项目(千级)
@    @   @      → CCSSNN (6位)
10   01  01     → 100101

# 中型项目(万级)
@    @    @     → CCSSSNNN (8位)
10   001  001   → 10001001

# 大型项目(十万级)
@     @    @    → CCCSSNNNN (10位)
100   01   0001 → 100010001

TID 生成规则

  • tables 读取 @ 列时会优先使用 Excel 单元格的显示值(cell.w),因此只要在表格里设好格式,就能保留前导零、填充宽度等格式化效果。
  • 如果整张表缺少 @ 列,或某一行的任意 @ 段为空值,转换阶段会直接抛出错误,错误信息会包含表名和 Excel 行号,便于快速定位。
  • 多段 @ 拼接出的 TID 是字符串;在 TypeScript 产物里会额外提供 XXXTTID 的品牌类型和 camelTids 数组,可用来约束业务代码。
ts
import { heroes, heroesTids, toHeroesTID } from './protocol/heroes';
import type { HeroesTID, IHeroes } from './protocol/heroesInterface';

const heroId: HeroesTID = heroesTids[0];
const hero: IHeroes = heroes[heroId];

// 访问接口时,可直接读取 `_tid`
console.log(hero._tid);

function loadByString(id: string): IHeroes | undefined {
  return heroes[toHeroesTID(id)];
}

TableContext 会提供基础的 KHTableID 类型,导出的 XXXTTID 都是它的别名;若想在 TS 中拿到具体的枚举类型,记得在 Excel 标记行写成 enum<HeroClass>,这样生成的接口才会是 TableContext.HeroClass,避免退化成普通字符串。

0 值语义

约定 0 作为特殊值:

  • 在可选引用中表示"无引用"
  • 避免使用 0 作为有效实体的 ID
  • 代码中通过 if (id)if (id !== 0) 简化判断

数据完整性

冲突检测

始终使用 --strict 模式进行构建:

bash
tables -i ./src -o ./out -f json --strict

这会在 TID 冲突时立即报错,避免数据被静默覆盖。

单元测试

建议为关键的引用关系编写验证脚本:

javascript
const waves = require('./out/waves.json');
const enemies = require('./out/enemies.json');

const enemyIds = new Set(Object.keys(enemies.result));

for (const [waveId, wave] of Object.entries(waves.result)) {
  const enemyId = String(wave.enemyId);
  if (!enemyIds.has(enemyId)) {
    throw new Error(`Wave ${waveId} references invalid enemy ${enemyId}`);
  }
}
console.log('✓ All wave references are valid');

版本控制

  • _rebuild_data.js 脚本纳入版本控制
  • Excel 文件也建议纳入版本控制(便于追溯数据变更)
  • out/ 目录根据需要决定是否提交(建议至少提交一份作为基准)

文档编写指引

  • 结构化示例请使用标准 Markdown 表格模拟 Excel:列在横向(A、B、C…),行从上到下(标记行 → 描述行 → 示例数据)。
  • 每个示例至少包含标记行、描述行以及 1~2 条数据,确保读者能够直接对照单元格写法与转换结果。
  • 括号、泛型、装饰器等 Token 必须在表格中拆分到独立的列,以便与 @khgame/schema 的 Token 化规则保持一致。
  • 若展示 Pair / Map / Array 输入,用 key:value、竖线 | 等真实数据示例描述具体写法,并说明其在 TemplateConvertor / SchemaConvertor 中的解析方式。
  • 在文档中引用代码行为时标注路径(例如 node_modules/@khgame/schema/lib/convertor/richConvertor.js),方便读者交叉验证。

更多参考

MIT Licensed