Unity configuration table reading - data storage - reading Excel files to cs files based on NPOI - reading xlsx files

Preface

  1. In game companies, in order to facilitate the interaction of game data between programs and planners, Excel files are generally used. The program needs to write a set of programs to read xlsx files. It can export xlsx files to json, xml, cs files, etc. and store them for convenience. read
  2. This article introduces how to read xlsx files and export them to cs files
  3. The advantage of exporting cs files is that the game is automatically loaded as soon as it is run. It is convenient to read through the dictionary, eliminating the trouble of loading and unloading data and data format parsing
    Disadvantages: configuration data It is always in the memory and consumes memory. CS files are generally in medium and large games (5-20M);
    Why should we choose cs files?
    The configuration data is in It plays a vital role in the game. Almost all functions (loading of pictures, models, sounds and other resources, multi-language implementation, various game guidance, dialogues, tasks, achievements) rely on configuration data, and for a medium and large game , several detailed model textures may be larger than the memory occupied by the configuration data. So it has great value and is greatly easier to read
  4. Read xlsx files using NPOI plugin

Editor extension

The following codes are all placed under an ExportExcel script
The following files are all placed in one file and under the Editor folder

private static string ExcelPath = Application.dataPath.Replace("Assets", "Excel");
private static string IgnoreChar = "#";
private static string exportDirPath = Application.dataPath + "/Scripts/Data/";
private static string templateFile = Application.dataPath + "/Editor/Excel/Template.txt";
[MenuItem("Tools/导出/快速导出配置表")]
    public static void Export()
    {
    
    
        EditorUtility.DisplayProgressBar("根据Excel导出cs文件", "导出中...", 0f);//创建加载条
        try
        {
    
    
            //返回ExcelPath路径下所以的子文件的路径
            string[] tableFullPaths = Directory.GetFiles(ExcelPath, "*", SearchOption.TopDirectoryOnly);
            foreach (string path in tableFullPaths)
            {
    
    
                Debug.Log($"导出中...{
      
      path}");
                StartExport(path);//遍历所有的excel文件
            }
            AssetDatabase.Refresh();//unity重新从硬盘读取更改的文件
            Debug.Log("导出完成");
        }
        catch
        {
    
    
            Debug.LogError("导出失败");
        }
        finally
        {
    
    
            EditorUtility.ClearProgressBar();删除加载条
        }
    }

Supports two modes: quick export and all export. In the editor menu bar, "Tools/Export/Export all configuration tables"

   public static bool isFastExport = true;
    [MenuItem("Tools/导出/导出全部配置表")]
    private static void FastExport()
    {
    
    
        isFastExport = false;
        Export();
    }

Export only modified xlsx files

Generally place the Excel file in the same path as the Assets folder to prevent unity from reloading resources
The ExportFile method uses the NPOI API to read each cell of the xlsx file ,store to sheetDict dictionary
How to export only the modified xlsx file?

  1. When the total size of the xlsx file is larger than 2-5M, the export time may be 1-2 minutes, which is a waste of time
    Only exporting the modified xlsx will take about 2-5 seconds clock
  2. Save each file name and the hash value of the file in a txt file, compare the hash value of the file, and re-export if the hash value changes.
public static Dictionary<string, string> hashDict = new Dictionary<string, string>();
public static StringBuilder allHash = new StringBuilder();
public static void StartExport(string[] tableFullPaths)
    {
    
    
        string hashFile = ExcelPath + "/AllFileHash.txt";//hash值文件,和xlsx文件在同一个目录下
        bool isExitHashFile = File.Exists(hashFile);//是否是第一次导出
        if (!isExitHashFile || !isFastExport)//不存在hash文件或不是快速导出,导出全部的xlsx文件
        {
    
    
            foreach (string tableFileFullPath in tableFullPaths)
            {
    
    
                if (tableFileFullPath.EndsWith(".xls") || tableFileFullPath.EndsWith(".xlsx"))
                {
    
    
                    Debug.LogFormat("开始导出配置:   {0}", Path.GetFileName(tableFileFullPath));
                    string hash = CalculateFileHash(tableFileFullPath);//计算文件的hash值
                    int startIndex = tableFileFullPath.IndexOf("\\");//E:/unitycode/项目名称/Excel\Test.xlsx
                    string tempFile = tableFileFullPath.Substring(startIndex + 1);//Test.xlsx
                    allHash.Append($"{
      
      tempFile}:{
      
      hash}\n");//Test.xlsx:5F6342BC7B0387...
                    StartPieceExport(tableFileFullPath);//导出所有的xlsx文件
                }
            }
        }
        else
        {
    
    
            ReadFileToDict(hashFile);//读取hash文件进一个字典,文件名->hash值
            foreach (string tableFileFullPath in tableFullPaths)
            {
    
    
                if (tableFileFullPath.EndsWith(".xls") || tableFileFullPath.EndsWith(".xlsx"))
                {
    
    
                    string hash = CalculateFileHash(tableFileFullPath);//计算哈市值
                    int startIndex = tableFileFullPath.IndexOf("\\");//E:/unitycode/项目名称/Excel\Test.xlsx
                    string tempFile = tableFileFullPath.Substring(startIndex + 1);//Test.xlsx
                    allHash.Append($"{
      
      tempFile}:{
      
      hash}\n");

                    if (hashDict.ContainsKey(tempFile))//字典存在文件名
                    {
    
    
                        if (hashDict[tempFile] != hash)//字典存在文件名,hash值不相等
                        {
    
    
                            StartPieceExport(tableFileFullPath);//文件更改了
                            Debug.Log("导出了" + tableFileFullPath);
                        }
                        else
                        {
    
    
                            //存在,文件没有改变
                        }
                    }
                    else//字典不存在文件名,文件为新增文件
                    {
    
    
                        StartPieceExport(tableFileFullPath);
                        //文件增加了
                    }//xlsx文件被删除的情况没有处理,需要手动删除对应的cs文件
                }
            }

        }
        SavehashFile(hashFile);//将allHash写入文件
    }

Store calculated hash file

The following are methods related to calculating and storing hash

hash related

public static void ReadFileToDict(string hashFile)
    {
    
    
        string output = File.ReadAllText(hashFile);//读取指定路径文件到一个字符串
        hashDict = ParseStringToDictionary(output);//将字符串解析为一个字典
    }
    public static Dictionary<string, string> ParseStringToDictionary(string input)
    {
    
    
//对话系统.xlsx:5F6342BC7B03878CFB...语言系统.xlsx:530D69327A0FA9A7A6...
        Dictionary<string, string> dictionary = new Dictionary<string, string>();
        //分割字符串,每一行是一个字符串
        string[] lines = input.Split(new[] {
    
     "\n", "\r\n" }, StringSplitOptions.RemoveEmptyEntries);
//对话系统.xlsx:5F6342BC7B03878CFB...
//语言系统.xlsx:530D69327A0FA9A7A6...
        foreach (string line in lines)
        {
    
    //对话系统.xlsx:5F6342BC7B03878CFB...
            string[] parts = line.Split(':');//使用:分割字符串
            if (parts.Length == 2)
            {
    
    
                string key = parts[0].Trim();//对话系统.xlsx
                string value = parts[1].Trim();//5F6342BC7B03878CFB...
                dictionary[key] = value;
            }
        }
        return dictionary;
    }
public static string CalculateFileHash(string filePath)//计算一个文件的的hash值
    {
    
    
        var hash = SHA256.Create();
        var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read);//打开文件流
        byte[] hashByte = hash.ComputeHash(stream);//计算文件流的hash值
        stream.Close();//关闭文件流
        return BitConverter.ToString(hashByte).Replace("-", "");//将字节数组转化为字符串,替换-
    }

public static void SavehashFile(string hashFile)//将一个字符串存储在文件中
 {
    
    
     using (StreamWriter sw = new StreamWriter(File.Create(hashFile)))
         sw.Write(allHash);
     Debug.Log("hashFile:" + hashFile);
 }

hash related

export data

Record xlsx file to two-dimensional array

