乐趣区

关于python:游戏开发Python打表工具系列-第二篇-工程启动篇打表流程描述

第二篇文章是对流程的概述,从第三篇文章开始编辑打表的具体细节
策动配表习惯应用 excel,咱们打表指标也是 xlsm 和 xlsx 文件

开始打表前须要确认好打表工具目录在哪,能够在所有 Excel 配表同层级的文件夹内新建个文件夹命名 TableCreater

TableCreater 里有几个目录要辨别分明,首先是 python 脚本文件夹叫 Scripts,还有在运行工具期间生成的各种文件,用 Temp 文件夹存储,Temp 里有 PB_Python、PB_lua、Proto、Bytes 等文件夹,打表流程启动后生成的各种文件要放到对应文件夹中

Scripts 文件夹内要有 start.py、main.py、excelToCSV.py、export_proto.py、export_pb_lua.py、export_pb_python.py、export_bytes.py、export_bytesTables.py

Scripts 文件夹内的文件一看就是用来打表的各个流程啦。咱们能够在 start.py 的 main 函数中按步骤执行即可

尽管我的项目是从 start.py 开始启动的,但具体的打表流程咱们放到 main.py 中运行,start.py 的职责是监听用户输出信息并收集信息塞给 main.py 进入打表流程

如下图所示,start.py 的性能有 tab 键补齐名字,分号多个输出,只是输出 all 示意全副 excel 打表,以及 clear 清空 log 等操作。

如图所示,我输出了一个 115_资源总表,在 start.py 里监听输出并主动把全门路拼齐,当按下回车时,start.py 把【115_资源总表】这个名字传给 main.py 开始了打表流程,因为咱们输出了一个表名而不是 ”all”,因而到了 main 函数中天然不是全打表

import os
#读行模块
import readline
#主动补全模块
import rlcompleter
#监听用户输出模块
import userInput
import sys
#开发者自定义的零碎参数
import env_debug
 
def getAllExportExcelNames():
    inputStr = ""
    isAll = False
    if env_debug.isRuning:
        if len(sys.argv) > 0: 
            inputStr,isAll = userInput.getAllExportExcelNames(sys.argv[13])
        else:
            print("env_debug error")
    else:
        #readline 模块与 colorama 模块有抵触, 无奈一起应用
        py = os.path.abspath("TableCreater/Python27/python")
        tips = os.path.abspath("TableCreater/Script/tips.py")
        os.system("{0} {1} 1".format(py,tips))
 
        while True:
            inputStr,isAll = userInput.getInput()
            if inputStr == "exit":
                os.system("exit")
                break
            elif inputStr == "clear":
                os.system("cls")
                py = os.path.abspath("TableCreater/Python27/python")
                tips = os.path.abspath("TableCreater/Script/tips.py")
                os.system("{0} {1} 1".format(py,tips))
            elif inputStr == "error":
                print("command error")
            else:
                break
    return inputStr,isAll
 
 
 
#excelNames: 要导出的所有 Excel 表格名称
#isAll: 是否是全副导出
#isVersion: 是否是出版本
def export(excelNames,isAll,isVersion=False):
    import main
    main.run(excelNames,isAll,isVersion)
    if not isVersion:
        os.system("pause")
 
def isVersion():
    return len(sys.argv) > 0 and sys.argv[len(sys.argv) - 1] == "version"
 
if __name__ == '__main__':
    env_debug.switch("common")
    caller = sys.argv[1]
    if caller == "1": #惯例导表
        if not isVersion():# 显示用户输出
            excelNames,isAll = getAllExportExcelNames()
            if excelNames != "exit":
                export(excelNames,isAll)
        else: #间接导出所有
            excelNames,isAll = userInput.getAllExportExcelNames("all")
            export(excelNames,isAll,True)
            
    elif caller == "2": #C# 导表
        excelNames,isAll = userInput.getAllExportExcelNames(sys.argv[10])
        export(excelNames,True,isVersion())
 
    elif caller == "3": #本地战斗服务器专属导表
        excelNames,isAll = userInput.getAllExportExcelNames(sys.argv[8])
        export(excelNames,True,isVersion())
 

最下面有个 import env_debug 模块,这是咱们本人写的零碎变量定义文件 env_debug.py 文件里,这里有必要解释一下为何自定义一个文件

 
[第一步]:将 excel 文件转 CSV 并输入到 CSV 目录
start.py 执行 excelToCSV.excute()

为何要把 excel 转 csv,excel 文件蕴含了 windows 的很多库,文件特地大,但 csv 文件是纯文本文件,每行数据用逗号 ’,’ 分隔,存储空间小。

