【游戏开发】Excel表格批量转换成lua的转表工具

一、简介

  在上篇博客《【游戏开发】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库的下载链接:

三、转表工具

1.思路分析

  一切准备就绪,可以开始我们的开发任务了。首先我们来大致地说一下转表工具的思路:

  1. 读取Excel表格文件的数据,依次读取配置目录下的Excel文件,然后逐个读取表里面Sheet的内容;
  2. 根据Excel表格中配置的字段类型,对数据进行校验,判断数据是否合法;
  3. 将通过校验的数据转为lua文件,一个Sheet切页对应一个lua配置文件;
  4. 使用通用的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 }
View Code

  下面是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 }
View Code

   下面是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 }
View Code

   最后压轴出场的是我们的核心类: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 (var cur in fieldIndexMap)
212             {
213                 stringBuilder.Append(string.Format("fieldIdx.{0} = {1}\n", cur.Key, cur.Value));
214             }
215 
216             //创建数据块
217             stringBuilder.Append("local data = {");
218             int rows = GetSheetRows(sheet);
219             int validRowIdx = 4;
220             //逐行读取并处理
221             for (int i = validRowIdx; i < rows; i++)
222             {
223                 StringBuilder oneRowBuilder = new StringBuilder();
224                 oneRowBuilder.Append("{");
225                 //对应处理每一列
226                 for (int j = 0; j < coloumnDesc.Count; j++)
227                 {
228                     ColoumnDesc curColoumn = coloumnDesc[j];
229                     var curCell = sheet.getCell(curColoumn.index, i);
230                     string content = curCell.getContents();
231 
232                     if (FieldType.c_struct != curColoumn.type)
233                     {
234                         FieldType fieldType = curColoumn.type;
235                         //如果不是数组类型的话
236                         if (!curColoumn.isArray)
237                         {
238                             content = GetLuaValue(fieldType, content);
239                             oneRowBuilder.Append(content);
240                         }
241                         else
242                         {
243                             StringBuilder tmpBuilder = new StringBuilder("{");
244                             var tmpStringList = content.Split(splitSymbol, StringSplitOptions.RemoveEmptyEntries);
245                             for (int k = 0; k < tmpStringList.Length; k++)
246                             {
247                                 tmpStringList[k] = GetLuaValue(fieldType, tmpStringList[k]);
248                                 tmpBuilder.Append(tmpStringList[k]);
249                                 if (k != tmpStringList.Length - 1)
250                                 {
251                                     tmpBuilder.Append(",");
252                                 }
253                             }
254 
255                             oneRowBuilder.Append(tmpBuilder);
256                             oneRowBuilder.Append("}");
257                         }
258                     }
259                     else
260                     {
261                         //todo:可以处理结构体类型的字段
262                         throw new Exception("暂不支持结构体类型的字段!");
263                     }
264 
265                     if (j != coloumnDesc.Count - 1)
266                     {
267                         oneRowBuilder.Append(",");
268                     }
269                 }
270 
271                 oneRowBuilder.Append("},");
272                 stringBuilder.Append(string.Format("\n{0}", oneRowBuilder));
273             }
274             //当所有的行都处理完成之后
275             stringBuilder.Append("}\n");
276             //设置元表
277             string str =
278                 "local mt = {}\n" +
279                 "mt.__index = function(a,b)\n" +
280                 "\tif fieldIdx[b] then\n" +
281                 "\t\treturn a[fieldIdx[b]]\n" +
282                 "\tend\n" +
283                 "\treturn nil\n" +
284                 "end\n" +
285                 "mt.__newindex = function(t,k,v)\n" +
286                 "\terror('do not edit config')\n" +
287                 "end\n" +
288                 "mt.__metatable = false\n" +
289                 "for _,v in ipairs(data) do\n\t" +
290                 "setmetatable(v,mt)\n" +
291                 "end\n" +
292                 "return data";
293             stringBuilder.Append(str);
294             return stringBuilder.ToString();
295         }
296 
297         /// <summary>
298         /// 处理字符串,输出标准的lua格式
299         /// </summary>
300         /// <param name="fieldType"></param>
301         /// <param name="value"></param>
302         /// <returns></returns>
303         private static string GetLuaValue(FieldType fieldType, string value)
304         {
305             if (FieldType.c_string == fieldType)
306             {
307                 if (string.IsNullOrWhiteSpace(value))
308                 {
309                     return "\"\"";
310                 }
311 
312                 return string.Format("[[{0}]]", value);
313             }
314             else if (FieldType.c_enum == fieldType)
315             {
316                 //todo:可以具体地相应去处理枚举型变量
317                 string enumKey = value.Trim();
318                 return enumKey;
319             }
320             else if (FieldType.c_bool == fieldType)
321             {
322                 bool isOk = StringToBoolean(value);
323                 return isOk ? "true" : "false";
324             }
325             else
326             {
327                 return string.IsNullOrEmpty(value.Trim()) ? "0" : value.Trim();
328             }
329         }
330 
331         /// <summary>
332         /// 字符串转为bool型,非0和false即为真
333         /// </summary>
334         /// <param name="value"></param>
335         /// <returns></returns>
336         private static bool StringToBoolean(string value)
337         {
338             value = value.ToLower().Trim();
339             if (string.IsNullOrEmpty(value))
340             {
341                 return true;
342             }
343 
344             if ("false" == value)
345             {
346                 return false;
347             }
348 
349             int num = -1;
350             if (int.TryParse(value, out num))
351             {
352                 if (0 == num)
353                 {
354                     return false;
355                 }
356             }
357 
358             return true;
359         }
360 
361         /// <summary>
362         /// 获取当前sheet的合法名称
363         /// </summary>
364         /// <param name="sheet"></param>
365         /// <returns></returns>
366         public static string GetSheetName(Sheet sheet)
367         {
368             var sheetName = sheet.getName();
369             return ParseSheetName(sheetName);
370         }
371 
372         /// <summary>
373         /// 检测Sheet的名称是否合法,并返回合法的sheet名称
374         /// </summary>
375         /// <param name="sheetName"></param>
376         /// <returns></returns>
377         private static string ParseSheetName(string sheetName)
378         {
379             sheetName = sheetName.Trim();
380             if (string.IsNullOrEmpty(sheetName))
381             {
382                 return null;
383             }
384             //只有以#为起始的sheet才会被转表
385             if (!sheetName.StartsWith("#"))
386             {
387                 return null;
388             }
389 
390             return sheetName;
391         }
392     }
393 }
View Code

  还记得上文提到的FileExporter类嘛,它会遍历每一张Sheet,然后调用XlsTransfer的GenLuaFile函数,把表格数据转为字符串,然后再把字符串导出为lua配置文件。在GenLuaFile函数中,将先对传入的sheet进行GetSheetColoumns处理,获取该Sheet中的每一个格子的信息(包括第几列Index,表格中的内容,对应的索引字段的名字,数据类型枚举,是否是数组标志位等等信息)。拿到这些信息以后,我们逐一对其进行进一步的处理,如果不是数组的话,我们将其直接添加到StringBuilder里面;如果是数组的话,我们根据字符"|",将其分解为n个单独的数据字段,然后存储为Lua中的table结构。在处理的过程中,会利用StringBuilder将数据自动化地格式为元表和table的lua数据结构,方便Lua端读取数据,具体操作可以看代码,这里就不再赘述。

