Unity学习----【数据持久化】二进制数据(五)--由Excel自动生成数据结构类与二进制文件

·来源于唐老狮的视频教学,仅作记录和感悟记录,方便日后复习或者查找


一.如何在Unity中读取Excel表格

1.加入插件

把这个DLL拖入到Unity工程当中。自己去网上搜。

2.如何使用这个插件

2.1.打开Excel表

cs 复制代码
[MenuItem("GameTool/打开Excel表")]
    private static void OpenExcel()
    {
        using (FileStream fs = File.Open(Application.dataPath + "/ArtRes/Excel/PlayerInfo.xlsx", FileMode.Open, FileAccess.Read ))
        {
            //通过我们的文件流获取Excel数据
            IExcelDataReader excelReader = ExcelReaderFactory.CreateOpenXmlReader(fs);
            //将excel表中的数据转换为DataSet数据类型 方便我们 获取其中的内容
            DataSet result = excelReader.AsDataSet();
            //得到Excel文件中的所有表信息
            for (int i = 0; i < result.Tables.Count; i++)
            {
                Debug.Log("表名:" + result.Tables[i].TableName);
                Debug.Log("行数:" + result.Tables[i].Rows.Count);
                Debug.Log("列数:" + result.Tables[i].Columns.Count);
            }
            fs.Close();
        }
    }

①这里依然通过文件流来打开Excel表格,把打开的文件流用ExcelReaderFactory.CreateOpenXmlReader(fs)来返回一个IExcelDataReader的数据。

②之后要使用它的话得先把他转化成DataSet

③一个Excel文件中可以有很多表,它们存在Tables这个容器当中

