前言 在Unity开发中,游戏数据的配置和管理是一款游戏能否正常展开开发工作的前提。由于Excel直观易用功能强大易于学习,用它配置数据非常迅速且容错率较高,游戏策划通常使用Excel来配置客户端的游戏数据。这就需要我们Unity开发者能把在Excel中配置的游戏数据转换为可以在Unity中使用的数据文件。
下面将逐步去实现一个支持C#和Lua的Excel的导出工具。
功能分析 C#代码 对于C#,如果配置文件扩展名是自定义格式(如.data,.config等)是不行的,因为Unity只能识别特定几种格式的文本文件,自定义格式无法识别,如果从AssetBundle中加载文本文件也必然要用到TextAsset类加载文本文件里的内容,无法识别的格式也是加载不出来的。常用数据序列化的格式无外乎xml、json、csv这几个,考虑到读取、解析的速度问题,使用.bytes格式文件来存储二进制数据是最佳的选择,所以工具导出的文件格式只能是.bytes。
有了数据文件还不够,工具还需要根据Excel中配置的字段名生成对应的实体类,JSON字符串也要解析出实体类,我们只要复制数据文件和生成的实体类代码到项目中就可以立刻进行功能开发而不用额外花时间手写实体类。
Visual Studio中已经实现了这个功能,编辑→选择性粘贴→将JSON粘贴为类。
Lua代码 和C#相同,工具能够能把Excel中配置的字段直接生成Lua Table,Excel中填写的JSON字符串也要解析成Lua Table。
导出限制 导出时,工具能够支持Excel中通过配置的方式来指定哪一行或哪一列不导出,也可以在表头直接指定这一张Excel整个不导出。
分析至此,下面就可以开始着手进行开发了。
准备工作 Unity开发日常工作中最常用的语言就是C#、Lua、Python。Lua主要是写可以热更的业务代码,Python主要是结合Shell写一些批处理。所以,在不学习新语言新技术的前提下,这个Excel工具使用Winform或者UnityEditor来开发是比较快速的,考虑到工具需要在没有安装Unity的情况下也可使用,所以开发平台直接使用Winform。
工具的界面要包含如下几个功能:
导出语言选择
Excel文件选择列表
读取、导出
配置增删改
配置路径、导出路径选择和拖拽
配置名称
随意设计就好,主要是一个ComboBox和CheckedListBox,读取和导出就是两个简单的Button,点击读取和导出时会弹出FolderBrowserDialog对话框选择路径,根据选择路径进行读取文件和导出数据。路径的配置使用xml保存在工具的根目录。
读取Excel文件 常规读取是使用数据库连接字符串连接到Excel进行逐列逐行读取,但我后来发现不同版本的Excel连接字符串的写法差异很大,不可能为每一个版本的Excel都写一个连接字符串,这里需要安装一个专门操作Excel的库:NPOI。使用NPOI不用关心Excel的版本差异只处理扩展名.xls和.xlsx就好,具体的用法看这篇文章 ,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 using NPOI.HSSF.UserModel;using NPOI.SS.UserModel;using NPOI.XSSF.UserModel;using System.Collections.Generic;using System.Data;using System.IO;namespace ExcelExport.Helper { public static class ExcelHelper { public static DataTable[] ExcelToTable (string file ) { IWorkbook workbook = null ; string fileExt = Path.GetExtension(file).ToLower(); using (FileStream fs = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) { if (fileExt == ".xlsx" ) { workbook = new XSSFWorkbook(fs); } else if (fileExt == ".xls" ) { workbook = new HSSFWorkbook(fs); } if (workbook == null ) { return null ; } if (workbook.NumberOfSheets < 1 ) { return null ; } DataTable[] dts = new DataTable[workbook.NumberOfSheets]; for (int i = 0 ; i < workbook.NumberOfSheets; i++) { dts[i] = new DataTable(); ISheet sheet = workbook.GetSheetAt(i); IRow header = sheet.GetRow(sheet.FirstRowNum); if (header == null ) { continue ; } List<int > columns = new List<int >(); for (int j = 0 ; j < header.LastCellNum; j++) { object obj = GetValueType(header.GetCell(j)); if (obj == null || obj.ToString() == string .Empty) { dts[i].Columns.Add(new DataColumn("Columns" + j.ToString())); } else { dts[i].Columns.Add(new DataColumn(obj.ToString())); } columns.Add(j); } int rowIndex = 0 ; for (int j = sheet.FirstRowNum; j <= sheet.LastRowNum; j++) { DataRow dr = dts[i].NewRow(); bool hasValue = false ; foreach (int k in columns) { IRow row = sheet.GetRow(j); if (row != null ) { dr[k] = GetValueType(row.GetCell(k)); } if (dr[k] != null && dr[k].ToString() != string .Empty) { hasValue = true ; } } if (hasValue || rowIndex == 3 ) { dts[i].Rows.Add(dr); } rowIndex++; } dts[i].TableName = sheet.SheetName; } return dts; } } private static object GetValueType (ICell cell ) { if (cell == null ) { return null ; } switch (cell.CellType) { case CellType.Blank: return string .Empty; case CellType.Boolean: return cell.BooleanCellValue; case CellType.Numeric: return cell.NumericCellValue; case CellType.String: return cell.StringCellValue; case CellType.Error: return cell.ErrorCellValue; case CellType.Formula: return GetCachedFormulaResult(cell); default : return string .Empty; } } private static object GetCachedFormulaResult (ICell cell ) { switch (cell.CachedFormulaResultType) { case CellType.Unknown: return null ; case CellType.Numeric: return cell.NumericCellValue; case CellType.String: return cell.StringCellValue; case CellType.Blank: return null ; case CellType.Boolean: return cell.BooleanCellValue; default : return null ; } } } }
以上代码在读取按钮点击时调用会返回一个DataTable的数组,每一个DataTable对应一张表,通过行列的索引就可以读取到对应位置的配置数据。
配置格式 Excel的配置格式要视情况而定,这里是这样规定的:
下一篇进行数据生成和代码生成的功能开发,并将其整合到UI界面上。