def execute(excelName):    
    xlrd.Book.encoding = "gbk"    
    excelPath = setting.dirPath_excel + excelName
    excel = xlrd.open_workbook(excelPath) #关上 Excel
    for sheet_name in excel.sheet_names():   #遍历子表, 一个 excel 可能有多个 sheet 页
        sheet = excel.sheet_by_name(sheet_name)# 拿到 sheet 数据
        #一般来说一个 excel 有多个页签,每一个页签对应一个 proto 文件,我的表名称写在第二行第一列
        tableName = sheet.cell_value(1,0)
        csvName = tableName
        maxCol = 0 #该 sheet 页最大列数
        for i in range(100): #获取以后 sheet 的最大列数
            if sheet.cell(3,i).ctype == 0:
                maxCol = i
                break
        filePath = setting.dirPath_csv + csvName + ".csv"
        fileObj = codecs.open(filePath,"wb")# 该代码会在目标目录创立文件,并设置读写格局
        fileObj.seek(0)# 从第 0 行开始写
        csv_writer = unicodecsv.writer(fileObj,encoding='utf-8-sig')#unicodecsv 是内部库,自行下载导入
        tableUser = getTableUser(sheet,endCol) #记录配表使用者,如果没有需要能够不记录,我的我的项目是服务器和客户端共用表,在数据名称的上一行记录该数据由 c 还是 s 应用
        csv_writer.writerow([tableUser,excelName.decode("gbk"),sheet_name])# 写在第一行
        rowCount = sheet.nrows #以后 sheet 最大行数
        for i in range(7,rowCount):# 正式开始遍历 excel 每个子表每行数据
            #每个子表每列数据
            for j in range(0,endCol):# 遍历该行每一列数据
                
                #此处把读取数据代码省略,读格子数据的代码为 sheet.cell(rowx,colx).value
                #把你读出来的数据拼接成正确的行
 
            csv_writer.writerow(rowDatas) #按行写入
 
        fileObj.close()
        #到此一个残缺的 XXX.csv 文件生成胜利,能够用 excel 关上查看该文件数据,提醒:关上的数据必须和 excel 是一个格子一个格子的,行数据不能在一个格子里

start.py 执行 export_proto.excute()

    def execute(csvName):            
        curCsvPath = setting.dirPath_csv + csvName + ".csv"
        csvfile = codecs.open(curCsvPath,"rb")
        csv_reader = unicodecsv.reader(csvfile,encoding='utf-8-sig')
        index = 0
        types = []
        titles = []
        for line in csv_reader:
            if(index == 0):
                canExport,msg = Parser.CanExport(line[0])
                if(not canExport):
                    debug.throwError("导出 proto 文件失败:{}.csv{}".format(csvName,msg))
            elif(index == 1):
                userSigns = line  #使用者标识
            elif(index == 2): #类型
                for i in range(0,len(line)):
                    if(Parser.CanUse(userSigns[i])):
                        types.append(line[i])
            elif(index == 3): #字段名称
                for i in range(0,len(line)):
                    if(Parser.CanUse(userSigns[i])):
                        titles.append(line[i])
            else:
                break
            index = index + 1
        csvfile.close()
    
        #code_block,code_struct,code_import = createcode_block(types,titles)
        code_block,code_struct = createcode_block(types,titles)
 
        code = m_code_template.format(code_struct,csvName,code_block,csvName,csvName)
 
        codefile = codecs.open(setting.dirPath_proto + "Table_" + csvName + ".proto","wb","utf-8")
        codefile.write(code)
        codefile.close()

生成的文件在 Temp/Proto 文件夹中

start.py 执行 export_pb_lua.excute(protoName)

调用 protoc-gen-lua.bat 并传入参数,

其实就是传入各种门路参数,以及 [第二步] 通过数据类型和数据名称构建的 Table_XXX.proto 文件门路

lua 版 pb 文件的输入门路等,最初 ret 如果为 true 代表生成胜利。

最终输入门路我写的是 Temp/PB_Lua, 文件名为 Table_xxx_pb.lua

这个 lua 文件就是能够间接在 Unity 工程中应用的 lua 版 pb,在工具的最初只须要把 Temp/PB_Lua 文件夹的文件全副 copy 到工程中即可。

        cmd = "{}\protoc-gen-lua.bat {} {} {} {}\Table_{}.proto".format(os.path.abspath(setting.protocPath_lua),
            os.path.abspath(setting.dirPath_proto),
            os.path.abspath(setting.dirPath_protoEnum),
            os.path.abspath(setting.dirPath_pb_lua),
            os.path.abspath(setting.dirPath_proto),
            protoName)
 
        ret,msg = debug.system(cmd)
        if(not ret):
            debug.throwError("{}生成 python 版 pb 失败 =>\n{}".format(protoName,msg))

