Unity develops Excel table reader

In actual game development, we may encounter the need to read Excel tables, and we can develop a set of plug-ins that use binary reading and writing. The overall idea is divided into two steps:

Step 1: Read the Excel file

1. Generate the information in the Excel table into a structure class. For this, we need to formulate a read and write rule for the Excel table. For example the table below. The first line is the field name, the second line is the field type, the third line is the primary key, and the fourth line is the explanation. From the fifth row onwards are the data that actually need to be persisted.

2. After formulating the rules of the Excel table, we start to use the code to generate the container (dictionary) of the data structure class and the storage structure class. First, declare a ToolExcel class file and three paths, as follows:

    /// <summary>
    /// excel文件存放的路径
    /// </summary>
    public static string EXCEL_PATH = Application.dataPath + "/ArtRes/Excel/";

    /// <summary>
    /// 数据结构类脚本存储位置路径
    /// </summary>
    public static string DATA_CLASS_PATH = Application.dataPath + "/Scripts/ExcelData/DataClass/";    /// <summary>
    /// 容器类脚本存储位置路径

    /// </summary>
    public static string DATA_CONTAINER_PATH = Application.dataPath + "/Scripts/ExcelData/Container/";

3. Write a method that can be called in the Unity editor. Features should be added before the method, so that the Unity editor can be called outside through the selection box. Then open the specified path of the table file in the method, get all the files in it, and use the for loop to judge whether it is an excel table file. If it is not a table file, go directly to the next cycle. If it is a table file, use the stream file stream to save all the tables in the file Get it, and then use foreach to traverse all the information in the table to perform three operations, the code is as follows.

