一、简介
在上篇博客《【游戏开发】Excel表格批量转换成CSV的小工具》 中,我们介绍了如何将策划提供的Excel表格转换为轻便的CSV文件供开发人员使用。实际在Unity开发中,很多游戏都是使用Lua语言进行开发的。如果要用Lua直接读取CSV文件的话,又要写个对应的CSV解析类,不方便的同时还会影响一些加载速度,牺牲游戏性能。因此我们可以直接将Excel表格转换为lua文件,这样就可以高效、方便地在Lua中使用策划配置的数据了。在本篇博客中,马三将会和大家一起,用C#语言实现一个Excel表格转lua的转表工具——Xls2Lua,并搭配一个通用的ConfigMgr来读取lua配置文件。
二、开发环境准备
由于要使用C#来读取Excel表格文件,所以我们需要使用一些第三方库。针对C#语言,比较好用的Excel库有NPOI和CSharpJExcel 这两个,其实无论哪个库都是可以用的,我们只是用它来读取Excel表格中的数据罢了。马三在本篇博客中使用的是CSharpJExcel库,因为它相对来说更轻便一些。下面附上NPOI和CSharpJExcel库的下载链接:
- CSharpJExcel库下载地址:https://sourceforge.net/projects/jexcelapi/files/CSharpJExcel/
- NPOI库下载地址:https://archive.codeplex.com/?p=npoi
三、转表工具
1.思路分析
一切准备就绪,可以开始我们的开发任务了。首先我们来大致地说一下转表工具的思路:
- 读取Excel表格文件的数据,依次读取配置目录下的Excel文件,然后逐个读取表里面Sheet的内容;
- 根据Excel表格中配置的字段类型,对数据进行校验,判断数据是否合法;
- 将通过校验的数据转为lua文件,一个Sheet切页对应一个lua配置文件;
- 使用通用的ConfigMgr对转出来的lua配置文件进行读取操作;
2.目录结构
项目整体的目录结构如下图所示:
图1:转表工具整体目录结构
ConfigMgr存放我们的ConfigMgr.lua,它是一个工具类,用来读取并管理转出来的Lua配置文件,兼具缓存数据的功能。Excel目录存放我们需要进行转换的Excel表格文件。LuaData目录存放转出来的Lua配置文件。Xls2Lua目录也就是我们的转表工具的目录了,它包含源代码和可直接运行的转表工具。
转表工具的设计结构如下图所示:
图2:转表工具设计结构
FileExporter类专门用来读取Excel文件和导出lua配置文件;GlobalDef类中定义了一些通用的数据结构和枚举等信息;XlsTransfer类即为我们的转表工具核心类,大部分数据都是在这里进行校验处理的。
下面我们就可以按照之前分析出来的思路编写具体的代码了,首先放上来的是我们主程序的入口,我们有一个名为config.ini的配置文件,程序运行的时候会先去这个配置信息中读取Excel的目录和输出目录,然后调用FileExporter.ExportAllLuaFile函数进行转表操作。
1 using System; 2 using System.Collections.Generic; 3 using System.IO; 4 using System.Linq; 5 using System.Text; 6 using System.Threading.Tasks; 7 8 namespace Xls2Lua 9 { 10 class Program 11 { 12 private static string inDir; 13 private static string outDir; 14 private static readonly string configPath = "./config.ini"; 15 16 static void Main(string[] args) 17 { 18 ReadConfig(); 19 FileExporter.ExportAllLuaFile(inDir, outDir); 20 } 21 22 private static void ReadConfig() 23 { 24 StreamReader reader = new StreamReader(configPath, Encoding.UTF8); 25 inDir = reader.ReadLine().Split(',')[1]; 26 inDir = Path.GetFullPath(inDir); 27 outDir = reader.ReadLine().Split(',')[1]; 28 outDir = Path.GetFullPath(outDir); 29 reader.Close(); 30 } 31 } 32 }
下面是FileExporter.cs的代码,在这里我们用到了之前提及的CSharpJExcel库,我们需要先把它加到我们工程的引用项中,然后在代码里调用即可。在这部分代码中,我们首先会调用ClearDirectory函数,清空之前转出来的lua配置文件。然后遍历Excel目录下的所有Excel文件,对其依次执行ExportSingleLuaFile函数。在ExportSingleLuaFile函数中主要做的是打开每一张Excel表格,并且依次遍历里面的Sheet文件,对其中命名合法的Sheet切页进行导出(sheet名称前带有#的为导出的表格,不带#的会被自动忽略掉,通过这个规则可以方便自由地控制导出规则,决定哪些Sheet导出,哪些Sheet不导出)。
1 using System; 2 using System.Collections.Generic; 3 using System.IO; 4 using System.Linq; 5 using System.Text; 6 using System.Threading.Tasks; 7 using CSharpJExcel.Jxl; 8 9 namespace Xls2Lua 10 { 11 /// <summary> 12 /// 负责最终文件的输出保存等操作类 13 /// </summary> 14 public class FileExporter 15 { 16 17 /// <summary> 18 /// 清空某个DIR下的内容 19 /// </summary> 20 /// <param name="dir"></param> 21 public static void ClearDirectory(string dir) 22 { 23 if (!Directory.Exists(dir)) 24 { 25 return; 26 } 27 Console.WriteLine("清空目录:" + dir); 28 DirectoryInfo directoryInfo = new DirectoryInfo(dir); 29 FileSystemInfo[] fileSystemInfos = directoryInfo.GetFileSystemInfos(); 30 31 foreach (var info in fileSystemInfos) 32 { 33 if (info is DirectoryInfo) 34 { 35 DirectoryInfo subDir = new DirectoryInfo(info.FullName); 36 try 37 { 38 subDir.Delete(true); 39 } 40 catch (Exception e) 41 { 42 Console.WriteLine("警告:目录删除失败 " + e.Message); 43 } 44 } 45 else 46 { 47 try 48 { 49 File.Delete(info.FullName); 50 } 51 catch (Exception e) 52 { 53 Console.WriteLine("警告:文件删除失败 " + e.Message); 54 } 55 } 56 } 57 } 58 59 /// <summary> 60 /// 导出所有的Excel配置到对应的lua文件中 61 /// </summary> 62 /// <param name="inDir"></param> 63 /// <param name="outDir"></param> 64 public static void ExportAllLuaFile(string inDir, string outDir) 65 { 66 ClearDirectory(outDir); 67 List<string> allXlsList = Directory.GetFiles(inDir, "*.xls", SearchOption.AllDirectories).ToList(); 68 Console.WriteLine("开始转表..."); 69 foreach (var curXlsName in allXlsList) 70 { 71 ExportSingleLuaFile(curXlsName, outDir); 72 } 73 Console.WriteLine("按任意键继续..."); 74 Console.ReadKey(); 75 } 76 77 public static void ExportSingleLuaFile(string xlsName, string outDir) 78 { 79 if (".xls" != Path.GetExtension(xlsName).ToLower()) 80 { 81 return; 82 } 83 84 Console.WriteLine(Path.GetFileName(xlsName)); 85 86 //打开文件流 87 FileStream fs = null; 88 try 89 { 90 fs = File.Open(xlsName, FileMode.Open); 91 } 92 catch (Exception e) 93 { 94 Console.WriteLine(e.Message); 95 throw; 96 } 97 if (null == fs) return; 98 //读取xls文件 99 Workbook book = Workbook.getWorkbook(fs); 100 fs.Close(); 101 //循环处理sheet 102 foreach (var sheet in book.getSheets()) 103 { 104 string sheetName = XlsTransfer.GetSheetName(sheet); 105 if (string.IsNullOrEmpty(sheetName)) continue; 106 sheetName = sheetName.Substring(1, sheetName.Length - 1); 107 Console.WriteLine("Sheet:" + sheetName); 108 string outPath = Path.Combine(outDir, sheetName + ".lua"); 109 string content = XlsTransfer.GenLuaFile(sheet); 110 if (!string.IsNullOrEmpty(content)) 111 { 112 File.WriteAllText(outPath, content); 113 } 114 } 115 } 116 } 117 }
下面是GloablDef.cs的代码,我们主要在里面定义了一些字段类型的枚举和一些通用数据结构,其中的ColoumnDesc类用来存储表格数据:
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 7 namespace Xls2Lua 8 { 9 /// <summary> 10 /// 表格字段类型的枚举 11 /// </summary> 12 public enum FieldType : byte 13 { 14 c_unknown, 15 c_int32, 16 c_int64, 17 c_bool, 18 c_float, 19 c_double, 20 c_string, 21 c_uint32, 22 c_uint64, 23 c_fixed32, 24 c_fixed64, 25 c_enum, 26 c_struct 27 } 28 29 /// <summary> 30 /// 表头字段描述 31 /// </summary> 32 public class ColoumnDesc 33 { 34 public int index = -1; 35 public string comment = ""; 36 public string typeStr = ""; 37 public string name = ""; 38 public FieldType type; 39 public bool isArray = false; 40 } 41 }
最后压轴出场的是我们的核心类:XlsTransfer,其核心代码如下:
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 using CSharpJExcel.Jxl; 7 8 namespace Xls2Lua 9 { 10 11 /// <summary> 12 /// Xls表格转换处理核心类 13 /// </summary> 14 public class XlsTransfer 15 { 16 /// <summary> 17 /// 分割字符串的依据 18 /// </summary> 19 private static readonly char[] splitSymbol = { '|' }; 20 21 /// <summary> 22 /// 根据字符串返回对应字段类型 23 /// </summary> 24 /// <param name="str"></param> 25 /// <returns></returns> 26 public static FieldType StringToFieldType(string str) 27 { 28 str = str.Trim(); 29 str = str.ToLower(); 30 if ("int32" == str) 31 return FieldType.c_int32; 32 else if ("int64" == str) 33 return FieldType.c_int64; 34 else if ("bool" == str) 35 return FieldType.c_bool; 36 else if ("float" == str) 37 return FieldType.c_float; 38 else if ("double" == str) 39 return FieldType.c_double; 40 else if ("string" == str) 41 return FieldType.c_string; 42 else if ("uint32" == str) 43 return FieldType.c_uint32; 44 else if ("uint64" == str) 45 return FieldType.c_uint64; 46 else if ("fixed32" == str) 47 return FieldType.c_fixed32; 48 else if ("fixed64" == str) 49 return FieldType.c_fixed64; 50 return FieldType.c_unknown; 51 } 52 53 /// <summary> 54 /// 根据字段类型,返回对应的字符串 55 /// </summary> 56 /// <param name="type"></param> 57 /// <returns></returns> 58 public static string FieldTypeToString(FieldType type) 59 { 60 if (type == FieldType.c_int32) 61 { 62 return "int32"; 63 } 64 else if (type == FieldType.c_int64) 65 { 66 return "int64"; 67 } 68 else if (type == FieldType.c_bool) 69 { 70 return "bool"; 71 } 72 else if (type == FieldType.c_float) 73 { 74 return "float"; 75 } 76 else if (type == FieldType.c_double) 77 { 78 return "double"; 79 } 80 else if (type == FieldType.c_string) 81 { 82 return "string"; 83 } 84 else if (type == FieldType.c_uint32) 85 { 86 return "uint32"; 87 } 88 else if (type == FieldType.c_uint64) 89 { 90 return "uint64"; 91 } 92 else if (type == FieldType.c_fixed32) 93 { 94 return "fixed32"; 95 } 96 else if (type == FieldType.c_fixed64) 97 { 98 return "fixed64"; 99 } 100 return ""; 101 } 102 103 /// <summary> 104 /// 获取表格的列数,表头碰到空白列直接中断 105 /// </summary> 106 public static int GetSheetColoumns(Sheet sheet) 107 { 108 int coloum = sheet.getColumns(); 109 for (int i = 0; i < coloum; i++) 110 { 111 string temp1 = sheet.getCell(i, 1).getContents(); 112 string temp2 = sheet.getCell(i, 2).getContents(); 113 if (string.IsNullOrWhiteSpace(temp1) || string.IsNullOrWhiteSpace(temp2)) 114 { 115 return i; 116 } 117 } 118 return coloum; 119 } 120 121 /// <summary> 122 /// 获取表格行数,行开头是空白直接中断 123 /// </summary> 124 /// <param name="sheet"></param> 125 /// <returns></returns> 126 public static int GetSheetRows(Sheet sheet) 127 { 128 int rows = sheet.getRows(); 129 for (int i = 0; i < sheet.getRows(); i++) 130 { 131 if (i >= 5) 132 { 133 if (string.IsNullOrEmpty(sheet.getCell(0, i).getContents())) 134 { 135 return i; 136 } 137 } 138 } 139 return rows; 140 } 141 142 /// <summary> 143 /// 获取当前Sheet切页的表头信息 144 /// </summary> 145 /// <param name="sheet"></param> 146 /// <returns></returns> 147 public static List<ColoumnDesc> GetColoumnDesc(Sheet sheet) 148 { 149 int coloumnCount = GetSheetColoumns(sheet); 150 List<ColoumnDesc> coloumnDescList = new List<ColoumnDesc>(); 151 for (int i = 0; i < coloumnCount; i++) 152 { 153 string comment = sheet.getCell(i, 0).getContents().Trim(); 154 comment = string.IsNullOrWhiteSpace(comment) ? comment : comment.Split('\n')[0]; 155 string typeStr = sheet.getCell(i, 1).getContents().Trim(); 156 string nameStr = sheet.getCell(i, 2).getContents().Trim(); 157 158 bool isArray = typeStr.Contains("[]"); 159 typeStr = typeStr.Replace("[]", ""); 160 FieldType fieldType; 161 if (typeStr.ToLower().StartsWith("struct-")) 162 { 163 typeStr = typeStr.Remove(0, 7); 164 fieldType = FieldType.c_struct; 165 } 166 else if (typeStr.ToLower().StartsWith("enum-")) 167 { 168 typeStr.Remove(0, 5); 169 fieldType = FieldType.c_enum; 170 } 171 else 172 { 173 fieldType = StringToFieldType(typeStr); 174 } 175 ColoumnDesc coloumnDesc = new ColoumnDesc(); 176 coloumnDesc.index = i; 177 coloumnDesc.comment = comment; 178 coloumnDesc.typeStr = typeStr; 179 coloumnDesc.name = nameStr; 180 coloumnDesc.type = fieldType; 181 coloumnDesc.isArray = isArray; 182 coloumnDescList.Add(coloumnDesc); 183 } 184 return coloumnDescList; 185 } 186 187 /// <summary> 188 /// 生成最后的lua文件 189 /// </summary> 190 /// <param name="coloumnDesc"></param> 191 /// <param name="sheet"></param> 192 /// <returns></returns> 193 public static string GenLuaFile(Sheet sheet) 194 { 195 List<ColoumnDesc> coloumnDesc = GetColoumnDesc(sheet); 196 197 StringBuilder stringBuilder = new StringBuilder(); 198 stringBuilder.Append("--[[Notice:This lua config file is auto generate by Xls2Lua Tools,don't modify it manually! --]]\n"); 199 if (null == coloumnDesc || coloumnDesc.Count <= 0) 200 { 201 return stringBuilder.ToString(); 202 } 203 //创建索引 204 Dictionary<string, int> fieldIndexMap = new Dictionary<string, int>(); 205 for (int i = 0; i < coloumnDesc.Count; i++) 206 { 207 fieldIndexMap[coloumnDesc[i].name] = i + 1; 208 } 209 //创建数据块的索引表 210 stringBuilder.Append("local fieldIdx = {}\n"); 211 foreach (
全部评论
请发表评论