Unity 配置表读取-数据存储-基于NPOI读取Excel文件转cs文件-xlsx文件读取

前言

  1. 在游戏公司中,为了方便程序与策划进行游戏数据的交互,一般会使用Excel文件,程序需要写一套读取xlsx文件的程序,能够将xlsx文件导出json,xml,cs文件等存储下来,方便读取
  2. 本次介绍,怎么将xlsx文件读取,并导出为cs文件
  3. 导出cs文件的好处,游戏一运行就自动加载进去,通过字典读取方便,省去了加载卸载数据和数据格式解析的麻烦
    缺点:配置数据一直在内存中,消耗内存,cs文件一般中大型游戏在(5-20M);
    为什么还要选择cs文件呢?
    配置数据在游戏中起着至关重要的作用,几乎所有功能(图片,模型,声音等资源的加载,多语言实现,游戏各种引导,对话,任务,成就)都依赖配置数据,而对于一个中大型游戏,几个精细的模型贴图可能要大于配置数据占用的内存.所以它具有很大的价值,大大方便读取
  4. 使用NPOI插件读取xlsx文件

编辑器扩展

以下代码都放入一个ExportExcel脚本下
下面的文件全部放在一个文件中,且在Editor文件夹下

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();删除加载条
        }
    }

支持快速导出和全部导出2个模式,在编辑器菜单栏,“Tools/导出/导出全部配置表”

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

只导出修改的xlsx文件

一般将Excel文件放在和Assets文件夹同级路径下,防止unity重新加载资源
ExportFile方法,利用NPOI的api读取xlsx文件的每个单元格,存储到sheetDict字典
怎么只导出修改的xlsx文件?

  1. 当xlsx文件总大小大于2-5M时,导出时间可能在1-2分钟,非常浪费时间
    只导出修改后的xlsx时间大概在2-5秒钟
  2. 将每个文件名和文件的hash值保存在一个txt文件中,比较文件的hash值,hash值改变的,就重新导出
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写入文件
    }

存储计算hash文件

下面为计算存储hash相关的方法

hash相关

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相关

导出数据

记录xlsx文件到二维数组

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,合并单元格

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;
}

将记录的数据写入cs文件

ParseExcelToCS,将数据导出为cs文件,写入文件,使用模板替换的方式,预先写好一个.txt的文件
下面是Template.txt文件,Assets/Editor/Excel/Template.txt
将数据写入字典,

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 data = new Dictionary()
    {
_DictContent
    };
}

ParseExcelToCS

写xlsx文件的规则 上面连续 1:备注,2:字段名,3:字段类型,第1,2,3行可以是备注,后面接字段
根据自己的习惯修改
Unity 配置表读取-数据存储-基于NPOI读取Excel文件转cs文件-xlsx文件读取_第1张图片

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);//将过滤有效的列加入一个数组
        //下面为该下面部分要输出的结果
        /// 
        /// id
        /// 
        //public int id;
        dataClassFields.AppendFormat("\t/// \r\n\t/// {0}\r\n\t/// \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 data = new Dictionary()
        //{
        //{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 将excel单元格转换为可以实例化的格式,如(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();
}

怎么使用
Data_Test.data,Data_Test.data[id]访问

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

结束语

Excel文件导出cs文件就讲解到这里,如果上面有代码不理解或者有问题,可以在评论区留言

你可能感兴趣的:(unity,excel,游戏程序,游戏引擎)