需求分析
游戏项目中大量用到 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;
///
/// 导入 Excel 文件,生成配置类和二进制配置文件
///
[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;
///
/// 执行导入
///
/// 导入上下文
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,写入长度0
sb.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 fields = new List();
// 类的版本号,根据字段名和字段类型计算得出,将表格导出成二进制时写入,读取时校验
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");
}
// 如果是行数据类,也要用 class
else if (name == "Data")
{
sb.AppendFormat($"{space}public class {name}\n");
sb.Append($"{space}{{ \n");
}
// 表中的字段,如果是对象,用 struct
else
{
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];
// 数组写入长度 0
if (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 baseTypes = new Dictionary()
{
{ "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;
///
/// 将 Excel 文件,导出成二进制配置文件
///
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))
{
// 创建 BinaryWriter
using (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 configTables = new Dictionary();
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 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 是可以进一步完善/优化的地方,但是不影响系统演示
浙公网安备 33010602011771号