简介
在游戏开发中,配置表是策划与程序员之间针对功能模块开发而搭建的桥梁。配置表在游戏开发中扮演非常重要的角色。策划需要向配置表填入各种数据,以完成功能数值的配置;程序员需要在项目代码中读取配置表,根据配置表和需求决定的业务逻辑来开发功能模块。
本文想探讨接触过的两个项目的配置表功能设计。在本文中,我不会泄露项目相关的具体信息,仅针对方案进行讨论。
两种配置表方案简介
此处先以表格的形式,列举两种方案之间的异同:
条目 | 大型SLG手游 | 小型MMO手游 |
---|---|---|
策划配置方式 | 使用Excel配置 | 使用Excel配置 |
代码读取方式 | 将Excel文件中的内容转换成Csv文件 | 将Excel文件中的内容转换成Json文件 |
读取后存储位置 | 在项目启动前,先读取Csv的内容到内存中,转成二进制文件写入Zookeeper,在项目启动时加载Zookeeper中的配置表信息到内存 | 在项目加载时读取Json到内存中 |
热更新方案 | 修改Zookeeper中的二进制文件,通过Zookeeper发布订阅机制,分布式系统中各个系统从Zookeeper中读取新的配置表信息 | 先修改Json文件,然后读取Json文件到内存中 |
配置表检查 | 在读取Csv到内存后,编写检查代码,检查内存对象之间的相关逻辑依赖 | 无 |
大型SLG手游
这是一个比较大型的SLG手游项目,使用了分布式的系统,服务器数量众多,因此需要一个统一的中心服务器来管理所有的配置信息。这个项目使用的是Zookeeper进行配置表的存储。
策划配置方式
策划使用Excel进行配置。每一列包含中文名、英文名、key类型(标记客户端、服务器或是客户端和服务器都用,且标记是否是key)、字段类型(数据data还是文字text)。
一张表最多只能有两个key,一个主key,一个副key。
比如,如果有一个可以领取宝箱的活动类型,活动可能有多个id,以满足不同的活动包装;每个活动都有多个宝箱。那么这张配置表的主key是活动id,副key是宝箱id。
读取配置表
首先将Excel文件中的内容转换成Csv文件,这一步会将客户端的配置列移除,仅保留服务器用到的配置列。然后使用Java代码读取Csv文件,将Csv文件中的数据构造成Java对象,然后再将Java对象序列化成二进制文件,上传到Zookeeper中的某个节点之下。
graph LR A[Excel文件]-->|Python转换|B[Csv文件]-->|Java代码读取|C[Zookeeper节点下]-->|服务器启动读取Zookeeper|D[服务器内存对象]
服务器启动时会从Zookeeper的节点中加载配置表文件。
在后续的开发过程中,发生过因为策划失误,修改了Excel后忘记生成Csv,而导致Excel文件与Csv文件不一致的情况。在这个项目中,Csv并不是读取配置表过程中最后的输出产物,仅仅是一个中间产物,所以内部也讨论过是否可以直接从Excel文件中读取到内存中的可能。
热更新方案
当服务器启动读取完Zookeeper中的配置表后,订阅Zookeeper中配置表节点的变化事件。
当配置表Excel文件被修改后,依次执行读取流程到上传到Zookeeper节点,此时触发节点变化事件,Zookeeper会将变化事件发布给所有订阅此节点的服务器,服务器接收到配置表节点变化后,再次加载新的配置表文件信息。
配置表检查
当Csv读取到内存后,会对每一场表生成一个Java表管理对象,Java表管理对象与配置表一一对应。同时,这个Java表管理对象中,还可以进行多张配置表之间的逻辑校验。比如奖励表中的道具,必须在道具表中有配置,否则就会报错。
小型MMO手游
这是一个比较小的MMO手游,服务器进程数量比较少,配置表的数量也不多。因此它不需要使用类似Zookeeper那样的配置中心,只需要各个服务器进程读取相同内容的Json文件即可。
策划配置方式
策划使用Excel进行配置表的编辑。表头包含中文名,英文名,类型(数字,或者any任意结构)。 一张表必须要有数字id,且必须列在第一列中。其他的部分不做强制要求。
读取配置表
Excel文件会转换为Json文件。 在代码中,服务器会加载所有的Json文件。
程序员一般不需要针对某一张配置表单独编写解析代码。每一张表都有一个id,其他字段都默认读取为Golang的interface{}类型,每一张表都能读取成统一的格式,即:表名-id-其余各列数据。如果这张表是一张只有一个key的表,那么可以直接通过表名+id查找对应的数据。
但如果是不止一个key的表,那么可以在默认加载之后,再新开辟内存空间,使用自定义的结构进行存储。
热更新方案
策划修改配置表Excel文件后,转成Json文件,然后手动上传到服务器中。 通过RPC的方式,向网关服务器发送“重新读取配置表”消息,网关通知服务器集群中的所有服务器进程,重新读取Json文件到内存中,替换之前的配置表内存数据。
配置表检查
没有配置表提前检查,仅在使用配置的时候抛出err错误。
优缺点
类型 | 大型SLG手游 | 小型MMO手游 |
---|---|---|
优点 | (1)热更新方便,一次发布到Zk上之后,所有服务器都更新 (2)有配置表检查,在加载配置表时校验配置错误信息 | (1)所有表都有默认取值的方法,方便开发 (2)Excel转Json后,Json文件方便人类阅读 |
缺点 | (1)Excel生成的Csv可读性差,维护性差,不如XML,Json带有自己的结构信息 (2)开发繁琐,需要为每一张表定义类,并且预测配置表的关联,有可能会因为过度关联导致自由度下降 (3)在这个项目中,一个Excel对应一张配置表,导致Excel表数量过多,难以分类维护 | (1)没有配置表校验,配错值只能等业务运行时抛出Error (2)热更新时,需要到所有服务器手动上传配置表json文件,然后再RPC发送到服务器执行配置表json文件的重新读取。 (3)策划需要多维护一份Excel配置表生成客户端、服务器的不同Json的配置文件 |
配置表功能的一些思考
接下来是在两个项目中,和不同类型的策划沟通,使用完全不同的编程语言和设计方式开发之中,自己进行的一部分思考。
所有的值一定要灵活配置吗?
在SLG游戏中,由于是一个Java项目,团队里开发过程中非常忌讳“魔法数字”,如果功能要求不会改变某个值,领导一般会要求大家在枚举类或者常量中新增这个功能专属的常量。这本身没有什么问题。但是有时候问题出在策划身上,策划设计的时候并不清楚某个值是否有改变的可能。为了规避这种可能,我们开发时几乎将所有的可变的值,都做成了配置表中读取全局变量。
这会带来一个问题:是否有些值是不存在改变的可能性的,或者说,改变可能很小? 比如,有一个活动是领取2个宝箱,而且UI设计的时候已经设计好了两个宝箱在游戏屏幕中摆放的位置,那么如果这个值交给策划配置,策划也不敢去修改这个值。那么这个值就属于“没有必要放权给策划配置的值”。
你可能会说,如果下次开活动,策划要配置3个宝箱,那还是要改代码,不是吗?没错,但是此时客户端,美术UI都要修改,到时候QA还是要统一测,这时候服务器一起跟进修改一个写死的魔法数字,对整条链路上的人都没有影响:策划还是要增加第三个宝箱的奖励配置、美术UI要设计三个宝箱在屏幕中的比例,QA还是要完整跑一遍活动。因此就算这个宝箱数量的值交给策划配置,也不会让这一切有什么优化,而更有可能的是,由于没有服务器开发的参与,策划配错了值,回过头来还是服务器这边查问题。
配置表的key的约定
我们还是以刚才的例子,一个活动可以配置多个宝箱,并且同时可以开启多个活动。有两个活动,id为1和2,每个活动会开3个宝箱,id为1,2和3。
在SLG项目中,它的Excel配置表如下:
活动id | 宝箱id | 宝箱奖励 |
---|---|---|
allmainkey | allchildkey | all |
1 | 1 | xxx |
1 | 2 | xxx |
1 | 3 | xxx |
2 | 1 | xxx |
2 | 2 | xxx |
2 | 3 | xxx |
在这里,allmainkey
是指这张表的主key是活动id,allchildkey
是指这张表的副key是宝箱id。并且必须要按照这种格式配置,才能导出正确的Csv文件,执行后续的操作。
如果一张表需要三个key,则在目前的条件下,由于没有类似
allthirdkey
的约定,所以第三个key无法被标识。
在MMO项目中,它的Excel配置如下
id | 活动id | 宝箱id | 宝箱奖励 |
---|---|---|---|
101 | 1 | 1 | xxx |
102 | 1 | 2 | xxx |
103 | 1 | 3 | xxx |
201 | 2 | 1 | xxx |
202 | 2 | 2 | xxx |
203 | 2 | 3 | xxx |
这张表在功能逻辑上,只有一个key: id。这个id的生成逻辑是: 活动id * 100 + 宝箱id。如此依赖,只要活动和宝箱没有重复,则id也不会有重复。并且也解决了上面的那个问题:如果需要三个key,则只需要往后加新的逻辑id列,然后对id的生成规则进行修改即可。
在SLG项目中,表的结构固定了只能拥有两个key,一主一从,导致大家不得不按照固定的写法去进行配置。但在MMO项目中,它更注重于逻辑上的设计,使用体验也更加灵活。
配置表需要注释吗?
先说结论:配置表需要注释,但是不应该写在配置表里。
在策划的工作过程中,策划不断修改配置表的值,以达到自己的设计目的。在此过程中,存在多次与程序商讨修改表结构/字段含义的过程。由于配置表并不直接完成功能,所以策划很难对配置值的使用方式有清楚的认知。在需求被修改、程序开发时提议修改优化、或是最后测试时遇到问题时,我们可能会急于完成功能而忽略策划表另一页的注释页,导致注释与实际效果不符合的情况。
我们终究是从代码层面来排查配置表的值是如何被使用的,因此我认为代码中的注释总体优于策划表的注释。
配置表究竟由程序员设计,还是由策划设计?
在大部分情况下,由策划设计。原因是功能设计者在设计的时候,对自己设计过程中的敏感数值有深刻的印象。因此配置表应该根据这些数值进行规划和设计。
但在部分情况下,如在之前的功能上进行所谓的优化,策划无法对旧有功能有全面认知的时候,容易出现新增重复功能的表、或是直接“复用”、“拓展”之前的表。有时这会让程序员感到莫名其妙。因此在这种情况下,程序员需要仔细倾听策划的需求,然后对旧有功能进行整理,商讨出新的解决方案。在这个过程中,可能是程序员进行策划表的设计。
总结
配置表是游戏开发中不可或缺的一部分,但是目前还找不到比Excel更方便的配置表管理编辑工具。策划可以用Excel做很多方便的计算,但是到了程序员这里,读取Excel之后的步骤,每一步都值得探讨。读取如何更高效、流程如何更健壮,更不容易出现人为的失误、热更如何更方便快捷、结构如何更简单易懂,易于维护。
我的经验如下:
- 从Excel到内存中,最好只有一个中间产物。在SLG项目中,由于存在
Excel转Csv
和Csv读取后转二进制文件
这两个步骤,导致每一次读取出错,都要排查Excel和Csv两个文件。、 - 热更新的功能,最好只有两个步骤:打包和上传。打包是指将Excel转成最后服务器可读取的文件,上传是指将生成的文件传入Zookeeper或者其他的配置中心,由配置中心通知服务器进行热更新操作。而不是由人工上传,人工执行热更新。
- 一张表只能有一个key,但是可以有多个逻辑上的id,这样可以保持一定的多key自由度,且可以通过唯一key找到某一行记录,方便客户端-服务器之间通信。
- 配置表不需要注释,只需要用简短清晰的词汇,对某一列的功能和使用场景表述清楚,但是程序代码中要对使用配置表的具体位置进行一些描述(当某些字段有特殊枚举含义/需要除以100转成小数之类)。、
- 配置表优先让策划进行设计,只有设计配置表的人,才知道功能的具体效果。(这个过程并不是要求一定要让策划把完整的策划表设计完,而是要求策划要尽可能全面的列出功能中所需要配置的字段。)如此一来,程序员才能对功能开发更有把握,可以从需要的字段中窥探策划的真实设计方向。