需求分析

游戏项目中大量用到 excel 文件,作为游戏配置数据,在代码中引用,查找配置的数据,驱动游戏的运行。这就需要定义一个数据导入和使用的工作流

一个最简单有效的工作流如下:

  1. 策划跟程序讨论确定表格的结构

  2. 策划配表,转换成 csv 格式

  3. 程序根据表格结构书定义数据表,加载,查找相关逻辑

    1. 定义数据结构

    2. 实现加载 csv 的逻辑

    3. 根据需要构建索引表

  4. 程序在需要时引用,查找数据表

这个过程有些繁琐,同时 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 是可以进一步完善/优化的地方,但是不影响系统演示

posted on 2025-10-20 10:10  ycfenxi  阅读(3)  评论(0)    收藏  举报