Unity excel 表格文件导入
需求分析
游戏项目中大量用到 excel 文件,作为游戏配置数据,在代码中引用,查找配置的数据,驱动游戏的运行。这就需要定义一个数据导入和使用的工作流
一个最简单有效的工作流如下:
-
策划跟程序讨论确定表格的结构
-
策划配表,转换成 csv 格式
-
程序根据表格结构书定义数据表,加载,查找相关逻辑
-
定义数据结构
-
实现加载 csv 的逻辑
-
根据需要构建索引表
-
-
程序在需要时引用,查找数据表
这个过程有些繁琐,同时 csv 是文本格式,文件体积大,解析效率低。
因此我们希望将该工作流简化,尽量自动化,并将数据导成二进制格式进行存储。因此我们的目标是:excel 作为资产,实现定制的导入逻辑,这样可以在编辑器中右键 导入,完成整个工作流
-
定义自定义的导入器,实现导入过程
-
根据表格的结构,自动生成上面步骤 3 中的过程定义的代码,包括:
-
生成定义数据结构的代码
-
生成读写文件的代码
-
根据表格定义,生成索引代码
-
-
将数据导出成二进制格式
-
-
定义二进制数据加载模块,自动加载所有导入的二进制数据
-
策划导入表格的同时,生成代码,如果出现错误,或没有与程序沟通就改变了表结构,由于生成的代码会改变,因此引用生成代码的地方就会编译错误,则可以在表格工作流的最上游,及时发现错误,并改正。
需求设计
由于需要生成代码,因此我们要能够通过分析 excel 得到“对象元”数据,其中
-
表元数据,只有一个字段:行元数据
-
行元数据,表中的每个列,作为其属性字段
-
内置元数据,从第三行开始,表中的每个格子,可能是简单类型,也可以是由 json 定义的对象类型,定义为内置元数据
元数据除了定义对象,还要实现一些逻辑:
-
生成代码相关逻辑
-
类代码
-
属性定义代码
-
Read/Write 代码
-
BuildIndex 相关代码
-
-
excel 中的数据可能是空的,因此要实现写 null 数据的代码
基于上述需求,我们定义 excel 的格式:
-
第一行为控制行
-
定义字段数据类型,bool, int, float, string, {json object},以及对应的数组类型:[bool], [int], [float], [string], [{json object}]
-
如果后面跟 * 则表示需要为该列创建索引。索引仅支持 int, string,且不可以是数组。
-
通过分析该行,构建表格的元对象数据
-
定义 end 作为结束控制符
-
-
第二行为名字
-
如果是简单类型,则加上前置的 _ 作为行对象的属性字段名字
-
如果是内置元对象类型,则名字是其类型名。
-
定义我们用来测试的 excel 文件:
三方库
读取 excel 文件,我们使用 EPPlus.dll
处理文件中定义的 json ,我们使用 NewtonSoft.json
实现
实现代码里加了详细的注释,因此不再写分析文档了,直接贴上代码。大家下载后,也可以直接使用
编辑器代码
编辑器代码仅在编辑器中使用,运行时不包含这些代码
导入器
通过派生 ScriptedImporter 定义 excel 文件导入器
using OfficeOpenXml;
using System.IO;
using System.Text;
using UnityEditor.AssetImporters;/// <summary>
/// 导入 Excel 文件,生成配置类和二进制配置文件
/// </summary>
[ScriptedImporter(1, "xlsx")]
public class ExcelImporter : ScriptedImporter
{public static readonly string cfgFolder = "Assets/Config";public static readonly string genFolder = "Assets/Scripts/Config/Gen";// 当前处理的 Excel 工作表public ExcelWorksheet sheet;// 表对象的元数据,只有一个行对象的数组public TableMetaObject tableObject = null;// 行对象的元数据,包含所有字段public TableMetaObject rowObject = null;// 当前处理的 Excel 文件public string excelFile;/// <summary>/// 执行导入/// </summary>/// <param name="ctx">导入上下文</param>public override void OnImportAsset(AssetImportContext ctx){string path = ctx.assetPath;FileInfo fi = new FileInfo(path);if (fi.Exists){using (ExcelPackage package = new ExcelPackage(fi)){excelFile = fi.FullName;string pureName = fi.Name;pureName = pureName.Substring(0, pureName.IndexOf('.'));// 创建表元数据对象tableObject = new TableMetaObject();tableObject.isTable = true;tableObject.cfgFile = pureName;tableObject.name = "Table_" + pureName;// 表元数据只有一个字段,是行对象的数组TableField tableRows = new TableField();tableRows.name = "datas";tableRows.type = TableFieldType.Object;tableRows.isArray = true;tableObject.fields.Add(tableRows);// 创建行元数据对象rowObject = new TableMetaObject();rowObject.name = "Data";tableRows.objectType = rowObject;ExcelTableObjectParser parser = new ExcelTableObjectParser();// 目前仅支持第一个工作表// TODO:为 excel 的 meta 信息添加工作表索引,支持指定工作表或多个工作表,并以工作表名作为类名sheet = package.Workbook.Worksheets[1];// 解析列定义, 第一行作为类型定义,第二行作为字段名定义int col = 1;while (true){// 可以为空列object ctrlCell = sheet.Cells[1, col].Value;if (ctrlCell != null){string ctrl = ctrlCell.ToString();// end 标记列结束if (ctrl == "end")break;if (ctrl.Length > 0)parser.ParseCol(this, col, ctrl);}col++;}// 生成类定义代码// TODO: 代码预编译,如果报错,提示错误,并且不生成代码文件StringBuilder sb = new StringBuilder();sb.Append("using System.Collections.Generic;\nusing System.IO;\nusing UnityEngine;\n\n");tableObject.ToClassDesc(sb, "");if (Directory.Exists(genFolder) == false){Directory.CreateDirectory(genFolder);}string classFile = genFolder + "/Table_" + pureName + ".cs";File.WriteAllText(classFile, sb.ToString());//Debug.Log(sb.ToString());// 将表格数据,按照类型定义,导出为二进制文件if (Directory.Exists(cfgFolder) == false){Directory.CreateDirectory(cfgFolder);}string file = cfgFolder + pureName + ".cfg";ExcelTableImporter importer = new ExcelTableImporter();importer.ExportData(file, this);sheet = null;}}}}
元数据字段
using System.Text;// 表元数据中的字段类型
public enum TableFieldType
{String,Int,Float,Bool,Object
}// 表元数据中的字段定义
public class TableField
{// 如果该字段是属于行元数据,记录该字段在表格中的列号public int col = -1;// 字段名public string name;// 字段类型public TableFieldType type;// 是否是数组public bool isArray;// 是否需要建立索引,索引仅支持 int, string 类型public bool isIndex = false;// 如果字段类型是 Object,则记录该对象的元数据public TableMetaObject objectType;// 生成类成员定义代码public void ToClassDesc(StringBuilder sb, string space){string arr = isArray ? "[]" : "";sb.Append($"{space}public {GetTypeName()}{arr} _{name};\n");}// 生成写数据代码public void ToWriter(StringBuilder sb, string space){string space1 = space + " ";// 数组类型if (isArray){// 如果数组不为空且长度大于0,写入数组长度和每个元素sb.Append($"{space}if(_{name} != null && _{name}.Length > 0)\n");sb.Append($"{space}{{\n");// 先写入数组长度sb.Append($"{space1}w.Write((int)_{name}.Length);\n");string space2 = space1 + " ";// 循环写入每个元素sb.Append($"{space1}for(int i = 0; i < _{name}.Length; i++)\n");sb.Append($"{space1}{{\n");if (type == TableFieldType.Object){sb.Append($"{space2}_{name}[i].Write(w);\n");}else{sb.Append($"{space2}w.Write(_{name}[i]);\n");}// 结束循环sb.Append($"{space1}}}\n");sb.Append($"{space}}}\n");// 如果数组为空或长度为0,写入长度0sb.Append($"{space}else\n");sb.Append($"{space}{{\n");sb.Append($"{space1}w.Write((int)0);\n");sb.Append($"{space}}}\n");}// 非数组类型else{// 写对象if(type == TableFieldType.Object){sb.Append($"{space}_{name}.Write(w);\n");}// 写字符串,字符串可能为空else if (type == TableFieldType.String){sb.Append($"{space}if(_{name} != null) w.Write(_{name});\n");sb.Append($"{space}else w.Write(string.Empty);\n");}// 写其他类型,直接写else{sb.Append($"{space}w.Write(_{name});\n");}}}// 生成读数据代码public void ToReader(StringBuilder sb, string space){string space1 = space + " ";// 数组类型if (isArray){// 先读数组长度sb.Append($"{space}int count_{name} = r.ReadInt32();\n");// 如果长度大于0,创建数组并循环读取每个元素sb.Append($"{space}if(count_{name} > 0)\n");sb.Append($"{space}{{\n");sb.Append($"{space1}_{name} = new {GetTypeName()}[count_{name}];\n");sb.Append($"{space1}for(int i = 0; i < count_{name}; i++)\n");sb.Append($"{space1}{{\n");string space2 = space1 + " ";// 如果是对象,创建对象,并读数据if (type == TableFieldType.Object){sb.Append($"{space2}_{name}[i] = new {GetTypeName()}();\n");sb.Append($"{space2}if(_{name}[i].Read(r) == false)\n");// 生成版本不匹配处理代码PrintTypeMismatch(sb, space2);}// 其他类型,直接读数据else{sb.Append($"{space2}_{name}[i] = {GetReader()};\n");}sb.Append($"{space1}}}\n");sb.Append($"{space}}}\n");}// 非数组类型else{// 读对象if (type == TableFieldType.Object){sb.Append($"{space}if(_{name}.Read(r) == false)\n");PrintTypeMismatch(sb, space);}// 读其他类型else{sb.Append($"{space}_{name}={GetReader()};\n");}}}// 如果该字段需要建立索引,生成索引相关代码public void ToIndex(StringBuilder sb, StringBuilder sb2, string space){// 生成构建索引代码string multiLineString = @"
public Dictionary<{keyType}, int> {keyName}2Index = new Dictionary<{keyType}, int>();public void Build{keyName}Index()
{{keyName}2Index.Clear();if (_datas != null && _datas.Length > 0){for (int i = 0; i < _datas.Length; i++){{keyName}2Index[_datas[i]._{keyName}] = i;}}
}public Data GetDataBy{keyName}({keyType} key)
{if ({keyName}2Index.TryGetValue(key, out int index)){if (index >= 0 && index < _datas.Length)return _datas[index];}return null;
}
";multiLineString = multiLineString.Replace("{keyType}", GetTypeName());multiLineString = multiLineString.Replace("{keyName}", name);multiLineString = multiLineString.Replace("\n", "\n" + space);sb.Append(multiLineString);// 生成调用构建索引代码sb2.Append($"{space+" "}Build{name}Index();\n");}// 获取字段类型名称public string GetTypeName(){switch (type){case TableFieldType.String: return "string";case TableFieldType.Int: return "int";case TableFieldType.Float: return "float";case TableFieldType.Bool: return "bool";case TableFieldType.Object: return objectType.name;}return "";}// 获取读取该字段的代码public string GetReader(){switch (type){case TableFieldType.String: return "r.ReadString()";case TableFieldType.Int: return "r.ReadInt32()";case TableFieldType.Float: return "r.ReadDouble()";case TableFieldType.Bool: return "r.ReadBoolean()";}return "";}// 生成类型版本不匹配处理代码public void PrintTypeMismatch(StringBuilder sb, string space){sb.Append($"{space}{{\n");string subSpace = space + " ";sb.Append($"{subSpace}Debug.LogError(\"Type Version mismatch: {GetTypeName()}\");\n");sb.Append($"{subSpace}return false;\n");sb.Append($"{space}}}\n");}
}
元数据对象
using System.Collections.Generic;
using System.IO;
using System.Text;public class TableMetaObject
{// 要生成的类的名字public string name;// 类属性public List<TableField> fields = new List<TableField>();// 类的版本号,根据字段名和字段类型计算得出,将表格导出成二进制时写入,读取时校验public int versionCode = 0;// 是否是表格类,表格类需要实现 ITable 接口,并且有单例实例public bool isTable = false;// 如果是表格类,记录表格文件名public string cfgFile = null;// 生成类的代码public void ToClassDesc(StringBuilder sb, string space){string fieldSpace = space + " ";// 如果是表格类,生成 ITable 接口实现和单例if (isTable){sb.AppendFormat($"{space}public class {name} : ITable\n");sb.Append($"{space}{{ \n");sb.Append($"{fieldSpace}private const string tableName = \"{ExcelImporter.cfgFolder}/{cfgFile}.cfg\";\n");sb.Append($"{fieldSpace}public static {name} Instance() {{ return inst; }}\n");sb.Append($"{fieldSpace}private static {name} inst = null;\n");sb.Append($"{fieldSpace}public string GetTableFile() {{ return tableName; }}\n");sb.Append($"{fieldSpace}public int GetKey() {{ return versionCode; }}\n");sb.Append($"{fieldSpace}public void SetupInstance() {{ inst = this; }}\n");}// 如果是行数据类,也要用 classelse if (name == "Data"){sb.AppendFormat($"{space}public class {name}\n");sb.Append($"{space}{{ \n");}// 表中的字段,如果是对象,用 structelse{sb.AppendFormat($"{space}public struct {name}\n");sb.Append($"{space}{{ \n");}// 先生成所有字段中对象类型的类定义for (int i = 0; i < fields.Count; i++){if (fields[i].objectType != null){fields[i].objectType.ToClassDesc(sb, fieldSpace);sb.Append('\n');}}// 生成版本号静态字段sb.Append('\n');StringBuilder sb4Version = new StringBuilder();for (int i = 0; i < fields.Count; i++){fields[i].ToClassDesc(sb, fieldSpace);sb4Version.Append(fields[i].name).Append(fields[i].GetTypeName());}versionCode = sb4Version.ToString().GetHashCode();sb.Append($"{fieldSpace}private static readonly int versionCode = {versionCode};\n");string rwSpace = fieldSpace + " ";// 生成写入函数sb.Append('\n');sb.Append($"{fieldSpace}public void Write(BinaryWriter w)\n");sb.Append($"{fieldSpace}{{\n");// 写入版本号sb.Append($"{rwSpace}w.Write(versionCode);\n");// 写入各字段for (int i = 0; i < fields.Count; i++){fields[i].ToWriter(sb, rwSpace);}sb.Append($"{fieldSpace}}}\n");// 生成索引构建函数和调用函数代码,仅表格类需要string indexBuilder = string.Empty;if (isTable){TableMetaObject rowObject = fields[0].objectType;// 索引构建函数代码StringBuilder stringBuilder = new StringBuilder();// 索引构建函数调用代码StringBuilder stringBuilder2 = new StringBuilder();stringBuilder2.Append($"{fieldSpace}private void BuildIndex()\n");stringBuilder2.Append($"{fieldSpace}{{\n");for (int i = 0; i < rowObject.fields.Count; i++){// 如果是索引字段,生成索引构建代码if (rowObject.fields[i].isIndex){rowObject.fields[i].ToIndex(stringBuilder, stringBuilder2, fieldSpace);stringBuilder.Append('\n');}}stringBuilder2.Append($"{fieldSpace}}}\n");indexBuilder = stringBuilder.ToString();if(indexBuilder.Length > 0){indexBuilder += stringBuilder2.ToString();}}// 生成读取函数sb.Append('\n');sb.Append($"{fieldSpace}public bool Read(BinaryReader r)\n");sb.Append($"{fieldSpace}{{\n");// 读取并校验版本号sb.Append($"{rwSpace}int verCode = r.ReadInt32();\n");sb.Append($"{rwSpace}if(verCode != versionCode) return false;\n\n");// 读取各字段for (int i = 0; i < fields.Count; i++){fields[i].ToReader(sb, rwSpace);}// 如果是表格类,并且有索引字段,数据读取完成后,生成索引构建调用代码if (isTable && !string.IsNullOrEmpty(indexBuilder)){sb.Append('\n');sb.Append($"{rwSpace}BuildIndex();\n");}sb.Append($"{rwSpace}return true;\n");sb.Append($"{fieldSpace}}}\n");// 输出索引构建函数代码sb.Append(indexBuilder);sb.Append($"{space}}}\n\n");}// 到将 excel 导成二进制时,如果字段对象是空或值是空字符串,写入默认值public void WriteDefault(BinaryWriter w){w.Write(versionCode);for (int i = 0; i < fields.Count; i++){TableField f = fields[i];// 数组写入长度 0if (f.isArray){w.Write((int)0);}else{switch (f.type){case TableFieldType.Bool: w.Write(false); break;case TableFieldType.Int: w.Write(0); break;case TableFieldType.Float: w.Write(0f); break;case TableFieldType.String: w.Write(string.Empty); break;case TableFieldType.Object: f.objectType.WriteDefault(w); break;}}}}
}
元数据对象解析工具类
using System.Collections.Generic;
using UnityEngine;
using JArray = Newtonsoft.Json.Linq.JArray;
using JObject = Newtonsoft.Json.Linq.JObject;
using JProperty = Newtonsoft.Json.Linq.JProperty;
using JToken = Newtonsoft.Json.Linq.JToken;// 解析 excel 文件,生成表对象和行对象的元数据
public class ExcelTableObjectParser
{ExcelImporter importer;// 解析列定义,生成字段元数据。如果字段是对象类型,递归生成对象元数据public void ParseCol(ExcelImporter importer,int col, string ctrl){this.importer = importer;// 从第二行获取字段名,无效则忽略该列object nameObject = importer.sheet.Cells[2, col].Value;if (nameObject == null)return;string name = nameObject.ToString();if (name.Length == 0)return;// 第一行,如果以 * 结尾,表示希望为该字段建立索引bool index = false;if (ctrl[^1] == '*'){index = true;ctrl = ctrl.Substring(0, ctrl.Length - 1);}// 解析字段TableField field = ParseType(ctrl, name);if (field == null)return;// 记录字段在表格中的列号field.col = col;// 如果希望建立索引,检查字段类型是否合法if (index){// 仅支持 int, string 类型,且非数组类型,才能建立索引if ((field.type == TableFieldType.Int || field.type == TableFieldType.String) && field.isArray == false){field.isIndex = true;}else{Debug.LogError($"Excel file {importer.excelFile} col {col} field {name} Only supports int or string types that are not arrays for indexing.");field.isIndex = false;}}importer.rowObject.fields.Add(field);}// 解析字段类型定义// type: 类型定义字符串// name: 字段名// 支持基础类型:int, float, bool, string 及其数组类型 [int], [float], [bool], [string]// 支持对象类型:json 对象定义,如 {"id":"int","name":"string","attrs":[{"key":"string","value":"int"}]}private TableField ParseType(string type, string name){type = type.Trim();TableField field = new();field.type = TableFieldType.String;field.name = name;// 先尝试解析基础类型,如果不是基础类型,再尝试解析类类型if (ParseBaseType(type, field) == false){try{JToken jToken = JToken.Parse(type);// 尝试数组if (jToken is JArray){JArray jsonArray = (JArray)jToken;if (ParseArray(jsonArray, field) == false){return null;}}// 尝试对象else if (jToken is JObject){JObject jsonObject = (JObject)(jToken);if (ParseObject(jsonObject, field) == false){return null;}}else{return null;}}catch (System.Exception e){Debug.LogException(e);}}return field;}// 基础类型定义struct BaseTypeDesc{public TableFieldType type;public bool isArray;public BaseTypeDesc(TableFieldType t, bool arr = true){type = t;isArray = arr;}}// 基础类型映射表static readonly Dictionary<string, BaseTypeDesc> baseTypes = new Dictionary<string, BaseTypeDesc>(){{ "int", new BaseTypeDesc(TableFieldType.Int, false) },{ "[int]", new BaseTypeDesc(TableFieldType.Int, true) },{ "float", new BaseTypeDesc(TableFieldType.Float, false) },{ "[float]", new BaseTypeDesc(TableFieldType.Float, true) },{ "bool", new BaseTypeDesc(TableFieldType.Bool, false) },{ "[bool]", new BaseTypeDesc(TableFieldType.Bool, true) },{ "string", new BaseTypeDesc(TableFieldType.String, false) },{ "[string]", new BaseTypeDesc(TableFieldType.String, true) },};// 解析基础类型private bool ParseBaseType(string type, TableField field){string tmp = type.ToLower();if (string.IsNullOrEmpty(tmp))return false;if(baseTypes.TryGetValue(tmp, out BaseTypeDesc desc)){field.type = desc.type;field.isArray = desc.isArray;return true;}return false;}// 解析 json 定义的数组类型private bool ParseArray(JArray array, TableField field){if (array.Count != 1)return false;field.isArray = true;JObject jsonObject = (JObject)array[0];return ParseObject(jsonObject, field);}// 解析 json 定义的对象类型private bool ParseObject(JObject jsonObject, TableField field){TableMetaObject tableObject = new TableMetaObject();field.type = TableFieldType.Object;field.objectType = tableObject;tableObject.name = field.name;// 解析对象的每个属性,作为字段foreach (JProperty property in jsonObject.Properties()){TableField subField = new TableField();subField.name = property.Name;// 数组if (property.Value is JArray){if (ParseArray(property.Value as JArray, subField))tableObject.fields.Add(subField);}// 对象else if (property.Value is JObject){if (ParseObject(property.Value as JObject, subField))tableObject.fields.Add(subField);}// 基础类型else{// 如果解析基础类型失败,默认为 string 类型if (ParseBaseType(property.Value.ToString(), subField) == false)subField.type = TableFieldType.String;tableObject.fields.Add(subField);}}return true;}
}
Excel 导入成二进制的类
using System.IO;
using UnityEngine;
using JArray = Newtonsoft.Json.Linq.JArray;
using JObject = Newtonsoft.Json.Linq.JObject;
using JToken = Newtonsoft.Json.Linq.JToken;/// <summary>
/// 将 Excel 文件,导出成二进制配置文件
/// </summary>
public class ExcelTableImporter
{public ExcelImporter assetImporter = null;// 以二进制格式导出表格数据public bool ExportData(string file, ExcelImporter assetImporter){this.assetImporter = assetImporter;bool succ = true;if(File.Exists(file)){File.Delete(file);}// 打开文件流using (FileStream fs = new FileStream(file, FileMode.OpenOrCreate, FileAccess.ReadWrite)){// 创建 BinaryWriterusing (BinaryWriter bw = new BinaryWriter(fs)){// 先写入表版本号bw.Write(assetImporter.tableObject.versionCode);// 预留行数位置int countRows = 0;bw.Write(countRows);// 从第三行开始,逐行导出,直到遇到空行或 end 标记int row = 3;while (true){object ctrlCell = assetImporter.sheet.Cells[row, 1].Value;if (ctrlCell == null)break;string ctrl = ctrlCell.ToString();if (ctrl == "end")break;// 导出一行数据if (ExportRow(bw, row) == false){Debug.LogError($"Export file {assetImporter.excelFile} failed at row {row}");succ = false;}row++;countRows++;}// 回写行数int offset = sizeof(int);fs.Seek(offset, SeekOrigin.Begin);bw.Write(countRows);}}// 如果导出失败,删除文件if (succ == false)File.Delete(file);return succ;}// 导出一行数据// 行数据直接导出,不是通过行的元数据对象导出的private bool ExportRow(BinaryWriter bw, int row){bool succ = true;// 先写入行版本号bw.Write(assetImporter.rowObject.versionCode);// 逐字段导出for (int i = 0; i < assetImporter.rowObject.fields.Count; i++){int col = assetImporter.rowObject.fields[i].col;object cellObject = assetImporter.sheet.Cells[row, col].Value;string value = string.Empty;if (cellObject != null){value = cellObject.ToString();}// 空值,写入默认值if (value.Length == 0){if (ExportNull(bw, assetImporter.rowObject.fields[i]) == false){Debug.LogError($"Export file {assetImporter.excelFile} failed at [{row}][{col}].");succ = false;}}// 非空值,按类型写入else{if (Export(bw, assetImporter.rowObject.fields[i], value) == false){Debug.LogError($"Export file {assetImporter.excelFile} failed at [{row}][{col}].");succ = false;}}}return succ;}// 导出空值private bool ExportNull(BinaryWriter bw, TableField field){if (field.isArray == false){switch (field.type){case TableFieldType.String:bw.Write(string.Empty);return true;case TableFieldType.Bool:bw.Write(false);return true;case TableFieldType.Int:bw.Write((int)0);return true;case TableFieldType.Float:bw.Write(0f);return true;case TableFieldType.Object:field.objectType.WriteDefault(bw);return true;}Debug.LogError($"Export file {assetImporter.excelFile} failed. Error type!");return false;}else{bw.Write((int)0);return true;}}// 导出值private bool Export(BinaryWriter bw, TableField field, string value){switch (field.type){case TableFieldType.String:return ExportString(bw, field, value);case TableFieldType.Bool:return ExportBool(bw, field, value);case TableFieldType.Int:return ExportInt(bw, field, value);case TableFieldType.Float:return ExportFloat(bw, field, value);case TableFieldType.Object:return ExportObject(bw, field, value);}Debug.LogError($"Export file {assetImporter.excelFile} failed. Error type!");return false;}// 将字符串形式的数组,转换为字符串数组private string[] ToArray(string value){if (value[0] == '[')value = value.Substring(1);if (value[^1] == ']')value = value.Substring(0, value.Length - 1);return value.Split(',');}// 导出字符串private bool ExportString(BinaryWriter bw, TableField field, string value){if (field.isArray == false){bw.Write(value);return true;}else{string[] arr = ToArray(value);bw.Write(arr.Length);for (int i = 0; i < arr.Length; i++)bw.Write(arr[i]);return true;}}// 导出布尔值private bool ExportBool(BinaryWriter bw, TableField field, string value){if (field.isArray == false){if (bool.TryParse(value, out bool v)){bw.Write(v);return true;}else{bw.Write(false);return false;}}else{bool succ = true;string[] arr = ToArray(value);bw.Write(arr.Length);for (int i = 0; i < arr.Length; i++){if (bool.TryParse(arr[i], out bool v)){bw.Write(v);}else{bw.Write(false);succ = false;}}return succ;}}// 导出整数private bool ExportInt(BinaryWriter bw, TableField field, string value){if (field.isArray == false){if (int.TryParse(value, out int v)){bw.Write(v);return true;}else{bw.Write(0);return false;}}else{bool succ = true;string[] arr = ToArray(value);bw.Write(arr.Length);for (int i = 0; i < arr.Length; i++){if (int.TryParse(arr[i], out int v)){bw.Write(v);}else{bw.Write(0);succ = false;}}return succ;}}// 导出浮点数private bool ExportFloat(BinaryWriter bw, TableField field, string value){if (field.isArray == false){if (float.TryParse(value, out float v)){bw.Write(v);return true;}else{bw.Write(0f);return false;}}else{bool succ = true;string[] arr = ToArray(value);bw.Write(arr.Length);for (int i = 0; i < arr.Length; i++){if (float.TryParse(arr[i], out float v)){bw.Write(v);}else{bw.Write(0f);succ = false;}}return succ;}}// 导出表格字段中的对象或数组,以 json 字符串形式存储在表格中private bool ExportObject(BinaryWriter bw, TableField field, string value){JToken jToken = JToken.Parse(value);if (jToken == null)return false;if (field.isArray){JArray jsonArray = jToken as JArray;if (jsonArray == null)return false;bw.Write(jsonArray.Count);for (int i = 0; i < jsonArray.Count; i++){JObject jsonObject = jsonArray[i] as JObject;if (jsonObject == null)return false;if(ExportObject(bw, field, jsonObject) == false)return false;}return true;}else{JObject jsonObject = jToken as JObject;if (jsonObject == null)return false;return ExportObject(bw, field, jsonObject);}}// 导出对象private bool ExportObject(BinaryWriter bw, TableField field, JObject jsonObject){bw.Write(field.objectType.versionCode);for (int i = 0; i < field.objectType.fields.Count; i++){TableField subField = field.objectType.fields[i];JToken subToken = jsonObject.GetValue(subField.name);if (subToken == null){if (ExportNull(bw, subField) == false)return false;}else{if (Export(bw, subField, subToken.ToString()) == false)return false;}}return true;}
}
运行时代码
二进制数据加载模块
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using UnityEngine;public interface ITableStreamManager
{public Stream GetStream(string file);
}public class FileTableStreamManager : ITableStreamManager
{public Stream GetStream(string file){return new FileStream(file, FileMode.Open, FileAccess.Read);}
}public interface ITable
{public int GetKey();public string GetTableFile();public bool Read(BinaryReader br);public void SetupInstance();
}public class ConfigLoader
{public static ConfigLoader Instance{get{return instance;}}private static ConfigLoader instance = null;public ITableStreamManager streamManager;private Dictionary<int, ITable> configTables = new Dictionary<int, ITable>();public ConfigLoader(){instance = this;}public void LoadConfigs(){// 获取所有已经加载的程序集Assembly[] allAssemblies = AppDomain.CurrentDomain.GetAssemblies();// 获取我们的程序集string targetAssemblyName = "ConfigGen";Assembly assembly = allAssemblies.FirstOrDefault(a => a.FullName.StartsWith(targetAssemblyName));if (assembly != null){Debug.Log($"Found assembly: {assembly.FullName}");}else{Debug.Log($"Assembly '{targetAssemblyName}' not found.");}Type[] types = assembly.GetTypes();List<Type> subclasses = types.Where(t => typeof(ITable).IsAssignableFrom(t) && t != typeof(ITable)).ToList();// 创建并加载所有 ITable 的子类foreach (Type type in subclasses){ITable table = Activator.CreateInstance(type) as ITable;configTables.Add(table.GetKey(), table);table.SetupInstance();using (Stream s = streamManager.GetStream(table.GetTableFile())){using (BinaryReader br = new BinaryReader(s)){table.Read(br);}}}}
}
数据加载脚本
项目中,只需要添加该脚本,则会自动加载数据
using UnityEngine;public class ConfigManager : MonoBehaviour
{ConfigLoader loader = new ();private void Awake(){loader.streamManager = new FileTableStreamManager();loader.LoadConfigs();}
}
测试代码
测试代码中直接引用表格,访问其数据
using UnityEngine;public class ExcelLoaderTest : MonoBehaviour
{void Start(){// 一段测试代码Table_test inst = Table_test.Instance();Table_test.Data d = Table_test.Instance().GetDataByid(123);Table_test.Data d2 = Table_test.Instance().GetDataByname("名字");}
}
注意
-
代码里有些 TODO 是可以进一步完善/优化的地方,但是不影响系统演示