12.Binary实践项目总结

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)DirectoryInfoGetFiles() 拿到目录下所有文件;扩展名不是 .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_PATHBinaryDataMgr.DATA_BINARY_PATH 必须同值,整合时一般只保留 BinaryDataMgr 上一处静态字段,编辑器里写文件也引用它,避免各写各的。
  • BEGIN_INDEX = 4,数据从第 5 行开始写。
字节布局
行数 4 字节 inttable.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 填字段,再取容器的 dataDicAdd(主键值, 行对象)。文件头里的主键名字符串必须能在 KGetField 到。
  • GetTable<T>()typeof(T).NametableDic,与 LoadTabletableDic.Add(typeof(T).Name, …) 一致。

资源包、改路径与静态初始化

  • Excel 放 ArtRes/Excel,改目录改 EXCEL_PATH
  • 生成类路径改 DATA_CLASS_PATHDATA_CONTAINER_PATH
  • 二进制输出与读取路径改 DATA_BINARY_PATH(编辑器生成与 BinaryDataMgr 读取必须一致)。
  • 扩展新类型要在 GenerateExcelBinary 的写入LoadTable 的读取 两处各加分支,否则运行时会读崩或数据错位。
  • 单例实例字段要写在静态路径字段后面:若静态初始化顺序里先执行了单例构造再去 InitData 里读路径,而此时 DATA_BINARY_PATH 等还没赋值,会拿到空或错误路径;把单例声明挪到静态路径之后可避免。

实现顺序建议(整条管线)

  1. 菜单能跑通,EXCEL_PATH 下放测试表,能遍历到文件与 Sheet。
  2. 生成数据类、刷新工程,确认能编译。
  3. 生成容器类,确认 Dictionary 泛型与主键列一致。
  4. 生成 .tao,用十六进制或日志核对头与一行数据宽度。
  5. 运行时 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 字节,bool 1 字节,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

×

喜欢就点赞,疼爱就打赏