private static void StartPieceExport(string path)
{
    
    
    //每一个sheet表->二维单元格
    Dictionary<string, List<List<string>>> sheetDict = new Dictionary<string, List<List<string>>>();
    FileStream stream = new FileStream(path, FileMode.Open, FileAccess.Read);//创建文件流读写每一个xlsx文件
    IWorkbook book = null;
    if (path.EndsWith(".xls"))//如果是xls文件,传入二进制文件流
        book = new HSSFWorkbook(stream);
    else if (path.EndsWith(".xlsx"))
        book = new XSSFWorkbook(path);//传入文件路径
    else
        return;
    int sheetNum = book.NumberOfSheets;//获取一个excel文件表的数量
    stream.Close();//关闭文件流
    //处理每一个sheet
    for (int sheetIndex = 0; sheetIndex < sheetNum; sheetIndex++)
    {
    
    
        string sheetName = book.GetSheetName(sheetIndex);//获取每个sheet文件的名字
        //名字以#为前缀,要忽略的sheet,以sheet为前缀,excel默认的名字,忽略
        if (sheetName.StartsWith(IgnoreChar) || sheetName.StartsWith("Sheet")) {
    
     continue; }
        ISheet sheet = book.GetSheetAt(sheetIndex);//得到每个sheet对象
        sheet.ForceFormulaRecalculation = true; //强制公式计算,执行excel自身支持的函数,如AVERAGE(D2:K5)
        int MaxColumn = sheet.GetRow(0).LastCellNum;//得到第一行的列数
        List<List<string>> FilterCellInfoList = new List<List<string>>();
        //处理每一行
        for (int rowId = 0; rowId <= sheet.LastRowNum; rowId++)
        {
    
    
            IRow sheetRowInfo = sheet.GetRow(rowId);
            if (sheetRowInfo == null) Debug.LogError("无法获取sheetRowInfo数据");
            var firstCell = sheetRowInfo.GetCell(0);//得到第一列
            if (firstCell == null||firstCell.ToString().Contains(IgnoreChar)) {
    
     continue; }//该行的第一列无效,跳过该行
            if (firstCell.CellType == CellType.Blank || firstCell.CellType == CellType.Unknown || firstCell.CellType == CellType.Error) {
    
     continue; }
            List<string> rowList = new List<string>();//存储每一行的单元格
            //处理每一个单元格
            for (int columIndex = 0; columIndex < MaxColumn; columIndex++)
            {
    
    
                ICell cell = sheetRowInfo.GetCell(columIndex);//得到第rowId行的第columIndex个单元格
                if (cell != null && cell.IsMergedCell)
                {
    
    //单元格不为空并且为可以合并的单元格
                    cell = GetMergeCell(sheet, cell.RowIndex, cell.ColumnIndex);
                }
                else if (cell == null)
                {
    
    // 有时候合并的格子索引为空,就直接通过索引去找合并的格子
                    cell = GetMergeCell(sheet, rowId, columIndex);
                }
                //计算结果,支持逻辑表达式
                if (cell != null && cell.CellType == CellType.Formula)
                {
    
    
                    cell.SetCellType(CellType.String);
                    rowList.Add(cell.StringCellValue.ToString());//将单元格加入这一行
                }
                else if (cell != null)
                {
    
    
                    rowList.Add(cell.ToString());
                }
                else
                {
    
    
                    rowList.Add("");
                }
            }
            FilterCellInfoList.Add(rowList);//将行数据加入表中
        }
        sheetDict.Add(sheetName, FilterCellInfoList);//将sheet名字和对应的表数据,加入字典
    }
    foreach (var item in sheetDict)
    {
    
    
        string fileName = item.Key;
        string dirPath = exportDirPath;
        ParseExcelToCS(item.Value, item.Key, fileName, dirPath);//将过滤记录好的表数据->cs文件
    }//item.Value=>sheetName(类名),item.Key=>表格描述
}

GetMergeCell, merge cells

private static ICell GetMergeCell(ISheet sheet, int rowIndex, int colIndex)
{
    
    
    //获取合并单元个的总数
    for (int i = 0; i < sheet.NumMergedRegions; i++)
    {
    
    
        //获取第一个合并单元格
        var cellrange = sheet.GetMergedRegion(i);
        //如果在单元格范围
        if (colIndex >= cellrange.FirstColumn && colIndex <= cellrange.LastColumn
            && rowIndex >= cellrange.FirstRow && rowIndex <= cellrange.LastRow)
        {
    
    
            //返回第一个单元格
            var row = sheet.GetRow(cellrange.FirstRow);
            var mergeCell = row.GetCell(cellrange.FirstColumn);
            return mergeCell;
        }
    }
    return null;
}

Write recorded data to cs file

ParseExcelToCS, export the data to a cs file, write it to the file, use template replacement, and pre-write a .txt file
The following is the Template.txt file, Assets/ Editor/Excel/Template.txt
Write data into the dictionary,

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
public class _DataClassName
{
_DataClassFields
    public _DataClassName(_DataClassParameter)
    {
_DataClassFun
    }
}
public static class _Data_XX
{
    public static Dictionary<string, _DataClassName> data = new Dictionary<string, _DataClassName>()
    {
_DictContent
    };
}

ParseExcelToCS

