前言


在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。

工具的界面要包含如下几个功能:


  1. 导出语言选择

  2. Excel文件选择列表

  3. 读取、导出


界面

  1. 配置增删改

  2. 配置路径、导出路径选择和拖拽

  3. 配置名称


界面

随意设计就好,主要是一个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
{
/// <summary>
/// 根据文件路径读取Excel文件
/// </summary>
/// <param name="file"></param>
/// <returns></returns>
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];//一张excel中可能有许多张表,要把这些表全部读出来

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)//第4行为标记行,可能为空
{
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的配置格式要视情况而定,这里是这样规定的:

  • 第一行第一列留空,其后为字段名

  • 第二行第一列为配置表名,其后为字段类型

  • 第三行第一列留空,其后可以留空或者填入字段含义

  • 第四行第一列填入“BAN”则整张表不导出,其后填入“BAN”则对应列整列不导出

  • 第五行开始,每行第一列填入“BAN”则整行不导出,其后为具体数据

  • 以上填入“BAN”的位置若没有不导出的需求则留空

配置

下一篇进行数据生成和代码生成的功能开发,并将其整合到UI界面上。