四、读取Lua配置文件

  经过上面的一系列操作,我们得到了转换后的Lua配置文件,它长成下面这个样子:

 1 --[[Notice:This lua config file is auto generate by Xls2Lua Tools,don't modify it manually! --]]
 2 local fieldIdx = {}
 3 fieldIdx.id = 1
 4 fieldIdx.text = 2
 5 local data = {
 6 {10000,[[测试文字1]]},
 7 {10001,[[测试文字2]]},}
 8 local mt = {}
 9 mt.__index = function(a,b)
10     if fieldIdx[b] then
11         return a[fieldIdx[b]]
12     end
13     return nil
14 end
15 mt.__newindex = function(t,k,v)
16     error('do not edit config')
17 end
18 mt.__metatable = false
19 for _,v in ipairs(data) do
20     setmetatable(v,mt)
21 end
22 return data
View Code
 1 --[[Notice:This lua config file is auto generate by Xls2Lua Tools,don't modify it manually! --]]
 2 local fieldIdx = {}
 3 fieldIdx.id = 1
 4 fieldIdx.path = 2
 5 fieldIdx.resType = 3
 6 fieldIdx.resLiveTime = 4
 7 local data = {
 8 {100,[[Arts/Gui/Prefabs/uiLoginPanel.prefab]],0,20},
 9 {2001,[[Arts/Gui/Textures/airfightSheet.prefab]],0,-2},}
10 local mt = {}
11 mt.__index = function(a,b)
12     if fieldIdx[b] then
13         return a[fieldIdx[b]]
14     end
15     return nil
16 end
17 mt.__newindex = function(t,k,v)
18     error('do not edit config')
19 end
20 mt.__metatable = false
21 for _,v in ipairs(data) do
22     setmetatable(v,mt)
23 end
24 return data
View Code

  其实它们都是一段lua代码,因此可以直接执行,而不必再去解析,所以会节省不少性能。先来让我们看一下它的结构。首先第一行是一行注释说明,表示该配置文件是由软件自动生成的,请不要随意更改!然后定义了一个名为fieldIdx的table,顾名思义,他就是用来把字段名和对应的列的index建立起索引关系的一个数据结构。例如id字段对应第一列,path字段对应第二列,以此类推。那么我们定义这个table的用处是什么呢?别急,我们马上就会用到它,先接着往下看。我们在fieldIdx后面紧接着定义了名为data的table,从上述配置文件中,我们可以很明显地看到data才是真正存储着我们数据的结构。按照行、列的顺序和数据类型,我们将Excel表格中的数据依次存在了data结构里面。再接着,定义了一个名为mt的table,他重写了__index、__newindex、__metatable这样几个方法。通过设置mt.__metatable = false关闭它的元表,然后在重写的__newindex中我们输出一个error信息,表示配置文件不可以被更改,这样就保证了我们的配置文件的安全,使得它不能再运行时随意的增删字段。然后我们把__index指向了一个自定义函数function(a,b),其中第一参数是待查找的table,b表示的是想要索引的字段。(__index方法除了可以是一个表,也可以是一个函数,如果是函数的话,__index方法被调用时会返回该函数的返回值)在这个函数中,我们会先去之前定义的fieldIdx中,获取字段名所对应的index,然后再去data表中拿index对应的值。而这个值就是我们最后需要的值了。最后别忘了,在整段代码的最后,遍历data,将里面每个子table的元表设置为mt。这样就可以根据Lua查找表元素的机制方便地获取到我们需要的字段对应的值了。(对lua的查找表元素过程和元表、元方法等概念不熟悉的读者可以先去看一下这篇博客《【游戏开发】小白学Lua——从Lua查找表元素的过程看元表、元方法》

  好了,我们的配置文件也成功获取到了,下面该去读取配置文件中的内容了。为了方便读取并且提高效率,我做了一个名ConfigMgr的类,它封装了一些函数,可以根据id获取对应的一行的数据或者根据表名获取该表的所有配置,并且兼具缓存功能,对已经加载过的配置文件直接做返回数据处理,不用多次加载读取,提高性能。ConfigMgr的代码如下所示:

 1 require "Class"
 2 
 3 ConfigMgr = {
 4     --实例对象
 5     _instance = nil,
 6     --缓存表格数据
 7     _cacheConfig = {},
 8     --具有id的表的快速索引缓存,结构__fastIndexConfig["LanguageCfg"][100] 
 9     _quickIndexConfig = {},
10 }
11 ConfigMgr.__index = ConfigMgr
12 setmetatable(ConfigMgr,Class)
13 
14 -- 数据配置文件的路径
15 local cfgPath = "../LuaData/%s.lua"
16 
17 -- 构造器
18 function ConfigMgr:new()
19     local self = {}
20     self = Class:new()
21     setmetatable(self,ConfigMgr)
22     return self
23 end
24 
25 -- 获取单例
26 function ConfigMgr:Instance()
27     if ConfigMgr._instance == nil then
28         ConfigMgr._instance = ConfigMgr:new()
29     end
30     return ConfigMgr._instance
31 end
32 
33 -- 获取对应的表格数据
34 function ConfigMgr:GetConfig(name)
35     local tmpCfg = self._cacheConfig[name]
36     if nil ~= tmpCfg then
37         return tmpCfg
38     else 
39         local fileName = string.format(cfgPath,name)
40         --print("----------->Read Config File"..fileName)
41         -- 读取配置文件
42         local cfgData = dofile(fileName)
43         
44         -- 对读取到的配置做缓存处理
45         self._cacheConfig[name] = {}
46         self._cacheConfig[name].items = cfgData;
47         return self._cacheConfig[name]
48     end
49     return nil
50 end
51 
52 -- 获取表格中指定的ID项
53 function ConfigMgr:GetItem(name,id)
54     if nil == self._quickIndexConfig[name] then
55         local cfgData = self:GetConfig(name)
56         if cfgData and cfgData.items and cfgData.items[1] then
57             -- 如果是空表的话不做处理
58             local _id = cfgData.items[1].id
59             if _id then
60                 -- 数据填充
61                 self._quickIndexConfig[name] = {}
62                 for _,v in ipairs(cfgData.items) do 
63                     self._quickIndexConfig[name][v.id]= v
64                     print("---->"..v.id)
65                 end
66             else
67                 print(string.format("Config: %s don't contain id: %d!",name,id))
68             end
69         end
70     end
71     if self._quickIndexConfig[name] then
72         return self._quickIndexConfig[name][id]
73     end
74     return nil
75 end
View Code

  在这里我们先定义了_cacheConfig和_quickIndexConfig这样两个字段,_cacheConfig用来缓存配置文件名对应的数据,而_quickIndexConfig用来缓存配置文件名+id对应的数据,这样虽然稍稍多占用了一些内存空间,但是极大地提升了我们访问数据的速度。为了方便调用ConfigMgr,我将其做成了单例类,在需要的地方调用一下Instance()方法,就可以获取到ConfigMgr的实例了。

  在ConfigMgr中主要有两个供外界访问的接口:GetConfig(name)和GetItem(name,id)。在GetConfig(name)函数中,首先根据name去缓存中查看是否有缓存数据,如果有缓存数据则直接返回,如果没有加载过该配置文件,则会把配置文件的根目录和配置文件名拼接成一个完整的配置文件路径,然后调用dofile方法,把这个数据加载进来,并且缓存进_cacheConfig表中,以便下次快速访问。在GetItem(name,id)函数中,首先会判断_quickIndexConfig缓存中是否有name对应的数据存在。如果有,则直接返回self._quickIndexConfig[name][id],也就是id对应的那一行的配置数据。如果没有,则调用上面的GetConfig(name)函数,把对应的名称的数据文件先加载进来,然后按照对应的name和id把数据一一缓存起来。

  最后,让我们在Main.lua中实战检验一下上面一系列的操作是否成功: 

 1 require "Class"
 2 require "ConfigMgr"
 3 
 4 function Main()
 5     local configMgr = ConfigMgr:Instance()
 6     local lang = configMgr:GetConfig("Language")
 7     print(lang.items[1].id .. " " .. lang.items[1].text)
 8     local myText = configMgr:GetItem("Language",10000).text
 9     print(myText)
10 end
11 
12 Main()
View Code

  其执行结果如下图所示:

  

  图3:最后的执行结果

  可以看到,我们成功地取到了表格中的数据并且输出了出来,因为lua编码的原因,中文变成了乱码,不过这并不影响我们在Unity开发中使用配置文件。

五、总结

  在本篇博客中,我们一起学习了如何使用C#制作一款简洁的转表工具,从而提升我们的工作效率。最后还是要推荐一款优秀的成熟的转表工具XlsxToLua。它是由tolua的开发者为广大的Unity开发人员制作的一款可以将Excel表格数据导出为Lua table、csv、json形式的工具,兼带数据检查功能以及导出、导入MySQL数据库功能。除此之外,还支持GUI界面等很多实用的功能,大家感兴趣的话可以到Github去查看该项目的具体内容:https://github.com/zhangqi-ulua/XlsxToLua

   本篇博客中的所有代码已经托管到Github,开源地址:https://github.com/XINCGer/Unity3DTraining/tree/master/XlsxTools/Xls2Lua

 

 

作者:马三小伙儿
出处:https://www.cnblogs.com/msxh/p/8539108.html 
请尊重别人的劳动成果,让分享成为一种美德,欢迎转载。另外,文章在表述和代码方面如有不妥之处,欢迎批评指正。留下你的脚印,欢迎评论!

posted @ 2018-05-23 22:55  马三小伙儿  阅读(8870)  评论(0编辑  收藏  举报