The rules for writing xlsx files are as follows: 1: remarks, 2: field name, 3: field type. Lines 1, 2, and 3 can be remarks, followed by fields.
Modify according to your own habits
Insert image description here

static string _DataClassName = "_DataClassName";
static string _DataClassParameter = "_DataClassParameter";
static string _DataClassFun = "_DataClassFun";
static string _Data_XXDict = "_Data_XX";
static string _DictContent = "_DictContent";
static string _Type = "_Type";
static string _DataClassFields = "_DataClassFields";
private static void ParseExcelToCS(List<List<string>> cellInfo, string tableName, string ExportFileName, string ExportPath)
{
    
    
    //读取模板文件
    TextAsset template = AssetDatabase.LoadAssetAtPath<TextAsset>("Assets/Editor/Excel/Template.txt");
    StringBuilder templateStr = new StringBuilder(template.text);
    string saveFullPath = exportDirPath + "Data_" + tableName + ".cs";//导出文件的路径
    if (File.Exists(saveFullPath))//存在文件,删除
        File.Delete(saveFullPath);
    List<string> oneRowList = cellInfo[0];//中文备注
    List<string> twoRowList = cellInfo[1];//字段名
    List<string> threeRowList = cellInfo[2];//字段类型
    string DataClassName = "Data_" + tableName + "Class";
    templateStr.Replace(_Data_XXDict, "Data_" + tableName);
    templateStr.Replace(_DataClassName, DataClassName);//数据的类
    StringBuilder dataClassFields = new StringBuilder();
    StringBuilder dataClassParameter = new StringBuilder();
    StringBuilder dataClassFun = new StringBuilder();
    List<int> vaildColumIndex = new List<int>();
    for (int i = 0; i < oneRowList.Count; i++)//循环第一行
    {
    
    
        if (oneRowList[i].StartsWith(IgnoreChar)) continue;
        if (twoRowList[i].StartsWith(IgnoreChar)) continue;
        vaildColumIndex.Add(i);//将过滤有效的列加入一个数组
        //下面为该下面部分要输出的结果
        /// <summary>
        /// id
        /// </summary>
        //public int id;
        dataClassFields.AppendFormat("\t/// <summary>\r\n\t/// {0}\r\n\t/// </summary>\r\n", oneRowList[i]);//写入备注
        dataClassFields.AppendFormat("\tpublic {0} {1};\r\n", threeRowList[i], twoRowList[i]);//public 类型 字段名
        //public Data_TestClass(int id,string name)
        //{
    
    
        //    this.id = id;
        //    this.name = name;
        //}
        dataClassParameter.AppendFormat("{0} {1}", threeRowList[i], twoRowList[i]);//构造函数
        if (i < oneRowList.Count - 1)
            dataClassParameter.Append(",");
        dataClassFun.AppendFormat("\t\tthis.{0} = {1};", twoRowList[i], twoRowList[i]);
        if (i < oneRowList.Count - 1)
            dataClassFun.Append("\r\n");
    }
    templateStr.Replace(_DataClassFields, dataClassFields.ToString());
    templateStr.Replace(_DataClassParameter, dataClassParameter.ToString());
    templateStr.Replace(_DataClassFun, dataClassFun.ToString());
    StringBuilder rowData = new StringBuilder();
    string _type = null;
    for (int i = 3; i < cellInfo.Count; i++)//从第3行开始写入数据
    {
    
    
        List<string> RowInfo = cellInfo[i];
        string id = null;
        if (threeRowList[0] == "string")//类型是字符串还是int
        {
    
    
            id = "\"" + RowInfo[0] + "\"";
        }
        else
        {
    
    
            id = RowInfo[0];
        }
        StringBuilder inner = new StringBuilder();
        for (int j = 0; j < vaildColumIndex.Count; j++)//遍历每一行每一列
        {
    
    
            int ColumIndex = vaildColumIndex[j];//提取有效的索引,如ColumIndex不是123456,是1345
            string cell = RowInfo[ColumIndex];
            string FieldName = twoRowList[ColumIndex];
            if (ColumIndex == 0)
            {
    
    
                _type = threeRowList[ColumIndex];
            }
            string FieldType = threeRowList[ColumIndex];
            cell = AllToString(cell, FieldType);
            inner.Append(cell);
            if (j < vaildColumIndex.Count - 1) inner.Append(",");
        }
        rowData.Append("\t\t{");
        rowData.AppendFormat("{0} ,new {1}({2})", id, DataClassName, inner);
        rowData.Append("},");
        //public static Dictionary<int, Data_TestClass> data = new Dictionary<int, Data_TestClass>()
        //{
    
    
        //{1 ,new Data_TestClass(1,"name")},
        //}
        if (i < cellInfo.Count - 1) rowData.Append("\r\n");//最后一行不用换行
    }
    templateStr.Replace(_DictContent, rowData.ToString());
    templateStr.Replace(_Type, _type);
    using (StreamWriter sw = new StreamWriter(File.Create(saveFullPath)))
    {
    
    
        sw.Write(templateStr.ToString());//写入最后数据到cs文件
        sw.Flush();
        sw.Close();
    }
}

