12.总结
12.2 核心要点速览
需求与交付目标
Excel 继续给策划改表。Player 里直接解析 xlsx 通常不划算,常见原因包括:
- 运行时要带 Excel 解析相关依赖,程序集和包体都变重;
- 启动或换表时读 xlsx,IO 和内存波动比读定长二进制明显;
- 表结构更容易以「原表格式」留在玩家可见路径里。
因此把解析和生成放在编辑器:产出 C# 数据类 + 自定格式的 .tao,运行时只按字节布局读文件,用 Dictionary 按主键取行即可。
触发方式是一条编辑器菜单命令(课内是 GameTool/GenerateExcel):扫配置目录下的工作簿,每个文件、每张 Sheet 按同一套「五行表头」规则走同一套代码,避免手抄列名和类型。
与需求分析篇列的三条交付物一致,点一次菜单应产出:
| 产物 | 用途 |
|---|---|
.tao |
GenerateExcelBinary 写入、LoadTable 读出,布局必须对称;运行时不再引用 Excel 解析库 |
数据类 *.cs |
Sheet 名即类名;第 1、2 行表头决定字段名与 int/float/bool/string |
*Container.cs |
对外就是一个 Dictionary<主键类型, 数据类> dataDic;主键列由第 3 行里标 key 的那一列决定 |
运行时在某个初始化点调用 LoadTable<TContainer, TRow>,把 TRow 对应的 TRow.tao 灌进内存;业务代码用 GetTable<TContainer>() 拿容器,再查 dataDic。整条工具链打成 unitypackage 给别的项目用时,说明里要写清 Excel 目录、StreamingAssets/Binary 与五行规则,否则路径或格式对不上会直接读失败。
编辑器菜单与资源刷新
配表管线要先在编辑器里有一键入口:MenuItem 挂在 UnityEditor 下,给静态方法贴路径字符串即可出现在菜单栏。路径里至少要有一层子菜单(字符串里出现 /),只写顶层一项会直接报错。方法不必挂在 MonoBehaviour 上,任意类里写静态方法都行。
磁盘上新建或改了 Assets 下的内容后,Project 窗口不一定立刻跟上,需要调 AssetDatabase.Refresh() 让 Unity 重新扫一遍。典型用法是代码里 Directory.CreateDirectory(...) 或写出文件后紧跟着刷新。
| API / 机制 | 作用 | 注意 |
|---|---|---|
MenuItem("父菜单/子项") |
在菜单栏注册命令 | 必须静态方法;路径含 / |
AssetDatabase.Refresh |
刷新资源数据库与 Project 视图 | 命名空间 UnityEditor |
Editor 文件夹 |
放仅编辑器需要的脚本与依赖 | 可多份、可嵌套;不会打进玩家包 |
Excel 解析 DLL 放在哪
Excel 本质是带格式的二进制/容器数据,用 C# 读就要引入能解析 xlsx 规则的库,一般以 DLL 或源码形式放进工程。课里读表走编辑器菜单,习惯把整个 Excel 相关依赖和读表脚本都放在 Editor 下:运行时程序集不引用这些类型,包体更干净,也避免把仅编辑器用的 API 带进 Player。
从 xlsx 到内存里的表
打开文件用 FileStream 只读;用 ExcelReaderFactory.CreateOpenXmlReader(stream) 得到 IExcelDataReader,再 AsDataSet() 转成 DataSet。一张工作簿里多张 Sheet 对应 DataSet.Tables 里多个 DataTable,按表名、行、列往下取单元格用 DataRow 和列索引即可。
读表时注意两件事:DLL 若在 Editor 程序集,读表脚本也必须在 Editor 文件夹,否则引用对不上;读的时候不要让本机 Excel 占着同一个文件,否则会 IOException。
| 类型 | 在流水线里的角色 |
|---|---|
FileStream |
打开磁盘上的 .xlsx / .xls |
IExcelDataReader |
流式读 Excel;课内用 CreateOpenXmlReader 对应 xlsx |
DataSet |
整本工作簿在内存中的容器 |
DataTable |
单张 Sheet |
DataRow |
一行,单元格用 row[k] 取 |
配表五行与表名
约定与工具代码硬编码在一起,动规则就要同步改生成与解析逻辑:
| 行号(从 0 计) | 含义 |
|---|---|
| 0 | 字段名,生成数据类里的成员名 |
| 1 | 字段类型,课内支持 int / float / bool / string |
| 2 | 主键列标记:该列写 key 表示主键 |
| 3 | 描述,给人看,生成逻辑不用 |
| 4 起 | 真实数据行 |
表名(Excel 里 Sheet 名)决定生成的 .cs 与 .tao 文件名:数据类叫 TableName.cs,容器叫 TableNameContainer.cs,二进制叫 TableName.tao。
ExcelTool:扫描目录与总控
- 常量
EXCEL_PATH指向放表的目录,默认Application.dataPath + "/ArtRes/Excel/"。 Directory.CreateDirectory(EXCEL_PATH)取DirectoryInfo,GetFiles()拿到目录下所有文件;扩展名不是.xlsx/.xls的一律跳过。- 每个 Excel 用
FileStream+CreateOpenXmlReader读到DataSet.Tables,再foreach (DataTable table in tableCollection),对每张表依次:数据类 → 容器类 → 二进制。
生成数据结构类
- 取
Rows[0]为字段名行,Rows[1]为类型行。 - 输出目录
DATA_CLASS_PATH,不存在则创建。 - 本质就是 字符串拼接 出
public class 表名 { public 类型 字段; ... },File.WriteAllText落盘,AssetDatabase.Refresh()。
生成容器类
- 输出目录
DATA_CONTAINER_PATH。 GetKeyIndex读 第 3 行Rows[2],找值为key的列,返回列索引;没有则返回0(依赖策划表不要标错)。- 用
GetVariableTypeRow拿到主键的 C# 类型,拼出public class 表名Container,字段为public Dictionary<主键类型, 表名> dataDic = new Dictionary<...>();(注意>与dataDic之间要有空格,否则生成代码非法)。
生成 .tao 自定义二进制
- 输出目录课里为
Application.streamingAssetsPath + "/Binary/";ExcelTool.DATA_BINARY_PATH与BinaryDataMgr.DATA_BINARY_PATH必须同值,整合时一般只保留BinaryDataMgr上一处静态字段,编辑器里写文件也引用它,避免各写各的。 BEGIN_INDEX = 4,数据从第 5 行开始写。
| 段 | 字节布局 |
|---|---|
| 行数 | 4 字节 int,table.Rows.Count - 4(数据行条数) |
| 主键字段名 | 4 字节长度 + UTF-8 正文,与数据类字段名一致 |
| 每条数据行 | 按列下标遍历,类型看第 2 行:int/float 各 4;bool 1;string 为 4 字节长度 + UTF-8 |
运行时:BinaryDataMgr 与 LoadTable
BinaryDataMgr除课内自带的BinaryFormatter存盘路径persistentDataPath + "/Data/"外,扩展了tableDic:键为容器类型名,值为加载好的容器实例。LoadTable<T, K>():T为容器类,K为数据类。打开的文件路径是DATA_BINARY_PATH + typeof(K).Name + ".tao",即文件名跟数据类名走,不跟Container后缀走。- 读文件全量进
byte[],用index顺序解析:行数 → 主键名 →count行循环;每行反射new K(),按字段类型用BitConverter/ UTF-8 填字段,再取容器的dataDic、Add(主键值, 行对象)。文件头里的主键名字符串必须能在K上GetField到。 GetTable<T>()用typeof(T).Name查tableDic,与LoadTable里tableDic.Add(typeof(T).Name, …)一致。
资源包、改路径与静态初始化
- Excel 放
ArtRes/Excel,改目录改EXCEL_PATH。 - 生成类路径改
DATA_CLASS_PATH、DATA_CONTAINER_PATH。 - 二进制输出与读取路径改
DATA_BINARY_PATH(编辑器生成与BinaryDataMgr读取必须一致)。 - 扩展新类型要在
GenerateExcelBinary的写入 与LoadTable的读取 两处各加分支,否则运行时会读崩或数据错位。 - 单例实例字段要写在静态路径字段后面:若静态初始化顺序里先执行了单例构造再去
InitData里读路径,而此时DATA_BINARY_PATH等还没赋值,会拿到空或错误路径;把单例声明挪到静态路径之后可避免。
实现顺序建议(整条管线)
- 菜单能跑通,
EXCEL_PATH下放测试表,能遍历到文件与 Sheet。 - 生成数据类、刷新工程,确认能编译。
- 生成容器类,确认
Dictionary泛型与主键列一致。 - 生成
.tao,用十六进制或日志核对头与一行数据宽度。 - 运行时
LoadTable+GetTable取一行字段,再考虑打成unitypackage与说明文档。
// 菜单与刷新:需 UnityEditor、System.IO
[MenuItem("Lesson01_知识点补充_Unity添加菜单栏按钮/刷新Project窗口内容")]
private static void 刷新Project窗口内容()
{
Directory.CreateDirectory(Application.dataPath + "/刷新Project窗口内容测试文件夹");
AssetDatabase.Refresh();
}
// 读表:需 Excel(ExcelDataReader)、System.Data、System.IO;菜单项版本还需 UnityEditor
using (FileStream fs = File.Open(Application.dataPath + "/ArtRes/Excel/PlayerInfo.xlsx",
FileMode.Open, FileAccess.Read))
{
IExcelDataReader reader = ExcelReaderFactory.CreateOpenXmlReader(fs);
DataSet dataSet = reader.AsDataSet();
}
12.3 面试题精选
进阶题
1. 配表五行各自管什么,改表名会影响哪些产物
题目
Excel 里前四行在课内工具里分别承担什么角色?把 Sheet 表名改掉后,磁盘上会跟着变哪些文件名?
深入解析
- 行 0~3:字段名、类型、主键标记、描述;行 4 起才是写入二进制的数据。
BEGIN_INDEX = 4与文件头里Rows.Count - 4都默认「前四行是元数据」。 - 表名:字符串拼接生成类名与文件名:
表名.cs、表名Container.cs、表名.tao,以及LoadTable打开的是 **typeof(K).Name + ".tao"**,因此表名与 C# 类名必须合法且一致。 - 主键行:
GetKeyIndex只在第 3 行找key,标错列会导致Dictionary键类型或默认索引 0 与策划意图不符。
答题示例
前四行是字段名、类型、主键标记和说明,第五行开始才是数据。
表名会变成数据类名、容器类名里的数据类型名、以及 tao 文件名;运行时打开的是数据类名对应的 tao,所以改表名等于改一整套类名和文件名,要一起编译通过。
参考文章
- 5.Excel表自动生成相关文件-制定配表规则
- 7.Excel表自动生成相关文件-生成数据结构类
- 9.Excel表自动生成相关文件-生成二进制数据文件
2. 课内 .tao 文件头与每一列在磁盘上的形状
题目
GenerateExcelBinary 写入的第一个 int、后面的主键名、以及 string 列与 int 列在字节流里分别怎么排?
深入解析
- 首个 4 字节:
BitConverter.GetBytes(table.Rows.Count - 4),表示后续要读多少条「数据行」,和LoadTable里第一个ToInt32对应。 - 主键名:再 4 字节长度 + UTF-8 字节;
LoadTable用这段字符串去GetField(keyName),必须与生成出来的数据类字段名一致。 - 列数据:按列下标顺序,对每一列看类型行:
int/float固定 4 字节,bool1 字节,string先长度 4 字节再正文。读写必须严格对称,多一字节少一字节后面全错位。
答题示例
先写一个 int 表示有多少行数据,再写主键字段名的长度和 UTF-8 内容。
后面每一行按列遍历:int、float 各四个字节,bool 一个字节,string 先四个字节长度再跟内容。读的时候按同样顺序推进 index。
参考文章
- 9.Excel表自动生成相关文件-生成二进制数据文件
- 10.Excel数据文件的使用
3. 主键列是怎么从表里选出来的
题目
GetKeyIndex 读的是哪一行?若没有一列等于 key 会怎样?容器类字典的键类型从哪里来?
深入解析
- 读 **
Rows[2]**,即配表第三行;逐列比较ToString() == "key",命中返回列索引。 - 未命中返回
0,等价于默认第一列作主键,容易与策划表不一致,属于隐性坑。 - 容器类里
Dictionary<键类型, 数据类>的键类型取 **rowType[keyIndex]**,即类型行上主键那一列的类型字符串,必须与真实主键列类型一致。
答题示例
第三行是主键标记行,哪一列写了 key 就用哪一列当主键;找不到就退回 0 列,容易错。
字典键类型是从第二行类型行里主键那一格读出来的,所以类型行和 key 标记必须对准同一列。
参考文章
- 8.Excel表自动生成相关文件-生成容器类
深度题
1. 为什么打开文件用 typeof(K).Name,字典里却用 typeof(T).Name
题目
LoadTable<T,K> 里文件路径用数据结构类名,而 tableDic 用容器类名存表,这样设计各解决什么问题?改其中一侧命名要注意什么?
深入解析
- 磁盘文件名:与生成二进制时
table.TableName + ".tao"一致,而表名对应的是 数据类K,不是KContainer,所以读必须用K的名字找文件。 - 内存索引:对外常用「拿整张表」的 API 是
GetTable<TowerInfoContainer>(),自然用 容器类型 做字典键,和调用方泛型一致。 - 一致性:若手动改类名或文件名只改一侧,会出现「能编译但 Load 找不到文件」或
GetTable取不到实例。
答题示例
tao 文件是按数据类名存的,生成时用的是 Sheet 名等于数据类名,所以打开文件要用 K 的名字。
tableDic 是给业务用的,按容器类型取整张表,所以 key 用 T。磁盘文件名跟数据类 K,内存索引跟容器 T,改类名或表名要生成和加载两边一起动。
参考文章
- 9.Excel表自动生成相关文件-生成二进制数据文件
- 10.Excel数据文件的使用
2. 反射按字段读表与 Excel 列顺序之间的隐含约定
题目
LoadTable 里用 GetFields() 遍历字段再按顺序从 bytes 里取值,和 GenerateExcelBinary 按列下标写入之间,依赖什么前提?若字段声明顺序与表列顺序不一致会怎样?
深入解析
- 写入侧是 固定列序
j = 0 .. Columns.Count-1;读入侧是 反射字段枚举顺序,在课内实现下与声明顺序相关,但规范上反射不保证顺序稳定,工程上更稳妥的是按字段名与列名建映射。 - 一旦顺序错位,会出现「前面几列对、后面全乱」或运行期类型转换异常,排查要对照十六进制与单行列数。
答题示例
写入是按 Excel 列顺序挨个写的,读是按反射拿到的字段顺序挨个读,课里等于假设声明顺序和列顺序一致。
实际上 GetFields 顺序不能当长期协议,产线里更常见是按列名找字段或显式配置映射,否则改一下字段声明顺序就会静默读错。
参考文章
- 9.Excel表自动生成相关文件-生成二进制数据文件
- 10.Excel数据文件的使用
3. 单例为什么要放在静态路径字段后面
题目
课里提到把单例声明挪到 DATA_BINARY_PATH 等静态字段之后,否则 InitData 里可能取路径失败。结合 C# 静态字段初始化顺序说明原因。
深入解析
- 静态字段初始化大致按类内声明顺序执行;若
BinaryDataMgr的单例实例字段写在DATA_BINARY_PATH等路径字段之前,首次访问Instance触发初始化时,路径字段可能仍是null,构造或InitData里拼路径会失败。 - Unity 场景下还会在启动时立刻
LoadTable,等于把问题放大成「首帧读表路径为空」。
答题示例
静态成员按声明顺序初始化,单例如果写在前面,new 单例或进构造函数时后面的路径静态字段可能还没赋值。
把路径常量写在前面、单例写在后面,保证用到路径时字段已经初始化完;或者不要在静态初始化阶段做读表,放到显式 Init。
参考文章
- 11.生成资源包
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com