Unity客户端框架之excel表解析
excel数据表格如下(把’=’号看做’|’符号,在markdown中表格和’|’字符冲突):
id | name | quality | pvTime | saleType | saleValue | icon | desc | useten |
---|---|---|---|---|---|---|---|---|
int | list=string | int | int | int | int | string | string | int |
# | 道具名称 | 道具品质 | 有效期:小时 | 出售类型 | 出售价格 | 道具图标ID | 道具描述 | 是否可以使用十次 |
# | 8个汉字以内 | 1:白 | 0:不失效 | 0:不可出售 | 不超过50汉字 | 0:不可以 | ||
# | 2:绿 | 1-9999:有效的小时数 | 1:可出售 | 1:可以 | ||||
# | 3:蓝 | |||||||
# | 4:紫 | |||||||
# | 5:橙 | |||||||
1001 | =金币=金钱 | 3 | 0 | 0 | -1 | 111.png | 金币可以买房子 | 1 |
1002 | =钻石=宝石 | 5 | 0 | 0 | -1 | 222.png | 钻石可以娶老婆 | 1 |
1003 | =改名卡 | 5 | 0 | 1 | 100 | 333.png | 可以你从新做人 | 1 |
1、解析约定
- 需要同时支持服务器(c/c++/lua)和客户端。
- 支持的类型int32、int64、string、String[]、int[] (开发中会用到数组形式的配置数据)。
- excel转成csv,客户端、服务端解析配表都基于这个编码为UFT8的csv文件。将以”,”大写逗号来分割字段、以”|”来分割字段数组。
- excel配置表中第一行始终为字段名称、第二行为字段类型(第二条)、从第三行开始的第一个单元格内容为”#”字符的代表是注视行、非”#”字符的为正式数据部分;
1.1、实现思路
- 基于csv解析。首先的了解下C#反射反射机制、泛型等概念。
- Unity中利用python将csv中第一行变量名称和第二行变量类型解析出来,生成c# 实体文件;(当然你也可以在unity写个Editor来实现,我这里用python)。
- 有了c#实体文件,遍历csv文件中所有行,在利用c#中反射原理创建对象隐射c#实体文件。
- 最终将配表数据转成对象数据加载到内存中。
2、实现代码
2.1、基于python生成c#实体文件
这里的操作文件读写与之前的一篇文章《quick-x.客户端框架之excel表转换解析成lua文件》非常相似,其实我也是拷贝过来改了改,这里就不多记录了
# 写程序lib文件
def writeCSLibFile(dataName,typeList,nameList,noteList):
libClassName = dataName+'Lib'
libFilePath = os.path.join(libDir,dataName+'Lib.cs')
if chVarDis.get() == 1:
libFilePath = os.path.join(curDir,'../../Assets/Scripts/game/library/' + dataName+'Lib.cs')
# 如果文件存在,需要保存用户自定义的内容
extLines = []
isExtLine = False
if os.path.exists(libFilePath):
readFp = codecs.open(libFilePath,"r","utf-8")
oneLine = readFp.readline()
if len(oneLine) != 0:
# 去掉文件头说明
oneLine = readFp.readline()
while len(oneLine) != 0:
if isExtLine == True:
extLines.append(oneLine)
if oneLine.find('START EXT END') != -1:
isExtLine = False
break
else:
if oneLine.find('START EXT BEGIN') != -1:
extLines.append(oneLine)
isExtLine = True
oneLine = readFp.readline()
readFp.close()
libFp = codecs.open(libFilePath,"w","utf-8")
libFp.write('/** 此文件由工具自动生成生成,如需修改,请在START EXT BEGIN 与 START EXT END中间修改 **/\n\n')
libFp.write('using UnityEngine;\n')
libFp.write('using System.Collections;\n')
libFp.write('using System.Collections.Generic;\n\n')
libFp.write('public partial class %s\n' % libClassName)
libFp.write('{\n')
num = len(nameList)
for i in range(0,num):
typeName = typeList[i]
oneProperty = nameList[i]
if typeName=="":
break
if typeName == "list|string":
libFp.write("\tpublic string[]".ljust(18) + oneProperty.ljust(10) + ';'.ljust(20))
elif typeName == "list|int" :
libFp.write("\tpublic int[]".ljust(18) + oneProperty.ljust(10) + ';'.ljust(20))
else :
libFp.write("\tpublic " + typeName.ljust(10) + oneProperty.ljust(10) + ';'.ljust(20))
# 写注释
libFp.write('// ')
for note in noteList:
libFp.write(note[i] + ' ')
libFp.write('\n')
# 字段
libFp.write('\n\tpublic static Dictionary<string, %s> dataList;\n' % libClassName)
# 函数getDataById
libFp.write('\tpublic static %s getDataById(string id) { return dataList[id]; }\n' % libClassName)
# 函数getDataDict
libFp.write('\tpublic static Dictionary<string, %s> getDataDict() { return dataList; }\n' % libClassName)
# 函数getDataList
libFp.write('\tpublic static List<%s> getDataList()\n' % libClassName)
libFp.write('\t{\n')
libFp.write('\t\tList<%s> returnList = new List<%s>();\n' % (libClassName, libClassName))
libFp.write('\t\tforeach (KeyValuePair<string, %s> kvp in dataList)\n' % libClassName)
libFp.write('\t\t{\n')
libFp.write('\t\t\treturnList.Add(kvp.Value);\n')
libFp.write('\t\t}\n')
libFp.write('\t\treturn returnList;\n')
libFp.write('\t}\n')
# 自定义扩展区间
if len(extLines) == 0:
libFp.write('\n/*----------------------------START EXT BEGIN-------------------------------*/\n')
libFp.write('/* 此区间为自定义扩展,工具再次生成不会覆盖*/\n\n')
libFp.write('/*-----------------------------START EXT END--------------------------------*/\n\n')
else:
for extLine in extLines:
libFp.write(extLine)
libFp.write('}')
libFp.close()
if chVarDis.get() == 1:
shutil.copy(libFilePath,os.path.join(libDir,dataName+'Lib.lua'))
def crytoFile(filePath,errList):
print('--------------------------------------------------------------')
print('准备转换: %s' % filePath)
if not filePath.endswith('.csv'):
errMsg = "非txt文档".decode('utf-8')
oneErr = {'name':filePath,'bec':errMsg}
errList.append(oneErr)
print(errMsg)
return False
fileName = os.path.basename(filePath)
dataName = os.path.splitext(fileName)[0]
# print(fileName)
# print(dataName)
# 所有行
lines = []
fp = open(filePath,"r")
line = fp.readline()
while len(line) != 0:
line = line.replace('\r','').replace('\n','')
lines.append(line.decode('utf-8-sig'))
line = fp.readline()
fp.close()
# 如果内容小于2行,说明不是本工程配置文件
if len(lines) < 2:
errMsg = "非本工程配置文件格式".decode('utf-8')
oneErr = {'name':filePath,'bec':errMsg}
errList.append(oneErr)
print(errMsg)
return False
# 变量名字
nameLine = lines[0]
nameList = nameLine.split(',')
del lines[0]
# 变量类型
typeLine = lines[0]
typeList = typeLine.split(',')
del lines[0]
# 注释
noteList = []
while len(lines) >0 and lines[0].startswith('#'):
noteLine = lines[0]
noteLine = noteLine.replace('#','')
noteLineList = noteLine.split(',')
noteList.append(noteLineList)
del lines[0]
# 写程序lib文件
writeCSLibFile(dataName,typeList,nameList,noteList)
return True
上面代码我缩减了一半了,UI相关的都去掉了,不知道markdown支不支持代码段折叠,太费篇幅了。
- 第6行生成解析的实体文件名(个人喜欢是配表名称+Lib后缀结尾)
- 第57行~71行对实体文件增加3个方法,提供获取数据接口
- 第74行~80行增加用户自定义区域,每个配表实体,都可能存在自己的实现逻辑,这个逻辑就卸载自定义区域内,下次再生成实体,不会被覆盖。
2.2、基于python生成c#实体文件
创建一个c#脚本名为LibraryMgr(单利)
提供一个解析配置文件接口ReadConfigData,一个初始化接口init。
2.2.1、 ReadConfigData接口
private void ReadConfigData<T>(string fileName)
{
Dictionary<string, T> objDic = new Dictionary<string, T>();
string getString = Resources.Load<TextAsset>("config/" + fileName).text;
int col, row;
ArrayList data;
utils.onConfigToArr(getString, out col, out row, out data);
FieldInfo[] fis = new FieldInfo[col];
for (int colNum = 0; colNum < col; colNum++) // 取出第一行的变量类型
{
fis[colNum] = typeof(T).GetField(((ArrayList)data[0])[colNum].ToString());
}
// 遍历行(0,1为字段名称和字段类型)
for (int rowNum = 2; rowNum < row; rowNum++)
{
T configObj = Activator.CreateInstance<T>();
for (int i = 0; i < fis.Length; i++)
{
string fieldValue = ((ArrayList)data[rowNum])[i].ToString(); // 取出第rowNum行第i个字段进行赋值
object setValue = new object();
string[] temp;
switch (fis[i].FieldType.ToString())
{
case "System.String[]":
temp = fieldValue.Split('|');
setValue = utils.delElementByIdx(temp, 0);
break;
case "System.Int32[]":
temp = fieldValue.Split('|');
temp = utils.delElementByIdx(temp, 0);
setValue = Array.ConvertAll(temp, int.Parse);
break;
case "System.Int32":
if (fieldValue.Trim().Equals("")) fieldValue = "0";
setValue = int.Parse(fieldValue);
break;
case "System.Int64":
if (fieldValue.Trim().Equals("")) fieldValue = "0";
setValue = long.Parse(fieldValue);
break;
case "System.String":
setValue = fieldValue;
break;
default:
Debug.Log("error data type");
break;
}
fis[i].SetValue(configObj, setValue); // 反射赋值
if (fis[i].Name == "id")// 将key作为字典id
{
objDic.Add(setValue.ToString(), configObj);
}
}
}
FieldInfo p = typeof(T).GetField("dataList");
p.SetValue(null, objDic);
}
- 第3行,字典objDic通过key-value形式存储解析好的对象,key为csv中字段为id的值(id字段配表会配置上此字段,在52~55行可以看到)。
- 第6~8行,中onConfigToArr函数遍历每一行并且进行”,”号分割,把每一行分割好的数据填充到一个ArrayList数组中返回出来,并且返回字段数量、数据数量(将excel导出csv用”,”分割,这个方法在下面会贴出来)
- 执行完第6-8行,第7行的data数据将会是
[0][0] = id, [0][1] = name, [0][2] = quality ......;
[1][0] = int, [1][1] = list|string, [1][2] = int ......;
......
[x][0]=1003, [x][1] = |改名卡, [x][2] = 5 ......;
- 第10~14行,基于反射机制将T(就是调用次函数传进来配表对应的解析实体)所有字段提取存放在字段数组fis中;
- 第17行,这里需要注意从2开始循环遍历有效数据,前2行为名称行、类型行这里排除(主要之前看到#注释行,在onConfigToArr函数中已经排除掉了,所以这里不用考虑了)。
- 第19行,实例化对象T(就是调用次函数传进来配表对应的解析实体)为configObj,用于接收下面for循环遍历数据中的每个字段值的值。
- 第22行,获取每一行的每一个字段的值,这个数据值将会通过对应的fis中存储的字段进行类型比较赋值
- 第23~57行,遍历每一行每个字段进行赋值操作。
- 第58-59行,数据解析完成全部保存在objDic字典中,最后将objDic填充到T中dataList字段中。即可
2.2.2、 init接口
在项目初始化时调用init即可将所有数据加载到内存中
public void init()
{
Debug.Log("********** 开始加载配表 **********");
this.ReadConfigData<ItemLib>("Item");
Debug.Log("Item load complete!");
// ... 其他数据表解析
Debug.Log("********** 加载配表结束 **********");
/*
// 调用测试
List<ItemLib> it2 = ItemLib.getDataList();
*/
}
3、遇到的坑
- Excel转编码为UTF-8的csv文件,应从文件->另存为->CSV UTF-8(逗号分隔)。而不是选择文件->导出->更换类型(这里导出默认格式为ANSI编码,在C#解析时中文会解析失败)
- 因为在C#中解析csv是通过英文的逗号”,”来分割,所以在配表字段中出现逗号,一定改为中文大写的逗号”,”;
- 编码!编码!编码!