[MenuItem("GameTool/GenerateExcel")]
    private static void GenerateExcelInfo()
    {
        //记在指定路径中的所有Excel文件 用于生成对应的3个文件
        DirectoryInfo dInfo = Directory.CreateDirectory(EXCEL_PATH);
        //得到指定路径中的所有文件信息 相当于就是得到所有的Excel表
        FileInfo[] files = dInfo.GetFiles();
        //数据表容器
        DataTableCollection tableConllection;
        for (int i = 0; i < files.Length; i++)
        {
            //如果不是excel文件就不要处理了
            if (files[i].Extension != ".xlsx" &&
                files[i].Extension != ".xls")
                continue;
            //打开一个Excel文件得到其中的所有表的数据
            using (FileStream fs = files[i].Open(FileMode.Open, FileAccess.Read))
            {
                IExcelDataReader excelReader = ExcelReaderFactory.CreateOpenXmlReader(fs);
                tableConllection = excelReader.AsDataSet().Tables;
                fs.Close();
            }

            //遍历文件中的所有表的信息
            foreach (DataTable table in tableConllection)
            {
                //生成数据结构类
                GenerateExcelDataClass(table);
                //生成容器类
                GenerateExcelContainer(table);
                //生成2进制数据
                GenerateExcelBinary(table);
            }

        }

Three methods in traversal:

1. Generate data structure class GenerateExcelDataClass method

But before we go to generate the data structure class, we need to get some information in the table and what the dynamically generated data structure class we need looks like. In our table file example, a set of rules is enacted. (The first four behaviors are variable name, type/primary key identification, and explanation) From this, we can roughly know that the specific class one should look like the following.

public class Sheet1
{
  public int id;
  public string name;
  public float atk;
  public float def;
}

Then in the GenerateExcelDataClass method, we need to know the class field name of the data and the data type so that we can splice strings . For this purpose, two methods, GetVariableNameRow and GetVariableTypeRow , can be written as follows:

    /// <summary>
    /// 获取变量名所在行
    /// </summary>
    /// <param name="table"></param>
    /// <returns></returns>
    private static DataRow GetVariableNameRow(DataTable table)
    {
        return table.Rows[0];
    }

    /// <summary>
    /// 获取变量类型所在行
    /// </summary>
    /// <param name="table"></param>
    /// <returns></returns>
    private static DataRow GetVariableTypeRow(DataTable table)
    {
        return table.Rows[1];
    }

With these two methods, you can call GenerateExcelDataClass to get the information of the table, the code is as follows. Its essence is actually to traverse two DataRow objects after obtaining them, and then use string splicing to generate classes.

    /// <summary>
    /// 生成Excel表对应的数据结构类
    /// </summary>
    /// <param name="table"></param>
    private static void GenerateExcelDataClass(DataTable table)
    {
        //字段名行
        DataRow rowName = GetVariableNameRow(table);
        //字段类型行
        DataRow rowType = GetVariableTypeRow(table);

        //判断路径是否存在 没有的话 就创建文件夹
        if (!Directory.Exists(DATA_CLASS_PATH))
            Directory.CreateDirectory(DATA_CLASS_PATH);
        //如果我们要生成对应的数据结构类脚本 其实就是通过代码进行字符串拼接 然后存进文件就行了
        string str = "public class " + table.TableName + "\n{\n";

        //变量进行字符串拼接
        for (int i = 0; i < table.Columns.Count; i++)
        {
            str += "    public " + rowType[i].ToString() + " " + rowName[i].ToString() + ";\n";
        }

        str += "}";

        //把拼接好的字符串存到指定文件中去
        File.WriteAllText(DATA_CLASS_PATH + table.TableName + ".cs", str);

        //刷新Project窗口
        AssetDatabase.Refresh();
    }

Also in addition to the data structure class, we also need a data container class

2. Generate data container class GenerateExcelContainer method

The essence of the data container class is actually a class used to store row-by-row data, of which there are only dictionary variables. The key of the dictionary is the key of the data in the table, and the value of the dictionary is the data structure class, which is used to store a row of data.

public class Sheet1Container
{
    public Dictionary<int,Sheet1>dataDic = new Dictionary<int, Sheet1>();
}

 To do this, we need a method GetKeyIndex . Since the table has established rules (the third row of the table marks the primary key), we need to know the index value of the primary key in order to determine which column of the third row is the index. code show as below:

        private static int GetKeyIndex(DataTable table) { 
        DataRow row = table.Rows[2];
        for (int i = 0; i < table.Columns.Count; i++)
        {
            if (row[i].ToString() == "key")
                return i;
        }
        return 0;
    }

After obtaining the column where the key is located, you can know which data is used as the key, and substitute the data that is used as the primary key into the function. The code is as follows:

    /// <summary>
    /// 生成Excel表对应的数据容器类
    /// </summary>
    /// <param name="table"></param>
    private static void GenerateExcelCotainer(DataTable table) {
        int keyIndex = GetKeyIndex(table);
        DataRow rowType = GetVariableTypeRow(table);
        if (!Directory.Exists(BinaryDataMgr.DATA_CONTAINER_PATH))
            Directory.CreateDirectory(BinaryDataMgr.DATA_CONTAINER_PATH);
        string str = "using System.Collections.Generic;\n";
        str += "public class " + table.TableName + "Container" + "\n{\n";
        str += "    ";
        str += "public Dictionary<" + rowType[keyIndex].ToString() + "," + table.TableName + ">";
        str += "dataDic = new " + "Dictionary<" + rowType[keyIndex].ToString() + ", " + table.TableName + ">();\n";
        str += "}";
        File.WriteAllText(BinaryDataMgr.DATA_CONTAINER_PATH+"/"+ table.TableName + "Container.cs",str);
        AssetDatabase.Refresh();
    }

After getting the two data classes, you can start generating binary files

3. Generate a binary file GenerateExcelBinary(DataTable tabel) The incoming parameter is a table

In fact, this method is to first judge whether there is a path for storing binary files, if not, create it, and then open up a binary file data stream. Write the data information of the incoming table in sequence.

1. First write how many rows of data need to be stored (since the rules formulated by our Excel table are the first four rows of table information, so the data starts from the fifth and fifth row)

2. Write the variable name of the primary key (there is a ready-made method to obtain the Index of the primary key). You only need to obtain the index data of the row of data of the variable name and convert it into a string

3. Traverse all the rows of content, and then write according to the type (writing is data written row by row. First traverse all the rows according to the number of rows, and then judge the type by column according to the length of the type row and write the file stream middle)

4. Finally close the stream

    /// <summary>
    /// 生成Excel二进制数据
    /// </summary>
    /// <param name="table"></param>
    private static void GenerateExcelBinary(DataTable table){
        if (Directory.Exists(BinaryDataMgr.DATA_CONTAINER_PATH))
            Directory.CreateDirectory(BinaryDataMgr.DATA_CONTAINER_PATH);
        //创建一个二进制文件写入
        using (FileStream fs = new FileStream(BinaryDataMgr.DATA_CONTAINER_PATH + table.TableName + ".mqx", FileMode.OpenOrCreate, FileAccess.Write
            )) {
            //存储具体的excel对应的二进制信息
            //1.先存储我们需要写入多少行的数据 方便读取
            fs.Write(BitConverter.GetBytes(table.Rows.Count - 4),0,4);
            //2.存储主键的变量名
            string keyName = GetVariableNameRow(table)[GetKeyIndex(table)].ToString();
            byte[] bytes = Encoding.UTF8.GetBytes(keyName);
            //存储字符串字节数组长度
            fs.Write(BitConverter.GetBytes(bytes.Length), 0, 4);
            //存储字符串字节数组
            fs.Write(bytes, 0, bytes.Length);
            //3.遍历所有内容的行 进行二进制的写入
            DataRow row;
            //得到类型行 根据规则决定如何写入书
            DataRow rowType = GetVariableTypeRow(table);
            for (int i = BINGEN_INDEX; i < table.Rows.Count; i++)
            {
                row = table.Rows[i];
                for (int j = 0; j < table.Columns.Count; j++)
                {
                    switch (rowType[j].ToString())
                    {
                        case "int":
                            fs.Write(BitConverter.GetBytes(int.Parse(row[j].ToString())),0,4);
                            break;
                        case "float":
                            fs.Write(BitConverter.GetBytes(int.Parse(row[j].ToString())), 0,4);
                            break;
                        case "sting":
                            fs.Write(BitConverter.GetBytes(int.Parse(row[j].ToString())), 0, 1);
                            break;
                        case "bool":
                            bytes = Encoding.UTF8.GetBytes(row[j].ToString());
                            //写入字符串字节数组长度
                            fs.Write(BitConverter.GetBytes(bytes.Length), 0, 4);
                            //写入字符串字节数组
                            fs.Write(bytes, 0, bytes.Length);
                            break;
                    }
                }
            }
            fs.Close();
        }
        AssetDatabase.Refresh();
    }

From the above, we have completed the reading and writing of binary files for Excel tables.

But after we save the Excel table as a secondary system, we need to read it out and store it in memory. For this reason, we also need a class BinaryDataManager to read binary files. Since it is a Manager manager class, it can be written as a singleton mode. For this we can first write a singleton pattern. code show as below:

A singleton interface, a singleton abstraction for initialization. Then there are three fields needed in the class, one of which is DATA_BINARY_PATH (the storage path of binary data) will be used by ToolExcel. There is also a dictionary container for storing fetched from disk to memory.

public interface ISingleton {
    void Init();
}

public abstract class Singleton<T> where T : ISingleton, new() {
    private static T instance;
    public static T Instance {
        get {
            if (instance==null)
            {
                instance = new Lazy<T>(true).Value;
                instance.Init();
            }
            return instance;
        }
    }
}

/// <summary>
/// 2进制数据管理器
/// </summary>
public class BinaryDataMgr:ISingleton
{
/// <summary>
    /// 2进制数据存储位置路径
    /// </summary>
    public static string DATA_BINARY_PATH = Application.streamingAssetsPath + "/Binary/";

    /// <summary>
    /// 用于存储所有Excel表数据的容器
    /// </summary>
    private Dictionary<string, object> tableDic = new Dictionary<string, object>();

    /// <summary>
    /// 数据存储的位置
    /// </summary>
    private static string SAVE_PATH = Application.persistentDataPath + "/Data/";
    public static BinaryDataMgr Instace => Singleton<BinaryDataMgr>.Instance;
}

There is also a method LoadTable<T,K>. The two generic parameters are the container class name and the data structure class name respectively. The method code is as follows:

We can divide this method into several parts

1. Open the stream to read the file, store the bytecode in the stream and then close the stream

2. Read the characteristic information stored in the stream, such as how many lines should be read and the name of the primary key

3. Reflection creates container class objects

4. Get the Type of the data structure class and reflect to get all the information in it

5. Read each line of information, instantiate a data structure object through reflection, then traverse the data structure information object (infos) to determine the data type in it, and read the corresponding type from the byte array

Since the order in which binary files are written is written in order according to the type of the data structure class, there will be no problem in reading line by line in order.

6. Get the dictionary object in the container object, then use the Add method of the dictionary to reflect the bar, then get the value of the key, use the reflection call method, and finally record the read table.

7. Close the stream

    public void LoadTable<T,K>()
    {
        //读取 excel表对应的2进制文件 来进行解析
        using (FileStream fs = File.Open(DATA_BINARY_PATH + typeof(K).Name + ".tang", FileMode.Open, FileAccess.Read))
        {
            byte[] bytes = new byte[fs.Length];
            fs.Read(bytes, 0, bytes.Length);
            fs.Close();
            //用于记录当前读取了多少字节了
            int index = 0;

            //读取多少行数据
            int count = BitConverter.ToInt32(bytes, index);
            index += 4;

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

            //创建容器类对象
            Type contaninerType = typeof(T);
            object contaninerObj = Activator.CreateInstance(contaninerType);
            //得到数据结构类的Type
            Type classType = typeof(K);
            //通过反射 得到数据结构类 所有字段的信息
            FieldInfo[] infos = classType.GetFields();

            //读取每一行的信息
            for (int i = 0; i < count; i++)
            {
                //实例化一个数据结构类 对象
                object dataObj = Activator.CreateInstance(classType);
                foreach (FieldInfo info in infos)
                {
                    if( info.FieldType == typeof(int) )
                    {
                        //相当于就是把2进制数据转为int 然后赋值给了对应的字段
                        info.SetValue(dataObj, BitConverter.ToInt32(bytes, index));
                        index += 4;
                    }
                    else if (info.FieldType == typeof(float))
                    {
                        info.SetValue(dataObj, BitConverter.ToSingle(bytes, index));
                        index += 4;
                    }
                    else if (info.FieldType == typeof(bool))
                    {
                        info.SetValue(dataObj, BitConverter.ToBoolean(bytes, index));
                        index += 1;
                    }
                    else if (info.FieldType == typeof(string))
                    {
                        //读取字符串字节数组的长度
                        int length = BitConverter.ToInt32(bytes, index);
                        index += 4;
                        info.SetValue(dataObj, Encoding.UTF8.GetString(bytes, index, length));
                        index += length;
                    }
                }

                //读取完一行的数据了 应该把这个数据添加到容器对象中
                //得到容器对象中的 字典对象
                object dicObject = contaninerType.GetField("dataDic").GetValue(contaninerObj);
                //通过字典对象得到其中的 Add方法
                MethodInfo mInfo = dicObject.GetType().GetMethod("Add");
                //得到数据结构类对象中 指定主键字段的值
                object keyValue = classType.GetField(keyName).GetValue(dataObj);
                mInfo.Invoke(dicObject, new object[] { keyValue, dataObj });
            }

            //把读取完的表记录下来
            tableDic.Add(typeof(T).Name, contaninerObj);
            fs.Close();
        }
    }

Declare a function that calls this method externally, pass the generated data class structure and the type name of the data container as a generic method to the above function, and then the binary data stored in the hard disk can be obtained in memory.

Completed the implementation of the editor.

Guess you like

Origin blog.csdn.net/qq_52690206/article/details/127843732