[游戲開發]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是一個格子一個格子的,行數據不能在一個格子里

[第二步]:讀取csv的數據類型行和數據名稱行,使用這兩行數據生成proto

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文件夾中

[第三步]:調用protoc-gen-lua.bat生成lua版pb文件
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工具的源碼

[第四步]:調用protoc.exe生成python版pb文件
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這句話的意義就很重要了。切換當前路徑

完結語

這套工具是我們項目組同事寫的,這一套打表框架我我只貼了一小部分代碼,省略了無數代碼,本篇文章只梳理核心思想。后面我們會聊到關於游戲,手游等等的開發


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM