一、简介

  在上篇博客《【游戏开发】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(',')[];
  26. inDir = Path.GetFullPath(inDir);
  27. outDir = reader.ReadLine().Split(',')[];
  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(, sheetName.Length - );
  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 = -;
  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 = ; i < coloum; i++)
  110. {
  111. string temp1 = sheet.getCell(i, ).getContents();
  112. string temp2 = sheet.getCell(i, ).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 = ; i < sheet.getRows(); i++)
  130. {
  131. if (i >= )
  132. {
  133. if (string.IsNullOrEmpty(sheet.getCell(, 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 = ; i < coloumnCount; i++)
  152. {
  153. string comment = sheet.getCell(i, ).getContents().Trim();
  154. comment = string.IsNullOrWhiteSpace(comment) ? comment : comment.Split('\n')[];
  155. string typeStr = sheet.getCell(i, ).getContents().Trim();
  156. string nameStr = sheet.getCell(i, ).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(, );
  164. fieldType = FieldType.c_struct;
  165. }
  166. else if (typeStr.ToLower().StartsWith("enum-"))
  167. {
  168. typeStr.Remove(, );
  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 <= )
  200. {
  201. return stringBuilder.ToString();
  202. }
  203. //创建索引
  204. Dictionary<string, int> fieldIndexMap = new Dictionary<string, int>();
  205. for (int i = ; i < coloumnDesc.Count; i++)
  206. {
  207. fieldIndexMap[coloumnDesc[i].name] = i + ;
  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 = ;
  220. //逐行读取并处理
  221. for (int i = validRowIdx; i < rows; i++)
  222. {
  223. StringBuilder oneRowBuilder = new StringBuilder();
  224. oneRowBuilder.Append("{");
  225. //对应处理每一列
  226. for (int j = ; 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 = ; k < tmpStringList.Length; k++)
  246. {
  247. tmpStringList[k] = GetLuaValue(fieldType, tmpStringList[k]);
  248. tmpBuilder.Append(tmpStringList[k]);
  249. if (k != tmpStringList.Length - )
  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 - )
  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()) ? "" : 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 = -;
  350. if (int.TryParse(value, out num))
  351. {
  352. if ( == 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. }

  还记得上文提到的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 Toolsdon't modify it manually! --]]
  2. local fieldIdx = {}
  3. fieldIdx.id =
  4. fieldIdx.text =
  5. local data = {
  6. {,[[测试文字1]]},
  7. {,[[测试文字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
  1. --[[Notice:This lua config file is auto generate by Xls2Lua Toolsdon't modify it manually! --]]
  2. local fieldIdx = {}
  3. fieldIdx.id =
  4. fieldIdx.path =
  5. fieldIdx.resType =
  6. fieldIdx.resLiveTime =
  7. local data = {
  8. {,[[Arts/Gui/Prefabs/uiLoginPanel.prefab]],,},
  9. {,[[Arts/Gui/Textures/airfightSheet.prefab]],,-},}
  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

  其实它们都是一段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[] then
  57. -- 如果是空表的话不做处理
  58. local _id = cfgData.items[].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

  在这里我们先定义了_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[].id .. " " .. lang.items[].text)
  8. local myText = configMgr:GetItem("Language",).text
  9. print(myText)
  10. end
  11.  
  12. Main()

  其执行结果如下图所示:

  

  图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 
请尊重别人的劳动成果,让分享成为一种美德,欢迎转载。另外,文章在表述和代码方面如有不妥之处,欢迎批评指正。留下你的脚印,欢迎评论!

【游戏开发】Excel表格批量转换成lua的转表工具的更多相关文章

  1. 【游戏开发】Excel表格批量转换成CSV的小工具

    一.前言 在工作的过程中,我们有时可能会面临将Excel表格转换成CSV格式文件的需求.这尤其在游戏开发中体现的最为明显,策划的数据文档大多是一些Excel表格,且不说这些表格在游戏中读取的速度,但就 ...

  2. C# Unity游戏开发——Excel中的数据是如何到游戏中的 (二)

    本帖是延续的:C# Unity游戏开发——Excel中的数据是如何到游戏中的 (一) 上个帖子主要是讲了如何读取Excel,本帖主要是讲述读取的Excel数据是如何序列化成二进制的,考虑到现在在手游中 ...

  3. C# Unity游戏开发——Excel中的数据是如何到游戏中的 (三)

    本帖是延续的:C# Unity游戏开发——Excel中的数据是如何到游戏中的 (二) 前几天有点事情所以没有继续更新,今天我们接着说.上个帖子中我们看到已经把Excel数据生成了.bin的文件,不过其 ...

  4. C# Unity游戏开发——Excel中的数据是如何到游戏中的 (四)2018.4.3更新

    本帖是延续的:C# Unity游戏开发--Excel中的数据是如何到游戏中的 (三) 最近项目不算太忙,终于有时间更新博客了.关于数据处理这个主题前面的(一)(二)(三)基本上算是一个完整的静态数据处 ...

  5. Excel将秒转换成标准的时间格式HH:MM:SS

    Excel将秒转换成标准的时间格式HH:MM:SS 比如120秒,转换成00:02:00 Excel公式为: =TEXT(A1/86400,"[hh]:mm:ss") A1为秒数据 ...

  6. word ppt excel文档转换成pdf

    1.把word文档转换成pdf (1).添加引用 using Microsoft.Office.Interop.Word; 添加引用 (2).转换方法 /// <summary> /// ...

  7. Linux下将UTF8编码批量转换成GB2312编码的方法

    Linux下将UTF8编码批量转换成GB2312编码的方法 在sqlplus中导入UTF8编码的sql脚本就会出现乱码错误,这时就需要将UTF8编码转换成GB2312编码,下面为大家介绍下在Linux ...

  8. 将psd格式批量转换成jpg或png格式(C#自制软件)

    此项目基于.net framework 4.0 将psd格式批量转换成jpg或png格式. 链接:https://pan.baidu.com/s/16IEjX0sHaY9H3Ah7mv6IPQ 提取码 ...

  9. 把ANSI格式的TXT文件批量转换成UTF-8文件类型

    把ANSI格式的TXT文件批量转换成UTF-8文件类型 Posted on 2010-08-05 10:38 moss_tan_jun 阅读(3635) 评论(0) 编辑 收藏 #region 把AN ...

随机推荐

  1. Linux 库函数与系统调用的关系与区别

    上周总结了<C 标准库的基础 IO>,其实这些功能函数通过「系统调用」也能实现相应功能.这次文章并不是要详细介绍各系统调用接口的使用方法,而是要深入理解「库函数」与「系统」调用之间的关系和 ...

  2. Markdown编辑技巧

    [前言] 保存Markdown的编辑技巧,写博客随笔,可以用下. [正文] 1.空格  //半角空格(英文)  //全角空格(中文)

  3. IT小团队的管理者的突围之道

    笔者前几天被问到一个问题,你在团队管理方面有什么值得分享的吗?咋一听,实用千言万语,但是事后回忆说出来的东西感觉空无一物,缺少干货.故想通过写一篇随笔思考整理一下,刷新一下自己对小团队管理的认知.这里 ...

  4. 利用truffle与智能合约进行交互

    先了解相关指令,再观看比较合适:http://truffle.tryblockchain.org/ 安装: 先完成上一条博客的安装,再来进行下面的操作:http://www.cnblogs.com/t ...

  5. Ubuntu系统下配置IP地址方法介绍

    配置IP方式有两种: 1.通过命令直接配置 sudo ifconfig eth0 IP地址 netmask 子网掩码------配置IP地 sudo route add default gw 网关-- ...

  6. iOS Swift开发的一些坑

    0.人难招,特别是对于没钱的小团队,大多数的iOS开发者没有Swift经验,有经验的也并不是很深入 0.1.语言unwrap坑,虽然有自动修正提示,但感觉代码过程不流畅. 1.Realm的缺憾: 最近 ...

  7. 设置firefox每次访问网页时检查所存网页的较新版本

    我们做技术,经常在写页面的时候需要多次刷新测试,可是浏览器都有自己的缓存机制,一般CSS和图片都会被缓存在本地,这样我们修改 的CSS就看不到效果了,每次都去清空缓存,再刷新看效果,这样操作太麻烦了. ...

  8. LCA最近公共祖先(倍增版)

    倍增版LCA lac即最近公共祖先,u和v最近公共祖先就是两节点公用的祖先中深度最大的 比如 其中 lca(1,2)=4, lca(2,3)=4, lca(3,5)=1, lca(2,5)=4; 如何 ...

  9. href="#" 是什么意思?

    <a href="#" onclick="process1()">开始你表演</a>作用:书签的另一种用法建立书签语法:<a na ...

  10. SDE与shapefile之间的数据导入与导出

    一.SDE要素导出到shapefile中. 1.创建一个新的shapefile文件. private bool CreateShapefile(string filepath, string name ...