目前来看,这些门路的传入程序必须固定,例如 proto 门路以及想要输入 lua 版本 pb 的门路,想要批改的话去改 protoc-gen-lua 工具的源码

start.py 执行 export_pb_python.excute(protoName)

调用 protoc.exe 并传入参数

和上一步有点类似,重点参数还是  [第二步] 生成的 proto 文件的门路

生成的文件名称为 Table_xxx_py2.py,我的工程输入目录为 Temp/PB_Python,目前 protoc.exe 只能输入 python、JAVA 和 C ++ 三种格局的 pb 文件,想生成 C# 等其余格局须要去网上找工具,应用形式和 lua 版本的一样

        cmd = "{}\protoc.exe --proto_path={} --python_out={} --proto_path={} {}\Table_{}.proto".format(os.path.abspath(setting.protocPath),
            os.path.abspath(setting.dirPath_proto),
            os.path.abspath(setting.dirPath_protoEnum),
            os.path.abspath(setting.dirPath_pb_python),
            os.path.abspath(setting.dirPath_proto),
            protoName)   
 
        ret,msg = debug.system(cmd)
        if(not ret):
            debug.throwError("{}生成 python 版 pb 失败 =>\n{}".format(protoName,msg))

[第五步]:csv 生成 bytes 文件导入 Unity 工程期待读数据
这一步过程比较复杂,代码中波及到屡次数据构建,先简略说思路

咱们提前把这个 python 文件的构造当做 str 写好,这个 python 文件是个模板,也就是上面模块一代码

咱们在模块二的逻辑代码中遍历 csv 的行数据,把行数据处理成一个 str 数据后塞入到这个模块一的 python 代码中,并把这段代码生成一个 py 文件期待第六步,运行生成的 py 文件

# 模块一!!字符串 m_code_template 是一段 python 文件代码,外面缺失了一些文件数据,比方文件门路、文件名称等,补全信息并且间接调用 python.exe 执行这段代码即可生成 bytes 文件
m_code_template = u'''
#! python2
#coding:utf-8
import os
import traceback
import sys    reload(sys)  sys.setdefaultencoding('utf8')
sys.path.insert(0, os.path.abspath('./TableCreater/Script'))
import Parser
import setting
import debug
sys.path.insert(0, os.path.abspath(setting.dirPath_pb_pythonEnum))
sys.path.insert(0, os.path.abspath(setting.dirPath_pb_python))
import Table_{}_pb2
 
def export():
    tableData = Table_{}_pb2.Table_{}()
    BYTES_PATH = {} +"{}.bytes"
{}
 
    bytes = tableData.SerializeToString()
    NEWFILE = open(BYTES_PATH,"wb")
    NEWFILE.write(bytes)
    NEWFILE.close()
 
try:
    export()
except:
    debug.error(traceback.format_exc())'''

上面是模块二代码

        #模块二 上面这段代码是补全 m_code_template 的信息,并生成一个用于转 bytes 的 python 文件
        
        for line in csv_reader:
                index = index + 1
                if(index == 0):
                    user = line[0]
                elif(index == 1):
                    userSigns = line
                elif(index == 2):
                    types = line
                elif(index == 3):
                    titles = line
                else:
                    code_row = createcode_row(line,types,titles,userSigns,tableName)
                    allRowStrs.append(code_row)
                    allRowStrs.append("\n")
                #     parser_code = parser_code + code_row + "\n"
                # print(index)
            
            #print("这里")
            parser_code = ''.join(allRowStrs)
            exportPath = "setting.dirPath_byte"
            if user == "client":
                exportPath = "setting.dirPath_byte_clientOnly"
            code = m_code_template.format(tableName,tableName,tableName,exportPath,tableName,parser_code)
            csvfile.close()
            c    odefile = codecs.open(setting.dirPath_code + "Table_" + tableName + ".py","wb","utf-8-sig") #创立一个 python 文件,并把数据写入到该 python 文件中
            codefile.write(code)
            codefile.close()

上面把模块二中数据构建的函数

def createcode_row(line,types,titles,userSigns,tableName):
    code = u"\tdata = tableData.datas.add()\n"
    for i in range(0,len(line)):
        if(Parser.CanUse(userSigns[i])):
            code = code + createcode_col(line[i],types[i],Parser.GetTitle(titles[i]),tableName)
    return code

贴代码有点不不便用户浏览,先这么整吧

