Home Page
Search
\begin{md} # 游戏配置表设计注意事项 > 写作中.... 在游戏开发和维护的过程中,配置表承担了非常大的功能。按照功能来分类, 有服务器配置表,客户端 UI 配置表,道具配置表,关卡配置表等非常多种类。 游戏的几乎所有功能都是通过配置来进行驱动,任何一个大中型的游戏,至少有上百个配置表。 策划一般使用 Excel 工具维护策划表,程序使用导表工具转化而来的文本或者二进制配置。 如下示意图描述了这种关系。 ``` 策划 ----> 配置.xlsx -----> 配置.lua -----------> 服务器/客户端程序读取 | | |----> 配置.py -------| | | |----> 配置数据库 -------| | | |----> 其他类型 ---------| ``` 文中提到的游戏程序同时指代游戏服务器程序和游戏客户端程序。 项目的维护过程中,因为配置错误或者异常导致的 bug 非常常见又难以排查。 在这个文章中,将结合实际的开发经验,对配置表的设计和维护中会遇到的场景提出几个解决方案。 下面提到的内容主要来源于开发手游和端游的经验及见闻。 ## 场景 1:客户端和服务器的公共配置如何同步 项目中的配置按照使用的位置来区分,大概分为如下 3 类: * **纯客户端配置**,例如客户端 UI 界面的一些展示规则 * **纯服务器配置**,例如服务器的起服配置,抽奖算法的一些属性配置,随机掉落规则等 * **客户端服务器公共配置**,例如一些活动的开启结束时间,抽奖奖励的内容等 如果是纯客户端的配置需要更新,可以依靠客户端包更新方案(分为热更和重启更新)进行, 一些手游在登录之前会有一个加载,就是在从服务器拉取新的配置和其他资源。 如果是纯服务器配置需要更新,直接更新服务器配置即可(分为热更和重启更新)。 如果是公共配置需要更新,其实就是同时进行上面的两种更新,但是需要注意的是更新的顺序关系, 一般来说,步骤如下。 1. 客户端更新内容推送到 CDN 2. 更新服务器配置 3. 客户端连接更新网关,网关推送客户端资源更新链接,客户端更新 如果是非重要配置,那么会等玩家下次登录时执行上述流程。 如果是重要内容更新或者严重 Bug 需要修复,则强制将玩家踢出游戏,进行重新登录。 需要注意的是,上面只是简单说明更新步骤,正常运营的游戏,一般来说更新步骤会非常复杂, 包含至少十几个流程,如内网验证,外网预发布服务器验证,外网测试服务器验证, 外网正式服务器灰度发布,外网正式服务器全量发布等。 ## 场景 2:配置版本管理 配置的版本管理也是非常重要的一个内容,在日常开发过程中,策划同事对配置表会进行很多的调整, 测试同事也会对配置进行频繁修改来验证代码逻辑和功能。将配置纳入版本管理至少有以下几个好处。 * 在配置出错影响正常的开发和发布时,进行回滚。 * 配置内一般没有注释,可以通过查看提交日志,理解某个配置的功能(策划离职,给新策划讲解时比较有用)。 * 在出现配置错误且难以排查问题时,通过二分法,快速确定首个出错的配置版本(这个还是比较常见的)。 ## 场景 3:无效配置如何排查 这个场景极为常见。很多游戏在迭代过程中会产生非常多的活动和道具,很多道具可能已经被废弃, 但是活动奖励中仍然配置了这些道具,并且有些道具和无效道具的配置放在同一个表格中。 针对这种方案,可以有如下几种处理方法。 ### 方案 1 不区分有效道具和无效道具,在代码中实际需要读取配置的时候再检查配置的有效性。 这个做法是很多项目的常见做法。原因很多: 1. 策划经常需要修改配置,不愿意在配置废弃时删除配置 2. 配置中一般有些字段表示是否使用这个配置,例如活动的开启结束时间,如果当前时间在区间内, 表示开启,否则表示未开启,程序一般依赖这类字段来识别配置是否起效。 3. 当时设计的时候没有考虑废弃,功能开发很久后需要废弃一些配置,策划和程序都不愿意对配置或者逻辑进行大改。 基于这些原因,这个方案成了比较常见的处理方案。但也是最难排查问题的方案,在实际项目中, 被这种做法折磨过很多次,例如废弃了一个配置但是忘记修改了废弃标志字段, 重新启用了一个废弃配置但是忘记修改废弃标志字段,读取配置的代码忘记检查废弃配置字段等等。 ### 方案 1 道具配置表使用一个字段表示是否启用,不启用按照废弃处理。在导表时或者程序读取配置时, 不读取设置了这个字段的配置,这样子就可以保证: * 读取这个道具的配置时立刻出错,提早暴露错误,避免出错很久后才发现问题 * 构造这个道具时时立刻出现错误,避免了发放错误道具和后续回收操作 相比于在程序中不读取配置,更倾向于在导表时不生成对应的配置,这个方案的优势更大: * 弃用配置不导表,可以减少配置的大小,加快程序读取配置的速度,降低内存使用量 * 可以肉眼比较策划配置表和导表内容,快速查错。如果选择在程序中不读取废弃错误到内存, 那么内存的配置和策划配置表的差异很难进行比较,对后期排查问题会带来很大困难 * 减少游戏程序中无关的逻辑。如果后期配置变化频繁,可能需要经常修改程序来适应配置表变化, 如果将这个功能放在配置表中,既可以降低游戏程序读取配置的逻辑复杂度,也可以将相关代码都统一维护在导表工具中。 这种方案比较简单,但是存在一些问题。如果粗心将一些道具错误的标记成了废弃, 那么真正发现问题的时候大概率是因为程序报错,做不到在服务器启动时发现错误配置。 ### 方案 2 在加载奖励道具表之时检查道具的有效性,这个方案可以保证后续在使用奖励道具表时,相关的道具都必然是有效的道具。 但是这种方案也存在局限性。假设有下列例子:有一个奖励道具表, 里面的奖励有三种道具, 分别是皮肤,武器,贴纸。这三种道具各自有一个配置表。启动游戏程序的时候,加载奖励道具表, 这个时候会检查奖励道具表中所有道具的有效性,在游戏允许过程中,如果发生了一次热更新, 更新了皮肤配置表,皮肤配置表中错误地将一个已经被配置在奖励道具表中的皮肤错误地标记为了废弃, 这个时候就破环了奖励道具表的保证:所有道具一定是有效的。 为了解决这个问题,可能我们会想,能不能对所有配置表建立一个依赖关系,在发现配置表更新之后, 对依赖关系进行拓扑排序,自动更新所有直接和间接依赖它的配置表,自动进行链式更新。 ```txt 道具配置表 | |--> 皮肤配置表 | |--> 武器配置表 | |--> 贴纸配置表 ``` 例如这个例子中,皮肤配置表更新了之后,自动更新依赖它的道具配置表。这个方案是可以的,但是限制很大: 1. 依赖关系不能太多,层次不能太深。不然如果更新了一张基础的配置表,会导致非常多的配置表和代码热更,无法控制影响范围 2. 不能存在循环依赖的配置表,包括直接循环依赖和间接循环依赖 3. 需要策划有意识地建立依赖关系,如果忘记了建立依赖关系,那么错误排查又是一个非常麻烦的事情 由于这几个原因,这个方案并不合适。但所幸的是,实际项目中一般不会出现三层以上的配置表依赖关系。 在这个场景下,方案 1, 2, 3 可以互相配合,降低无效配置出错的可能性。 ## 场景 3:设计配置检查工具 ## 场景 5:配置热更新 ## 场景 6:区分在用的配置和废弃配置 ## 场景 : 配置表拓展 假设项目要开发一个武器系统,游戏中的武器含有品级的概念,分为 5 个档次, 分别是白色装备,绿色装备,蓝色装备,红色装备,橙色装备。在设计配置表的 时候很自然会设计为如下的格式 | 品质 | QualityType | | ----- | ---- | | 白色 | 1 | | 绿色 | 2 | | 蓝色 | 3 | | 红色 | 4 | | 橙色 | 5 | 按照品质高低将 QualityType 设计为连续的数字有很多好处,比如方便不同品质进行比较。 在数据库中存储一个武器的品质也必定是存储 QualityType 这个数字而不是一个字符串表示的品质。 那么,假设后期项目要求在蓝色品质和红色品质之间插入一个紫色品质的话,怎么对这个配置表进行拓展呢? 最简单的修改方案自然是直接插入,后面的 QualityType 后移。新配置表如下。 | 品质 | QualityType | | ----- | ---- | | 白色 | 1 | | 绿色 | 2 | | 蓝色 | 3 | | 紫色 | 4 | | 红色 | 5 | | 橙色 | 6 | 但是如果这些品质字段已经广泛的应用到了项目的各个部分,成为了最基础的配置。 甚至使用这套品质表示的武器已经投放到了运营中的项目,那么最简单的修改方案就 显得风险极高。因为这个修改导致的不兼容不只是发生在代码中,还有玩家信息数据库中。 老玩家的数据都是以旧的 QualityType 进行表示的。 如果想不修改旧的 QualityType,那么考虑第二种方案,新建一个映射关系,配置表如下。 | 品质 | QualityType | RealQualityType | | ----- | ---- | ---- | | 白色 | 1 | 100 | | 绿色 | 2 | 200 | | 蓝色 | 3 | 300 | | 红色 | 4 | 500 | | 橙色 | 5 | 600 | | 紫色 | 6 | 400 | 在这个新的配置表中,增加的紫色品质的 QualityType 是 6。同时新增了一列 RealQualityType, 表示真正的品质大小,需要比较不同品质优劣时按照 RealQualityType 进行比较。 同时还将不同 RealQualityType 的值增加的幅度调整为 100,为了以后中间再插入其他品质预留位置。 假设后期想在红色和橙色品质之间插入一个黄色品质,那么可以按照下面的方式进行拓展。 | 品质 | QualityType | RealQualityType | | ----- | ---- | ---- | | 白色 | 1 | 100 | | 绿色 | 2 | 200 | | 蓝色 | 3 | 300 | | 红色 | 4 | 500 | | 橙色 | 5 | 600 | | 紫色 | 6 | 400 | | 黄色 | 7 | 550 | 按照这种方式进行修改,只需要调整一下比较品质的代码即可,QualityType 的大小不再代表品级高低, 这一功能由 RealQualityType 进行承担。 回到开头,如果让你重新这个系统,那么在一开始,不让 QualityType 按顺序递增 1 而是一个更大的值 或许就是更好的方案。在设计设计,可以将配置表设计成这个格式。 | 品质 | QualityType | | ----- | ---- | | 白色 | 1000 | | 绿色 | 2000 | | 蓝色 | 3000 | | 红色 | 4000 | | 橙色 | 5000 | 这样子可能初看起来不太直观,但是增加了拓展性。后续要增加紫色和黄色品质,也非常容易。 | 品质 | QualityType | | ----- | ---- | | 白色 | 1000 | | 绿色 | 2000 | | 蓝色 | 3000 | | 紫色 | 3500 | | 红色 | 4000 | | 黄色 | 4500 | | 橙色 | 5000 | ## 场景 : 道具 ID 分配 在游戏内部,一般使用整数做为道具 ID,道具 ID 分配需要注意以下几个问题。 使用 ID 的前几位表示道具类型,可以使用前 4 位或者更多位表示类型。 例如在一个游戏中,使用前两位表示各个大的系统,如皮肤和武器。使用第三位和第四位表示武器系统的子武器类型, 如匕首,棍子。比如我们使用 10 位数表示道具ID,现在需要给武器系统的两种匕首分配皮肤ID,那么可以设计如下。 | ID | 描述 | 其他... | | --- | --- | --- | | 1001000001 | 古代匕首-皮肤1 | ...... | | 1001000002 | 古代匕首-皮肤2 | ...... | | 1001000003 | 古代匕首-皮肤3 | ...... | | 1002000001 | 精致匕首-皮肤1 | ...... | | 1002000002 | 精致匕首-皮肤2 | ...... | | 1002000003 | 精致匕首-皮肤3 | ...... | 在这个表中, ID 的 前两位数字 "10" 代表系统类型为“武器”, 第三位和第四位数字的 "01" "02" 代表 武器子类型 “古代匕首” 和 ”精致匕首“,最后六位就代表具体的皮肤 ID。 使用类似这种方案,可以通过 ID 快速判断出道具所属的对应系统。 使用这个方案时,需要注意几点: 1. 不是使用 0 作为开始数字,因为程序如果在读入 ID 时忽略前缀 0,那么最后读入的 ID 就不是 10 位, 后续在使用 ID 时需要考虑将数字位数补齐。 2. 在设计时,需要为 ID 的各个字段分配足够的位数。例子中使用 2 位数字作为系统编号,对于一些大型 游戏,可能是不够的,如果游戏后期系统超过了两位数,就需要对 ID 进行扩展,这会是一件极为艰巨的任务, 因为代码中可能很多地方可能将系统字段是两位数字当做了基础规则和隐藏条件。这些 ID 也有可能已经被各个 系统广泛使用,甚至写入了数据库。如果需要拓展 ID,牵扯到的范围会非常之广,也会造成很多直接或者隐藏的 bug (经历的两个项目都拓展过 ID,所以这是经验之谈)。在资源充裕的情况下,尽可能为每个字段分配足够的长度。 ## 需要认识到的问题 1. 配置表之间天然存在依赖关系 ## 配置表设计原则 1. 尽量不要将有效配置和废弃配置放在一起 2. 尽量不要在内存中动态生成配置 3. 读取到内存中的配置尽量保证只读,不要做任何修改 4. 配置表之间要在维护依赖关系的同时减少依赖关系 \end{md}
Home Page