④[MenuItem("GameTool/打开Excel表)]是一个特性,用于把一个公共静态类方法可以在编辑器顶部被呼出调用

2.2.获取表中单元格信息

cs 复制代码
[MenuItem("GameTool/读取Excel里的具体信息")]
    private static void ReadExcel()
    {
        using (FileStream fs = File.Open(Application.dataPath + "/ArtRes/Excel/PlayerInfo.xlsx", FileMode.Open, FileAccess.Read))
        {
            IExcelDataReader excelReader = ExcelReaderFactory.CreateOpenXmlReader(fs);
            DataSet result = excelReader.AsDataSet();

            for (int i = 0; i < result.Tables.Count; i++)
            {
                //得到其中一张表的具体数据
                DataTable table = result.Tables[i];
                //得到其中一行的数据
                //DataRow row = table.Rows[0];
                //得到行中某一列的信息
                //Debug.Log(row[1].ToString());
                DataRow row;
                for (int j = 0; j < table.Rows.Count; j++)
                {
                    //得到每一行的信息
                    row = table.Rows[j];
                    Debug.Log("*********新的一行************");
                    for (int k = 0; k < table.Columns.Count; k++)
                    {
                        Debug.Log(row[k].ToString());
                    }
                }
            }

            fs.Close();
        }
    }

①单元格信息这里我们通过DataRow这个容器来一行一行遍历范围。给这个容器中传入的索引就是这一行的第几列单元格。

②当然也可以通过DataColumn来一列一列遍历。相应地传入的索引就是这一列的第几行的单元格

③这两个属性变量分别可以通过table.Rows和table.Columns来获取。

3.其他补充说明


二.由Excel表格生成相应的数据结构类

所谓生成一个数据结构类,就是用代码自动生成一个相应的数据结构类文件脚本的。我们可以通过文件类来完成这个文本的写入。

那我们需要写入些什么呢?考虑一个常见的类,我们可以看到我们可以提前知道的字符是public, {}这几个。而类名,变量类型,变量名都是我们不知道的,因此可以考虑在Excel表格中提供

1.自定义表格规则

为了能够正确地通过表格生成相应的数据结构类,我们定义以下规则

  • 第一行:写入当前变量的名字
  • 第二行:写入当前变量的类型
  • 第三行:规定当前数据的主键【之所以要这个是为了后面的生成数据容器用,之后再说】
  • 第四行:相当于解释每个变量的作用,不参与数据读写(可以看成是注释)
  • 第五行往下:真正的数据
  • 表格名:即为类名(一张表对应一个类)

2.读入表格并根据规则生成数据结构类

2.1.读入表格

cs 复制代码
[MenuItem("GameTools/Generate Data From Excel")]
public static void GenerateDataFromExcel()
{
    Debug.Log("由Excel表格生成相应的数据类,数据容器,与二进制文件");

    //1.首先需要获取所有的Excel文件
    FileInfo[] fileInfos = Directory.CreateDirectory(BinaryDataMgr.EXCEL_PATH).GetFiles();
    for(int i = 0; i < fileInfos.Length; i++) {
        //只打开读取后缀为".xls"、".xlsx"的文件
        if (fileInfos[i].Extension != ".xls" && fileInfos[i].Extension != ".xlsx")
            continue;

        Debug.Log(fileInfos[i].Name);
        //打开Excel文件
        using(FileStream fs = File.Open(BinaryDataMgr.EXCEL_PATH + fileInfos[i].Name, FileMode.Open, FileAccess.Read)) {
            IExcelDataReader excelDataReader = ExcelReaderFactory.CreateOpenXmlReader(fs);
            DataSet dataSet =  excelDataReader.AsDataSet();

            //开始读取里面的表
            DataTableCollection tableCollection = dataSet.Tables;
            for(int j = 0; j < tableCollection.Count; j++) {
                //对于每个表都需要进行如下的操作

                //生成数据结构类
                GenerateDataClass(tableCollection[j]);

                //生成数据容器类
                GenerateDataContainer(tableCollection[j]);

                //生成二进制文件
                GenerateBinaryData(tableCollection[j]);
            }
        }
    }

    //刷新显示新创建的文件和文件夹
    AssetDatabase.Refresh();
}

①这里我们首先获取了所有Excel文件,它的特征是后缀为 ".xlsx" 或者 ".xls"

②然后我们通过之前学习的知识,打开文件后再逐个打开表格。因为我们规定一张表对应一个类,因此我们逐个遍历表格,给他进行数据结构类的生成操作。

  • 顺便一提,这几个文件的路径位置,我们放置在一个二进制数据管理类中。它这里也有地方需要访问这几个路径,而且不用放在编辑器文件夹中,因此写在这里。
  • Editor文件夹中的脚本可以访问之外的脚本,但是这之外的脚本不能访问Editor文件夹中的脚本

2.2.生成数据结构类

cs 复制代码
/// <summary>
/// 用于生成数据结构类
/// </summary>
/// <param name="table">根据哪张表来生成数据结构类</param>
private static void GenerateDataClass(DataTable table)
{
    //根据读写规则,第一行为字段名,第二行为字段类型,第三行指定哪个为主键,第四行是描述信息,第五行往后开始才是正式需要存储的数据
    //这里生成需要的数据结构类,我们只需要获取第二行的信息,以及表的名字作为该脚本的名字即可。
    DataRow variableNames = GetVariableNameRow(table);
    DataRow variableTypes = GetVariableTypeRow(table);
    int lenght = table.Columns.Count;
    //开始拼接需要写入到脚本中的文本
    string str = "public class " + table.TableName + "{\n";
    for (int i = 0; i < lenght; i++) {
        str += "\tpublic " + variableTypes[i] + " " + variableNames[i] + ";\n";
    }

    str += "}";

    //先检查一下创建位置的文件夹是否存在
    if (!Directory.Exists(BinaryDataMgr.CLASS_PATH)) {
        Directory.CreateDirectory(BinaryDataMgr.CLASS_PATH);
    }

    //接着打开或者创建一个脚本并把文本写入到其中去
    File.WriteAllText(BinaryDataMgr.CLASS_PATH + table.TableName + ".cs", str);
}
cs 复制代码
/// <summary>
/// 获取字段名行
/// </summary>
private static DataRow GetVariableNameRow(DataTable table)
{
    return table.Rows[0];
}

/// <summary>
/// 获取字段类型行
/// </summary>
private static DataRow GetVariableTypeRow(DataTable table)
{
    return table.Rows[1];
}

①这里我们按规定,先获取该表格第一二行。它们分别存储了变量名和变量类型

②然后我们一列一列地读取并用一个字符串拼接。拼接完毕之后先检查存储位置是否有对应文件夹存在,不存在就先创建一个,之后用File中的方法把字符串写入其中。

③文件的名字就是表的名字 + ".cs",表示这是一个C# 脚本文件


三.由Excel表格生成相应的数据容器类

数据容器类长这个样子。因为一张表中肯定是有很多数据,一行代表一个类的数据。因此我们需要用一个键来指定获取我们需要的类对象。因此用字典刚好可以满足我们的这个需要。这也是上面为什么有一行是用于指定哪一个变量为主键。

cs 复制代码
/// <summary>
/// 生成用于装载整个表中所有对象的数据容器
/// </summary>
/// <param name="dataTable">根据哪张表来生成数据容器</param>
private static void GenerateDataContainer(DataTable table)
{
    //生成的数据容器就是用字典 Dictionary<key, class>  用每个表中规定的主键去快速获取任一个对象
    DataRow typeRow = GetVariableTypeRow(table);
    string keyType = GetKeyName(table);
    string className = table.TableName;

    //可以开始拼接了
    string str = "using System.Collections.Generic;\n\n";
    str += "public class " + className + "Container{\n";
    str += "\tpublic Dictionary<" + keyType + ", " + className + "> dataDic = ";
    str += "new Dictionary<" + keyType + ", " + className + ">();";

    str += "\n}";

    //检查存放的文件夹位置是否存在
    if (!Directory.Exists(BinaryDataMgr.CONTAINER_PATH)) {
        Directory.CreateDirectory(BinaryDataMgr.CONTAINER_PATH);
    }

    File.WriteAllText(BinaryDataMgr.CONTAINER_PATH + table.TableName + "Container.cs", str);
}
cs 复制代码
/// <summary>
/// 获取设定主键的行
/// </summary>
private static DataRow GetKeySetRow(DataTable table)
{
    return table.Rows[2];
}

/// <summary>
/// 获取当前表中的主建是第几列决定的
/// </summary>
private static int GetRowKeyIndex(DataTable table)
{
    int lenght = table.Columns.Count;
    DataRow keyRow = GetKeySetRow(table);
    for (int i = 0; i < lenght; i++) {
        if(keyRow[i].ToString() == "key") {
            return i; 
        }
    }

    //如果没有正确配置key的话,默认用第一列作为主键
    return 0;
}

/// <summary>
/// 获取建名
/// </summary>
private static string GetKeyName(DataTable table)
{
    DataRow nameRow = GetVariableNameRow(table);
    int index = GetRowKeyIndex(table);
    return nameRow[index].ToString();
}

①数据容器类的名字就是类名 + Container。

②它需要一个建名和一个类名来完成整个的拼接。因此我们需要去获取这个建名,方法是遍历建所在的行,找到标为key的值(没有找到就默认返回第一个元素)

③获取完了之后按顺序拼接即可,注意引用命名空间也要拼接进去

完成上述步骤之后,我们应该可以看到如下效果的:
配置表格 点击调用函数进行生成 生成的文件夹 生成的数据结构类 生成的数据结构类容器

好啦,至此我们就可以由Excel表格顺利生成我们需要的数据结构类


四.由Excel表格生成相应的数据二进制文件

cs 复制代码
/// <summary>
/// 把表格中的数据都以二进制的形式存储起来
/// </summary>
private static void GenerateBinaryData(DataTable table)
{
    //检查存储的路径是否存在
    if (!Directory.Exists(BinaryDataMgr.BINARY_PATH)) {
        Directory.CreateDirectory(BinaryDataMgr.BINARY_PATH);
    }

    //考虑读取的时候需要怎么处理,肯定是读取一整个到一个字节数组中。然后我们需要逐行对她进行读取
    //因此读取的时候我们需要一共有几条数据需要去读取
    //同时因为是要放置在容器中,容器是字典,所以我们还需要知道,读取的一条数据中的哪一个变量是作为建的
    using (FileStream fs = File.Open(BinaryDataMgr.BINARY_PATH + table.TableName + ".nai", FileMode.OpenOrCreate, FileAccess.Write)) {
        //获取类型行
        DataRow typeRow = GetVariableTypeRow(table);
        
        //存储行数
        int rowCount = table.Rows.Count - 4;
        fs.Write(BitConverter.GetBytes(rowCount) ,0, 4);    
        
        //存储主键名字
        string keyName = GetKeyName(table);
        Byte[] bytes = Encoding.UTF8.GetBytes(keyName);
        fs.Write(BitConverter.GetBytes(bytes.Length), 0, 4);
        fs.Write(bytes, 0, bytes.Length);

        DataRow dataRow;

        for (int i = BEGIN_INDEX; i < table.Rows.Count; i++) {
            dataRow = table.Rows[i];
            for (int j = 0; j < table.Columns.Count; j++) {
                    switch (typeRow[j].ToString()) {
                        case "int":
                            fs.Write(BitConverter.GetBytes(int.Parse(dataRow[j].ToString())), 0, 4);
                            break;

                        case "float":
                            fs.Write(BitConverter.GetBytes(float.Parse(dataRow[j].ToString())), 0, 4);
                            break;

                        case "bool":
                            fs.Write(BitConverter.GetBytes(bool.Parse(dataRow[j].ToString())), 0, 1);
                            break;

                        case "string":
                            string str = dataRow[j].ToString();
                            bytes = Encoding.UTF8.GetBytes(str);

                            //先把字符串的字节数组的长度写入
                            int lenght = bytes.Length;
                            fs.Write(BitConverter.GetBytes(lenght), 0, 4);

                            //然后把字符串写入
                            fs.Write(bytes, 0, lenght);

                            break;
                    }
            }
        }

        fs.Close();
    }

①在开始存储数据之前,我们要考虑如何读取。首先我们肯定是一行一行读取以装载每个类对象,接着我们还需要把他们装载到容器中,因此需要存储一共有多少行(注意这里存储的行数-4,因此前四行不是真正的数据)。那是如何装载的呢?我们肯定是需要知道它的建的名字,因此还需要存储它的建的名字。

②接着我们用两层循环遍历每一行的每一列。根据当前单元格的类型,选择相应的存储方式。

写完这个之后我们再点击生成按钮可以看到如下的:
生成的文件夹 生成的文件


五.读取数据

5.1.载入数据

好啦,生成与存储功能基本完毕了。接下来只要再读取出来即可。

我们肯定是要把数据都读取到内存中的,这样才方便我们后续的使用。那我们有这么多的数据,要怎么来有序地获取我们需要的数据呢?可以考虑用一个字典:<容器名,容器对象>这样的方式,来存储相应的容器。这样我们需要使用相应的数据的时候,先获取该数据所在的容器,再在容器中获取它所在的对象即可。

同时因为这些数据肯定是需要在游戏运行中使用的,因此我们不能把读取的逻辑也放在Editor文件夹中的

cs 复制代码
 //输入一个要获取的类的名字,然后获取她对应的数据容器
 public Dictionary<string, object> containerDic = new Dictionary<string, object>();

①这里因为我们不能知道每个容器类是谁,因此用object来装载

cs 复制代码
 /// <summary>
 /// 加载数据到容器中
 /// </summary>
 /// <typeparam name="T">容器</typeparam>
 /// <typeparam name="k">数据结构类</typeparam>
 /// <param name="fileName"></param>
 public void Load<T, K>() where T : class
 {
     //首先打开文件,把里面的字节都读到字节数组里
     //Byte[] bytes = File.ReadAllBytes(fileName);
     Byte[] bytes;
     using (FileStream fs = File.Open(BINARY_PATH + typeof(K).Name + ".nai", FileMode.Open, FileAccess.Read)) {
         bytes = new byte[fs.Length];
         fs.Read(bytes, 0, bytes.Length);

         fs.Close();
     }

     int index = 0;
     //首先读取有多少行
     int rowCount = BitConverter.ToInt32(bytes, index);
     index += 4;

     //然后读取主键的名字
     int strLenght = BitConverter.ToInt32(bytes, index);
     index += 4;
     string keyName = Encoding.UTF8.GetString(bytes, index, strLenght);
     index += strLenght;

     //创建容器对象
     Type containerType = typeof(T);
     object containerObj = Activator.CreateInstance(containerType);
     //获取类对象和它的字段信息
     Type classType = typeof(K);

     FieldInfo[] fieldInfos = classType.GetFields();


     //之后在下面我们正式开始读取数据
     for (int i = 0; i < rowCount; i++) {
         //每一行对应一个实例对象
         object dataObj = Activator.CreateInstance(classType);
         for (int j = 0; j < fieldInfos.Length; j++) {
             Type fieldType = fieldInfos[j].FieldType;
             if(fieldType == typeof(int)) {
                 fieldInfos[j].SetValue(dataObj, BitConverter.ToInt32(bytes, index));
                 index += 4;

             }
             else if(fieldType == typeof(float)) {
                 fieldInfos[j].SetValue(dataObj, BitConverter.ToSingle(bytes, index));
                 index += 4;
             }
             else if(fieldType == typeof(bool)) {
                 fieldInfos[j].SetValue(dataObj, BitConverter.ToBoolean(bytes, index));
                 index += 1;
             }
             else if(fieldType == typeof(string)) {
                 strLenght = BitConverter.ToInt32(bytes, index);
                 index += 4;
                 string str = Encoding.UTF8.GetString(bytes, index, strLenght);
                 fieldInfos[j].SetValue(dataObj, str);
                 index += strLenght;
             }                                                                                
         }

         //所有的字段都填入对应数据之后,把该对象加入到容器中
         //先获取容器类中的字典对象
         object dicObj = containerType.GetField("dataDic").GetValue(containerObj);
         //再获取字典对象中的ADD方法
         MethodInfo add = dicObj.GetType().GetMethod("Add");
         //使用ADD方法把当前对象加入到字典中
         object key = classType.GetField(keyName).GetValue(dataObj);
         add.Invoke(dicObj, new object[] { key, dataObj });
     }

     //到这里说明把数据都完整装载到容器中去了
     //接下来把容器装入相应的字典位置即可
     containerDic.Add(containerType.Name, containerObj);
 }

①对于加载数据,我们是要把对应的类对象加载到对应的数据容器中。因此需要两个泛型,这里T表示数据容器类,K表示数据结构类。

②读取的时候我们先直接把一整个文件中的二进制字节都读入到一个字节数组中

③之后我们根据根据每个变量的长度来读取对应的数据,要用一个index记录当前读取到哪里了

④首先需要把 行数 和 建名 读出来。之后再去每一行地去实例化一个对象,并根据该类中的字段数去一个一个装载对应的数据。

⑤当一个对象装载完毕之后,需要把他加入到容器中去。

  • 对于容器,我们可以通过反射来获取容器类中的字典变量,再用反射获取这个字典变量中的Add方法
  • 然后我们可以获取键名,然后通过键名去获取它在对象中对应的类型以及变量值。之后我们调用这个Add方法来把建值和类对象加入到字典中

⑥当一个容器装载完毕了之后,我们就可以把它存到容器字典中去了

那这个函数在哪里调用比较好呢?在它的构造函数中就好了

cs 复制代码
private BinaryDataMgr() {
    InitData();
}

public void InitData()
{
    Load<TowerInfoContainer, TowerInfo>();
    Load<PlayerInfoContainer, PlayerInfo>();
    Load<TestInfoContainer, TestInfo>();
}

5.2.外部使用数据

外部要读取数据,肯定是要获取相应的容器。因此它们可以传入一个容器类,然后我们通过反射获取它的名字,再把它在容器字典中获取并返回出去

cs 复制代码
public T GetTable<T>() where T : class
{
    string containerName = typeof(T).Name;
    if (!containerDic.ContainsKey(containerName)) {
        return null;
    }

    return containerDic[containerName] as T;
}  

外部使用实例:

cs 复制代码
using UnityEngine;

public class Test : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {     
        TowerInfoContainer container =  BinaryDataMgr.Instance.GetTable<TowerInfoContainer>();
        print(container.dataDic[4].name);

        TestInfoContainer testInfoContainer = BinaryDataMgr.Instance.GetTable<TestInfoContainer>();
        print(testInfoContainer.dataDic[1].name);
    }

①直接调用GetTable方法,并传入需要获取的容器对象。

②获取容器后,根据主建值来获取对应的数据对象


六.总结

①这里通过Excel的插件来读取Excel文件中的各个表格,以及表格中的单元格数据

②自动化生成数据结构类,容器类。其实就只是获取相应的字符,并用代码的方式写入文件中即可

③自动化生成二进制数据。则是先存储几条数据,主键名字。然后再一行一行遍历每个单元格存储到文件按中

④读取的时候先读取行数和主键名字。然后再根据当前类的字段数去逐个读取装载到对应的位置

⑤为了外部方便调用,我们又用一个容器字典。来把容器装进去,以方便外部快速获取对应的容器

⑥Editor文件夹中的文件不会被打包出去,也不能被外部的脚本使用。但是它可以使用外部的脚本

相关推荐
笨笨的摸索8 小时前
链表题类型注解解惑:理解Optional,理解ListNode
数据结构·经验分享·链表
心前阳光8 小时前
Unity通过Object学习原型模式
学习·unity·原型模式
你说今年的枫叶好像不够红啊8 小时前
LeetCode[两数之和] java版
数据结构·算法·leetcode
YC汐宇8 小时前
数据结构:顺序栈与链栈的原理、实现及应用
数据结构·算法·链表
AIGC安琪9 小时前
字节跳动把AI大模型入门知识点整理成手册了,高清PDF开放下载
人工智能·学习·ai·语言模型·大模型·llm·ai大模型
野犬寒鸦9 小时前
力扣hot100:螺旋矩阵(边界压缩,方向模拟)(54)
java·数据结构·算法·leetcode
lx7416026989 小时前
torch学习 自用
学习
脑洞代码9 小时前
20250903的学习笔记
服务器·笔记·学习
ai绘画-安安妮9 小时前
AI工程师必看!GitHub上10个高价值LLM开源项目,建议立即收藏
人工智能·学习·程序员·开源·大模型·github·转行