前言
配表在一个游戏里是一个很重要的部分,它能让策划通过数值来控制游戏很大一部分体验。所以配表在一个游戏里是必不可少的一个环节。最近我就在做一个配表工具,现在就写了一篇文章来介绍一下我这个打表工具的思路
正文
配表的关注点
- 方便
- 内存
- 健壮
初级版本
现在在我们客户端开发游戏,一般都需要在两个地方读取配置,unity中的c#使用,热更新lua里使用的。所以一个简单的配表工具,就是直接生成一份lua的table配表
table = {
key = {
value =
}
}
return table
用来在lua中读取。还有一份资源专门用来给c#读取的配置,这个格式很多比如json,xml,或者unity中的asset的资源。这样的可视化的好处是方便策划用来检查配置是否正确,也方便程序直接修改配表来更改配置的可以不用打表就可以测试功能,使用也比较方便,都有第三方的插件用来生成对应结构供运行时使用。但是这种的坏处就是内存占用比较高,一套配置在运行中中需要分别生成c#和lua两套结构,也容易被破解然后直接被获取应用配置。
升级版本
碰到问题,那我们就去解决这个问题,首先我们先解决这种可视化,能被用户破解修改的这个问题。我决定选择二进制文本来解决这个问题,因为二进制文件必须知道我们序列化结构,才能把二进制文本解析成我们代码所需要的结构。所以我们直接把配表生成二进制文件,然后在运行时,在反序列化成对应的类型(int,string,bool等),这样之后,我们的内存压力也变小了,因为二进制文件比json这种可视结构要小很多。而且我们还能按需序列化了,能占用更少的运行时内存。但是使用二进制去解析有个问题,就是如果生成二进制所使用的结构和解析二进制使用的结构不匹配,会出现解析失败。
三代版本
现在先解决上一个版本新出现的一个优化点,按需加载配置。先说一下这个概念吧,就是按游戏的进度,每次所需要使用的配置内容不会特别多,很多配置很有可能后面都不需要使用了,所以我们可以每次使用的时候就加载那一行的数据,这样我们就可以少序列化很多数据,减少运行时内存压力。在配表中,我们会有一个唯一key,用来读取某一行数据,那根据这个功能,我就生产了一张key对应当前行的二进制索引位置,从这个位置开始序列化这一行的数据。
最后解决序列化的问题
在解决序列化问题的时候,我决定使用protobuf的一部分特性,然后把那些需要定制化的结构,都在运行时候自己再解析,只支持一些基础类型,而且还能结构公用
message ConfigValue{
int32 int_value = 1;
string string_val = 2;
float float_val = 3;
bool bool_val = 4;
}
message ConfigRow{
repeated ConfigValue config_val = 1; //行数据
}
message ConfigIndex{
ConfigValue key = 1;
int32 star_index = 2;
int32 end_index = 3;
}
message ConfigTable{
repeated string config_title = 1; //表头
repeated string config_type = 2; //字段类型
string key = 3; //key的字段名字
repeated ConfigIndex config_index = 4; //key对应数据中的位置
}
message ConfigData{
repeated ConfigRow config_row = 1; //数据内容
}
结构如上,定义了一个configValue这个结构,它是用来保存基础类型,在我的规划里,我只需要知道int,string,float,bool这四种基础类型,List这样的数组通过定义一个特殊的切割符号,通过string导出使用。configRow这个结构就是用来导出一行的数据;configData就是结构就是这张配置的数据内容。
Configindex是代表每一行的唯一key和当前行在configData二进制中的位置。
Config_title代表的是表头,就是这个字段列表
Config_type代表的就是这个表头所代表的类型
示例
{ "configRow": [ { "configVal": [ { "intValue": 1 }, { "stringVal": "测试普通+伴随" }, { "intValue": 2001 }, { "stringVal": "111,0,47.5" }, { "floatVal": 10 }, { "stringVal": "1|2|3" } ] }, { "configVal": [ { "intValue": 2 }, { "stringVal": "测试普通+嘿嘿" }, { "intValue": 2004 }, { "stringVal": "888,255,47" }, { "floatVal": 33 }, { "stringVal": "4|5|6" } ] } ] }
{ "configTitle": [ "id", "des", "key", "coordinate", "testFloat", "TestListInt" ], "configType": [ "int", "string", "int", "vector3", "float", "listInt" ], "configIndex": [ { "key": { "intValue": 2001 }, "starIndex": 2, "endIndex": 64 }, { "key": { "intValue": 2004 }, "starIndex": 66, "endIndex": 128 } ] }
打出的内容如上所示
我们就可以通过获再configTitle中获取key知道key是第三个元素,并且它的类型是int,所以我们就能从configIndex里的intvalue取值,2001这个key的元素的在二进制里的索引和大小,然后序列化configRow得到相对应的值。因为我们的字段和类型都是一一对应的,所以当我们删除了字段coordinate的时候
{ "configRow": [ { "configVal": [ { "intValue": 1 }, { "stringVal": "测试普通+伴随" }, { "intValue": 2001 }, { "floatVal": 10 }, { "stringVal": "1|2|3" } ] }, { "configVal": [ { "intValue": 2 }, { "stringVal": "测试普通+嘿嘿" }, { "intValue": 2004 }, { "floatVal": 33 }, { "stringVal": "4|5|6" } ] } ] }
{ "configTitle": [ "id", "des", "key", "testFloat", "TestListInt" ], "configType": [ "int", "string", "int", "float", "listInt" ], "configIndex": [ { "key": { "intValue": 2001 }, "starIndex": 2, "endIndex": 50 }, { "key": { "intValue": 2004 }, "starIndex": 52, "endIndex": 100 } ] }
对应的索引下标就会删除,我们的二进制位置也就做了一次偏移。这样我们只需要在处理配表的时候做一次容错,找不到configtitle的时候,返回一个空的对象,那么在没有使用这个字段的时候不会因为字段的删除导致解析出错,影响到程序运行。
这样策划在打表的时候就不需要关心结构是否匹配,只需要无脑的把表打出来,然后程序在用的时候再对字段做出处理。
最后再说点
每家公司,每个策划组都有自己配表的方式,有的人因为一张表过大,喜欢分多个excel然后打表的时候整合再一起;有的人想要简化运行时做结构生成,想要在打表的时候就把结构也生成出来;所以需求千变万化,都是使用者的习惯来做定制化的,我只能表达我遇到的问题,所解决的问题。