第二篇文章是对流程的概述,从第三篇文章开始编辑打表的具体细节
策划配表习惯使用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这句话的意义就很重要了。切换当前路径
完结语
这套工具是我们项目组同事写的,这一套打表框架我我只贴了一小部分代码,省略了无数代码,本篇文章只梳理核心思想。在后面我们会说到游戏制作