源设计起源 · 为什么要做这个模块
- 没有它,Excel 和业务代码之间就是手工搬运 + 运行时反射:工具把"读 Excel → 生成强类型结构体 + 容器 + 数据文件"固定成一条流水线,业务侧只用 await RevDataTableManager.LoadAsync<某表>() 取表,不必在运行时反射逐个字段去塞。
- 数据做成 struct、字段用 public,是为了性能与零开销:值类型让上万行配置不产生托管堆分配;生成代码直接给 public 字段赋值,做到零反射、零装箱。
- 数据文件用纯文本 TXT,不是 json / 二进制:为的是可读、可 diff、可手工核对 —— 出问题时能直接看、直接比对。
- 错误要在导出时炸,而不是运行时:主键为空 / 重复、类型写错、字段名不是合法标识符,都在导出前全量校验,报错带 Excel 行号(如"第 5 行 [Cost]:abc 不是合法的 int"),能直接指给策划;坏表跳过,其它表照常导出。
- 两个导出按钮,是为了别动不动就全量重编译:只改数值时用"仅生成数据":只写数据 txt、一个代码文件都不碰,回到 Unity 不会触发脚本重编译。
更完整的"早期版本 vs 新版 / 为什么重写"论证,见同目录《导表工具对比与设计说明》。
本篇只讲为什么;怎么用见同目录《使用说明》。
0三句话速览(完整用法见《使用说明》)
① 写 Excel:第 1 行字段名 / 第 2 行类型 / 第 3 行描述 / 第 4 行起数据(第一个字段是主键) ② 启动工具 → 选 Excel → 读取 → 选三个输出目录 → 开始导出 ③ 把 Code\ 拷到 Assets/Revolution/Generation\,把 Data\ 拷到 「资源根目录」\Data\(如 Assets/GameRes/Data) ④ 业务代码: await RevDataTableManager.LoadAsync<HeroTable>(); HeroTable.Instance.FindByKey(1001, out var h);
1准备
1.1 运行环境
- .NET 8 桌面运行时(Windows)。工程目标框架是
net8.0-windows。 - 工具是 WPF 独立程序,不需要开 Unity,也不需要装 Excel(自带 xlsx 解析)。
1.2 启动方式
# 方式一:命令行(开发期常用) cd RevolutionFramework\Revolution.ExcelTool dotnet run # 方式二:直接跑 exe(打包给策划/TA 用) Revolution.ExcelTool\bin\Debug\net8.0-windows\Revolution.ExcelTool.exe
dotnet publish -c Release,把产物目录整个拷走即可(零第三方依赖,只有一个 exe + dll)。1.3 界面长什么样
首次启动会自动猜你的 Unity 工程路径(在工具目录附近找 RevolutionFrameWork_Unity),猜到了就预填好三个输出目录;猜不到也不影响使用,手动选一下即可。
2Excel 该怎么写
2.1 四行结构(这是唯一的契约)
| 行 | 内容 | 说明 |
|---|---|---|
| 第 1 行 | 字段名 | 要能当 C# 字段名(见第五章) |
| 第 2 行 | 字段类型 | 只支持 int / float / string / bool |
| 第 3 行 | 字段描述 | 只生成代码注释和 txt 表头,不参与数据 |
| 第 4 行起 | 数据 | 一行一条记录 |
- 第一个字段(第一列)就是主键,不需要额外标记。
- 一个工作表 = 一张表,表名就是工作表名(
Hero→ 生成Hero结构体 +HeroTable容器)。
2.2 一个完整示例(可以直接抄)
工作表名:Hero
| A | B | C | D | E | F | G | H | I | |
|---|---|---|---|---|---|---|---|---|---|
| 1 | ID | Name | Quality | Attack | Hp | MoveSpeed | IsRanged | Camp | Desc |
| 2 | int | string | int | float | int | float | bool | string | string |
| 3 | 英雄ID | 英雄名 | 品质(1-5) | 攻击力 | 生命值 | 移动速度 | 是否远程 | 所属阵营 | 英雄描述 |
| 4 | 1001 | 亚瑟 | 3 | 120.5 | 3600 | 3.8 | false | 秩序 | 持剑的圣骑士 |
| 5 | 1002 | 妲己 | 3 | 95 | 3200 | 3.6 | true | 无序 | 魅惑的狐妖法师 |
| 6 | 1003 | 后羿 | 4 | 160.8 | 3100 | 3.7 | true | 秩序 | 射日的神射手 |
工作表名:Buff(演示 string 主键)
| A | B | C | D | E | F | |
|---|---|---|---|---|---|---|
| 1 | ID | Name | Type | Duration | Stackable | Desc |
| 2 | string | string | int | float | bool | string |
| 3 | 状态ID | 状态名 | 类型 | 持续(秒) | 可否叠加 | 状态描述 |
| 4 | BUFF_ATK_UP | 攻击提升 | 1 | 5 | false | 攻击力提升 20% |
| 5 | DEBUFF_STUN | 眩晕 | 3 | 1.5 | false | 无法移动与释放技能 |
Revolution.Demo\ExcelTool\Excel\DemoConfig.xlsx(4 张表:Hero / Item / Skill / Buff)。2.3 列的处理规则
| 情况 | 行为 |
|---|---|
| 第 1 行该列留空 | 整列被忽略(Excel 右侧常见的空列,不用管) |
列名以 # 开头 | 视为策划备注列,整列忽略(可以写"策划:这列别删") |
| 列数不一致 | 以第 1 行的字段数为准;某行少填的列按空值处理 |
2.4 行的处理规则
| 情况 | 行为 |
|---|---|
| 整行为空 | 跳过 |
该行第一列以 # 开头 | 视为注释行,整行跳过(可以在表尾写备注) |
| 主键列为空 | 报错(主键必填) |
| 主键重复 | 报错 |
3字段类型与取值规则
3.1 四种类型
| Excel 里写 | 生成的 C# 类型 | 运行时解析器 |
|---|---|---|
int | int | RevDataFieldParser.ToInt |
float | float | RevDataFieldParser.ToFloat |
string | string | RevDataFieldParser.ToStr |
bool | bool | RevDataFieldParser.ToBool |
大小写不敏感、前后空格会被去掉(Int、 int 都认)。
3.2 容错与默认值
| 类型 | 空单元格 / 解析失败 | 说明 |
|---|---|---|
int | 0 | 用 InvariantCulture 解析 |
float | 0f | 同上(系统小数点是逗号也不会读错) |
bool | false | 见下表 |
string | "" | null 统一成空串,省得业务到处判空 |
bool 认这些写法(不区分大小写):
| 真 | 假 |
|---|---|
true / 1 / 是 / yes / y | false / 0 / 否 / no / n |
int 列填了 abc 会在导表时报错(第 5 行 [Cost]:"abc" 不是合法的 int),不会被静默变成 0。
4主键规则
| 规则 | 说明 |
|---|---|
| 谁是主键 | 第一个字段(第一列),不需要额外标记 |
| 类型 | int / string 都行 → 分别生成 RevDataTable<int, T> / RevDataTable<string, T> |
| 必填 | 主键列为空 → 报错 |
| 唯一 | 主键重复 → 报错(导出期就拦截,不留到运行时) |
| 类型体检 | 用 float 或 bool 做主键会给出警告(浮点精度会导致"表里明明有却查不到";bool 最多两行数据) |
| ⚠️ 别踩 | 主键值不要以 # 开头(会被当成注释行整行跳过) |
字符串主键的典型用法(业务代号做主键):
BUFF_ATK_UP / DEBUFF_STUN / ITEM_SWORD_01
运行时就是:
BuffTable.Instance.FindByKey("BUFF_ATK_UP", out Buff buff);
5命名规则
5.1 字段名 → C# 字段名
| Excel 里的字段名 | 生成的 C# 字段 | 规则 |
|---|---|---|
ID | Id | 长度 ≤ 3 且全大写 → 首字母大写、其余小写 |
HP | Hp | 同上 |
Name | Name | 原样(首字母已是大写) |
moveSpeed | MoveSpeed | 首字母改大写 |
英雄名 | 英雄名 | 汉字是合法 C# 标识符,允许 |
英雄 id | 报错 | 有空格 → 不能作为标识符 |
2Name | 报错 | 数字开头 |
5.2 表名(工作表名)→ 类名
工作表名 "Hero" → 结构体 Hero + 容器 HeroTable + 数据文件 Hero.txt 工作表名 "Buff" → 结构体 Buff + 容器 BuffTable + 数据文件 Buff.txt 工作表名 "HeroSkin" → 结构体 HeroSkin + 容器 HeroSkinTable + 数据文件 HeroSkin.txt
my_hero 会生成 My_hero 这种不好看的类名和文件名。
6界面操作流程(六步)
-
选数据源
「选择文件」→ 可多选.xlsx(Excel 的临时文件~$xxx.xlsx会自动跳过);
「选择目录」→ 扫描该目录下顶层的所有.xlsx(不递归)。
选中后会自动读取一次。
源会被记住:下次启动会自动加载上次的源(窗口先出来,表随后加载); 点「选择文件 / 选择目录」时,对话框也直接落在上次那个目录。 源要是已被删 / 改名,只在日志里提示一句(不会刷红色错误),点「选择文件」重选即可。 -
点「读取」
日志里会列出每张表:### 表 Hero:主键=ID(int),9 字段 / 5 行 / 有效=True;有问题的表会打出[提示]/[错误]。 - 看左侧「表列表」 每项显示"字段数 · 行数";有错误的表会标红(点开看预览区里的具体错误)。
- 看右侧「表结构预览」 显示字段摘要(
★ID:int,Name:string…)+ 数据前 50 行,可以直接核对。 -
选三个输出目录(选过就会被记住,下次启动自动填回)
输出 建议位置 说明 数据结构类目录 Assets\Revolution\Generation\DataStructures生成 RevDataStructures.cs容器类目录 Assets\Revolution\Generation\RevDataTables生成 RevDataTables.csTXT 数据文件目录 <资源根目录>\Data(如Assets\GameRes\Data)每张表一个 <表名>.txt工具与 Unity 完全解耦:不预填、也不读 Unity 那边的配置,只记住你自己上次用过的源与目录 —— 存在本机%APPDATA%\Revolution\ExcelTool\settings.txt。想让它忘掉,删掉那个文件即可。
目录不存在也没关系,导出时会自动创建。 -
点「全量生成(代码+数据)」或「仅生成数据」 → 看⑥日志区
数据结构类 → ...\RevDataStructures.cs 容器类 → ...\RevDataTables.cs 数据文件 → ...\Data(4 个表,18 条数据) 导出成功:4 张表,共 18 条数据。
两个按钮怎么选:改了表结构(加字段 / 改类型 / 增删表)→ 「全量生成(代码+数据)」(代码 + 数据一起重新生成); 只改了 Excel 里的数值 → 「仅生成数据」:只写数据 txt,一个代码文件都不碰 —— 回到 Unity 也不会触发脚本重编译(省掉十几秒起步的一次全量编译)。
7输出物详解
7.1 RevDataStructures.cs(数据结构类)
namespace Revolution { /// <summary>Hero —— 配置表的一条数据(对应 Excel 一行;struct:值类型,零 GC)</summary> [Serializable] public struct Hero { /// <summary>英雄ID(★ 主键)</summary> public int Id; /// <summary>英雄名</summary> public string Name; // ... } }
struct而不是class:上万行配置不会在托管堆上分配,几乎不产生 GC。- 字段是
public字段(不是属性)—— 生成代码直接赋值,零反射零装箱。 - 描述来自 Excel 第 3 行,直接变成 XML 注释,写代码时鼠标悬停就能看到。
7.2 RevDataTables.cs(容器类)
namespace Revolution { /// <summary>Hero 表的容器(主键:ID,9 个字段,5 条数据)</summary> public sealed class HeroTable : RevDataTable<int, Hero> { /// <summary>便捷访问;未加载时为 null(先 RevDataTableManager.LoadAsync<HeroTable>())</summary> public static HeroTable Instance => RevDataTableManager.Get<HeroTable>(); public HeroTable() : base("Hero") { } protected override int GetKey(in Hero data) => data.Id; protected override bool ParseRow(string[] cells, out Hero data) { data = default; if (cells.Length < 9) return false; // 列数不够 → 数据文件与结构不匹配 data.Id = RevDataFieldParser.ToInt(cells[0]); // 英雄ID data.Name = RevDataFieldParser.ToStr(cells[1]); // 英雄名 // ... return true; } } }
RevResManager 的参数口径完全一致)——
ResourceRoot 默认 "Data/"(也就是生成的 RevResPath.Data),
ResourceName 默认 = 表名。所以 base("Hero") 等价于 ("Data/", "Hero")。
要把某张表放到别的目录就显式写:public HeroTable() : base("Hero", "Data/Activity/", "Hero") { }
每张表只生成两个方法(GetKey / ParseRow),查询能力全部继承自运行时基类 RevDataTable<TKey, TData>:
| 能力 | 用法 |
|---|---|
| 按主键查 | FindByKey(key, out data) / TryGet(key, out data) / Get(key) |
| 按下标查 | GetByIndex(index) |
| 遍历 | Count + GetByIndex 或 All |
| 判存在 | ContainsKey(key) |
一张表生成代码只有二十来行,而且改工具重新生成时不会动到数据结构文件(两个文件分开的用意)。
7.3 TXT 数据文件
以 Buff.txt 为例:
#Buff #ID Name Type Duration Stackable Desc #状态ID 状态名 类型 持续(秒) 可否叠加 状态描述 BUFF_ATK_UP 攻击提升 1 5 false 攻击力提升 20% DEBUFF_STUN 眩晕 3 1.5 false 无法移动与释放技能
格式规范:
| 规则 | 说明 |
|---|---|
| 一行一条记录 | 字段之间用 \t(制表符) 分隔 |
# 开头整行是注释 | 运行时整行跳过(表名 / 字段名 / 描述都放这里,方便肉眼核对与 git diff) |
| 空行跳过 | — |
| 行分隔符 | 写文件统一用 \n,读的时候兼容 \r\n |
| 编码 | UTF-8 不带 BOM(带 BOM 会让文件首字符变成 \uFEFF,注释行会失效) |
转义规则(字符串里出现这些字符时会被写成两个字符,运行时还原):
| 原字符 | 文件里写成 |
|---|---|
\ | \\ |
| 制表符 | \t |
| 回车 | \r |
| 换行 | \n |
空白单元格:保留为空,运行时取该类型的默认值:
3004 女王崇拜 1002 35 true 连续三次冲击... ← Damage 列空着 → 运行时读到 0f
为什么用 txt 而不是 json / 二进制:可读、可 diff、可手工核对(详见对比文档第三章)。
8接入 Unity
8.1 两个输出目录怎么选
Revolution.Demo\ExcelTool\Code\ → Assets\Revolution\Generation\ ← 生成代码(asmdef 已引用 Revolution.Runtime) Revolution.Demo\ExcelTool\Data\ → Assets\GameRes\Data\ ← 数据文件(逻辑路径正好是 Data\<表名>)
Assets\Revolution\Generation\下的Revolution.Generation.asmdef已引用Revolution.Runtime,所以生成出来的容器类能直接继承RevDataTable<,>。Assets\GameRes\只是示例:资源根目录由你在打包工具窗口里配置(代码里没有默认值),放在根目录下的文件逻辑路径 = 相对路径去掉扩展名,所以<资源根目录>/Data/Hero.txt的逻辑路径就是Data/Hero—— 正好等于生成代码里base("Hero")推出的默认路径。
导出后回 Unity,会自动刷新并编译(日志里也会提示)。
8.2 运行时加载(三行)
// ① 加载(走资源系统:编辑器直读工程、真机走 AB) await RevDataTableManager.LoadAsync<HeroTable>(); // ② 查询 if (HeroTable.Instance.FindByKey(1001, out Hero hero)) Debug.Log($"{hero.Name} 攻击力 {hero.Attack} 是否远程 {hero.IsRanged}"); // ③ 用完卸载(归还资源引用) RevDataTableManager.Unload<HeroTable>();
字符串主键 / 字符串表名同样支持:
BuffTable.Instance.FindByKey("BUFF_ATK_UP", out Buff buff); // 字符串主键 RevDataTableManager.Get("Buff"); // 按表名取表 RevDataTableManager.TryGet<BuffTable>("Buff", out BuffTable tbl); // 按表名取具体容器
8.3 走 AB 模式:数据表直接就能进包
数据文件是 .txt,而打包工具的排除后缀 excludeExtensions 默认只剩 .cs / .meta ——
所以数据表会正常进 ResMap,不需要任何额外设置。
⚠️ 但反过来要注意:如果你手动把 .txt 加进排除列表,数据表就会被静默踢出包:
不会进 ResMap → RevABResPolicy 查不到 → PathNotMapped
有 N 个资源被 excludeExtensions 排除,不会进包),
看到那条就去把 .txt 删掉。编辑器直读模式下不受影响(
RevEditorResPolicy 是按路径直接读工程的)。详见《资源加载系统架构解析》第四章「打包注意事项」。
9报错对照表
导出的报错都带行号(和 Excel 左上角行号一致),直接指给策划看:
| 报错信息 | 原因 | 怎么改 |
|---|---|---|
表 [X] 行数不足 3 行 | 缺表头 | 补全"字段名 / 类型 / 描述"三行 |
表 [X] 一个有效字段都没有 | 第 1 行全空 | 填字段名 |
字段 [X] 的类型 "xxx" 不支持 | 类型写了别的 | 改成 int / float / string / bool |
字段名 [X] 重复 | 两列同名 | 改名 |
字段名 [X] 不能作为 C# 标识符 | 有空格/标点/数字开头 | 改名(汉字可以) |
第 N 行:主键 [X] 为空 | 主键列没填 | 填上 |
第 N 行:主键 [X] = v 重复 | 主键重复 | 改值或删行 |
第 N 行 [X]:"abc" 不是合法的 int | 类型与数据不符 | 改数据或改类型 |
表 [X] 用 float 做主键不推荐 | 警告 | 建议改 int / string |
表 [X] 用 bool 做主键 | 警告 | 基本是配错了,确认一下 |
表 [X] 校验未通过,已跳过导出(N 个错误) | 该表有错 | 按上面逐条修;其它表照常导出 |
所有表都未通过校验,没有任何可导出的表 | 全军覆没 | 同上 |
存在同名表 [X](来自 Y) | 不同文件里出现同名工作表 | 改工作表名(会生成重复类名,编译不过) |
未指定XX目录 | 输出路径为空 | 在④里选目录 |
创建XX目录失败:... | 权限 / 路径非法 | 换一个可写目录 |
[错误] 读取 X 失败:... | 文件损坏或不是 xlsx | 用 Excel 打开后"另存为 .xlsx" |
10局限与边界(用之前先知道)
- 只支持
.xlsx(Excel 2007+)。老的.xls是 OLE 复合文档,不是 zip,读不了 —— 用 Excel 另存为 xlsx 即可。 - 不处理图表、批注、条件格式、合并单元格;公式取其缓存值(Excel 保存时会写入计算结果,够导表用)。
- 只支持 4 种类型(int / float / string / bool),不支持数组、嵌套结构、枚举。
- 只支持单主键,且必须是第一个字段。
- 每次导出会整体重写两个代码文件(表很多时会变大,并触发 Unity 重编译)。
- 没有 schema 版本号:靠"导表同时覆盖代码与数据"保证一致,别只拷其中一个。
- 坏表会被跳过(而非中止),所以导出后一定看一眼日志有没有
[提示],别以为"没报红色就全导了"。
11附:文件索引
| 文件 | 职责 |
|---|---|
Revolution.ExcelTool\Core\XlsxReader.cs | xlsx 解析(zip + XML) |
Revolution.ExcelTool\Core\ExcelTable.cs | 表模型 + 配置规则校验 |
Revolution.ExcelTool\Core\CodeGenerator.cs | 生成数据结构类 + 容器类 |
Revolution.ExcelTool\Core\DataTextWriter.cs | 生成 TXT 数据文件 |
Revolution.ExcelTool\Core\ExportService.cs | 导出编排(过滤 / 重名检查 / 编码 / 落盘) |
Revolution.ExcelTool\MainWindow.xaml(.cs) | 界面 |
Runtime\RevDataLoad\Core\RevDataTable.cs | 容器基类(字典 + 顺序表 + 查询 API) |
Runtime\RevDataLoad\Core\RevDataTextFormat.cs | TXT 格式(★ 与工具共用同一份源码) |
Runtime\RevDataLoad\Core\RevDataFieldParser.cs | 字段取值(容错 + InvariantCulture) |
Runtime\RevDataLoad\Manager\RevDataTableManager.cs | 运行时入口(加载 / 查询 / 卸载) |
Revolution.Demo\ExcelTool\ | 可直接使用的样例(Excel + 生成产物) |