AllToString converts excel cells into a format that can be instantiated, such as (1,2,3)=>new Vector3(1,2,3)

private static string AllToString(string cell, string type)
{
    
    
    StringBuilder result = new StringBuilder();
    switch (type)
    {
    
    
        case "int":
            if (cell.Length <= 0) return "0";
            result.Append(cell);
            break;
        case "int[]":
            if (cell.Length <= 0) return "new int[] { }";
            result.Append("new int[] {");
            result.Append(cell);
            result.Append("}");
            break;
        case "int[][]":
                if (cell.Length <= 0) return "new int[][] { }";
                result.Append("new int[][] {");
                string[] s = cell.Split(',');
                for (int i = 0; i < s.Length; i++)
                {
    
    
                    result.Append("new int[]" + s[i]);
                    if (i < s.Length - 1)
                    {
    
    
                        result.Append(",");
                    }
                }
                //result.Append(cell);
                result.Append("}");
                break;
        case "string":
            if (cell.Length <= 0) return "null";
            result.Append("\"");
            result.Append(cell);
            result.Append("\"");
            break;
        case "string[]"://支持"111","222"111;222
            if (cell.Length <= 0) return "null";
                result.Append("new string[] {");
                if (cell.IndexOf(";") < 0 && cell.IndexOf("\"") < 0)
                {
    
    
                    result.Append("\"");
                    result.Append(cell);//"aaa"=>new string[]{"aaa"}
                    result.Append("\"");
                }
                else
                {
    
    
                    if (cell.IndexOf(";") > 0)
                    {
    
    
                        string[] str = cell.Split(';');
                        for (int i = 0; i < str.Length; i++)
                        {
    
    
                            result.Append("\"");
                            result.Append(str[i]);//"aaa;bbb"=>new string[] {"aaa","bbb"}
                            result.Append("\"");
                            if (i < str.Length)
                            {
    
    
                                result.Append(",");
                            }
                        }
                    }
                    else
                    {
    
    
                        result.Append(cell);//"aaa","bbb"=>new string[] {"aaa","bbb"}
                    }
                }
                result.Append("}");
                break;
            break;
        case "float":
            if (cell.Length <= 0) return "0f";
            result.AppendFormat("{0}f", cell);
            break;
        case "double":
            if (cell.Length <= 0) return "0d";
            result.AppendFormat("{0}d", cell);
            break;
        case "bool":
            if (cell.Length <= 0) return "false";
            result.Append(cell.ToLower());
            break;
        case "Vector3":
            if (cell.Length <= 0) return "null";
            result.AppendFormat("new Vector3{0}", cell);
            break;
        case "Vector3[]":
            StringBuilder sb = new StringBuilder();
            if (cell.Length <= 3) return "null";
            string[] strings = cell.Split(new char[] {
    
     '(', ')' }, System.StringSplitOptions.RemoveEmptyEntries);
            for (int i = 0; i < strings.Length; i++)
            {
    
    
                if (strings[i].Length <= 1) continue;
                sb.Append("new Vector3");
                sb.Append("(");
                sb.AppendFormat("{0}", strings[i]);
                sb.Append(")");
                if (i < strings.Length - 1)
                    sb.Append(",");
            }
            result.Append("new Vector3[]");
            result.Append("{");
            result.AppendFormat("{0}", sb);
            result.Append("}");
            break;
    }
    return result.ToString();
}

how to use
Data_Test.data,Data_Test.data[id] access

//下面是导出关键类
public static class Data_Test
{
public static Dictionary<int, Data_TestClass> data = new Dictionary<int, Data_TestClass>()
{
{ 1, new Data_TestClass(1, “name”) }
}
}

Conclusion

This ends the explanation of exporting cs files from Excel files. If you don’t understand the code above or have questions, you can leave a message in the comment area.

Guess you like

Origin blog.csdn.net/qq_58047420/article/details/133873201