Unity客户端框架之excel表解析

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、解析约定

  1. 需要同时支持服务器(c/c++/lua)和客户端。
  2. 支持的类型int32、int64、string、String[]、int[] (开发中会用到数组形式的配置数据)。
  3. excel转成csv,客户端、服务端解析配表都基于这个编码为UFT8的csv文件。将以”,”大写逗号来分割字段、以”|”来分割字段数组。
  4. excel配置表中第一行始终为字段名称、第二行为字段类型(第二条)、从第三行开始的第一个单元格内容为”#”字符的代表是注视行、非”#”字符的为正式数据部分;

1.1、实现思路

  1. 基于csv解析。首先的了解下C#反射反射机制、泛型等概念。
  2. Unity中利用python将csv中第一行变量名称和第二行变量类型解析出来,生成c# 实体文件;(当然你也可以在unity写个Editor来实现,我这里用python)。
  3. 有了c#实体文件,遍历csv文件中所有行,在利用c#中反射原理创建对象隐射c#实体文件。
  4. 最终将配表数据转成对象数据加载到内存中。

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支不支持代码段折叠,太费篇幅了。

  1. 第6行生成解析的实体文件名(个人喜欢是配表名称+Lib后缀结尾)
  2. 第57行~71行对实体文件增加3个方法,提供获取数据接口
  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);
    }
  1. 第3行,字典objDic通过key-value形式存储解析好的对象,key为csv中字段为id的值(id字段配表会配置上此字段,在52~55行可以看到)。
  2. 第6~8行,中onConfigToArr函数遍历每一行并且进行”,”号分割,把每一行分割好的数据填充到一个ArrayList数组中返回出来,并且返回字段数量、数据数量(将excel导出csv用”,”分割,这个方法在下面会贴出来)
  3. 执行完第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    ......;
  1. 第10~14行,基于反射机制将T(就是调用次函数传进来配表对应的解析实体)所有字段提取存放在字段数组fis中;
  2. 第17行,这里需要注意从2开始循环遍历有效数据,前2行为名称行、类型行这里排除(主要之前看到#注释行,在onConfigToArr函数中已经排除掉了,所以这里不用考虑了)。
  3. 第19行,实例化对象T(就是调用次函数传进来配表对应的解析实体)为configObj,用于接收下面for循环遍历数据中的每个字段值的值。
  4. 第22行,获取每一行的每一个字段的值,这个数据值将会通过对应的fis中存储的字段进行类型比较赋值
  5. 第23~57行,遍历每一行每个字段进行赋值操作。
  6. 第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、遇到的坑

  1. Excel转编码为UTF-8的csv文件,应从文件->另存为->CSV UTF-8(逗号分隔)。而不是选择文件->导出->更换类型(这里导出默认格式为ANSI编码,在C#解析时中文会解析失败)
  2. 因为在C#中解析csv是通过英文的逗号”,”来分割,所以在配表字段中出现逗号,一定改为中文大写的逗号”,”;
  3. 编码!编码!编码!

猜你喜欢

转载自blog.csdn.net/sszyc8/article/details/81205329
今日推荐