def createcode_col(cell,dataType,title,tableName):
    code = u""if(cell != u""):
        if(Parser.IsArray(dataType)):
            if(Parser.IsStructArray(dataType)):
                #print("构造体数组")
                code = code + createcode_structArray(cell,dataType,title)
            else:
                #print("根本数据类型数组")
                code = code + createcode_baseTypeArray(cell,dataType,title)
        else:
            if(Parser.IsStructArray(dataType)):
                code = code + createcode_struct(cell,dataType,title,tableName)
            else:
                if dataType == u'JSON':
                    dataType = u'STRING'
                if dataType == u'STRING':
                    cell = cell.replace('\"','\\"')
                #根本数据类型赋值
                code = code + u"\tdata.{} = Parser.GetValue(u\"{}\",u\"\"\"{}\"\"\")\n".format(title,dataType,cell)
    return code

[第六步]:生成 Lua 读表脚本,业务开发调用该脚本读数据
[第五步] 负责把 csv 数据各种拼凑组合到一个 python 模板中,而后生成 py 文件,第六步就轮到调用该 py 文件啦

# 第五步中生成的 python 文件是可执行的,应用 cmd 命令执行该 python 文件即可生成对应的 bytes 文件
cmd = "\"TableCreater\\Python27\\python\"{}Table_{}.py".format(setting.dirPath_code,codeName)
        os.system(cmd)

这一步会生成咱们想要的 bytes 文件并输入到 Temp/Bytes 文件夹中,文件名字为 xxx.bytes, 这个 xxx 就是咱们后面的 csvName 或 protoName

[第七步]:生成对立的读表接口
应用 python 全自动生成一个 lua 文件用来治理所有的读表性能,因为一个工程的表切实太多了。手写太繁琐,该工具能够叫 AllTables.lua,当系统启动时,调用该对立入口,把所有表全副加载入内存,对立治理

此处只举两个表为例,别离是 Achievement 成就表和 Table_ItemInfo 道具表,AllTables.lua 代码全副由 python 批量生成,不是手写的,无论有多少个表都能够循环写入,业务开发的人如果想读成就表,则只须要调用 Table_Achievement.GetRowData(keyName)即可,之后依据框架设计走同步或者异步加载 bytes 文件并读取行数据,首次加载必定是要先把 bytes 数据按 KeyName 已 key、value 的形式存下来不便读取

_G.Table_Achievement = 
{
    Belong = "common",
    KeyName = "conditionSid",
    InitModule = function ()
        require "Logic/Table/AutoGen/TablePb/Table_Achievement_pb"
        TableCtrl.LoadTable("Achievement")
    end,
    Parser_Table = function (bytes)
        local table = Table_Achievement_pb.Table_Achievement()
        table:ParseFromString(bytes);
        return table.datas
    end,
    GetRowData = function (id)
        return TableCtrl.GetRowData("Achievement",id)
    end,
    GetAllRowData = function ()
        return TableCtrl.GetAllRowData("Achievement")
    end,
}
 
_G.Table_ItemInfo = 
{
    Belong = "common",
    KeyName = "id",
    InitModule = function ()
        require "Logic/Table/AutoGen/TablePb/Table_ItemInfo_pb"
        TableCtrl.LoadTable("ItemInfo")
    end,
    Parser_Table = function (bytes)
        local table = Table_ItemInfo_pb.Table_ItemInfo()
        table:ParseFromString(bytes);
        return table.datas
    end,
    GetRowData = function (id)
        return TableCtrl.GetRowData("ItemInfo",id)
    end,
    GetAllRowData = function ()
        return TableCtrl.GetAllRowData("ItemInfo")
    end,
}
 
#当系统启动时,调用 AllTables 的 RegisterModule
local AllTables = 
{Init = function (RegisterModule) #这个是零碎注册模块的中央
        RegisterModule(Table_Achievement,false)
        RegisterModule(Table_ItemInfo,false)
}

[额定常识]
咱们写好的 python 工程是可运行的,但其余开发人员不心愿关上工程去运行,因而咱们写一个 exportToClient.cmd 文件执行写好的 python 工程

@echo off 
echo "cd /d %~dp0 的作用是切换到当前目录"
cd /d %~dp0
"./export_fight_proto/Python27/python" "./export_fight_proto/start.py"
:end

打表工具只是出包流程中的其中一步,如果其余 cmd 文件要调用 exportToClient.cmd, cd /d %~dp0 这句话的意义就很重要了。切换以后门路

完结语
这套工具是咱们项目组共事写的,这一套打表框架我我只贴了一小部分代码,省略了有数代码,本篇文章只梳理核心思想。前面咱们会聊到对于游戏,手游等等的开发

退出移动版