游戏开发Excel表格批量转换成lua的转表工具
Posted msxh
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了游戏开发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库的下载链接:
- 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 (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 }
还记得上文提到的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
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
其实它们都是一段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
在这里我们先定义了_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()
其执行结果如下图所示:
图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的转表工具的主要内容,如果未能解决你的问题,请参考以下文章