一、概述
游戏开发中,会使用到很多配置文件。适当的利用配置文件,可以有效实现程序设计的灵活性,避免对程序功能的不断修改,降低程序开发人员与策划人员之间的沟通成本,提高效率。对策划人员来说,也可以方便的进行数值测试,以达到设计目的。
二、问题
但随着项目的推进,配置文件的数量以及需要配置的项目越来越多,维护变得越来越困难,出错几率就会增加。当然,通过自动化配置、自动容错检查等方法可以最大限度地降低人工配置量,防止出错,但对于不便实现自动化配置的文件以及配置文件数太多时还是存在未知的风险,俗话说物极必反,事物总有两面性,在享受配置文件给我们带来的好处时,也需要考虑可能会产生的问题,无节制的使用配置文件终究会带来不可控的结果。另外一个方面,配置项目的增多,通常意味着程序中更多的逻辑处理,会增加程序的复杂性,不易于理解,也容易导致程序BUG。因此,对哪些内容可以进行配置,哪些内容不必要进行配置,我们需要权衡利弊,仔细斟酌,做出正确选择。
其实写这篇文章,是因为最近在项目中遇到的一个问题,下面的讨论也会从这个问题出发,通过对该功能的讨论,阐述我对配置文件的一些看法。
需求场景:设计游戏中的BUFF系统,BUFF可以导致改变角色某些状态的持续效果,其处理系统包含了三个方面的功能。第一个是BUFF的基础配置,第二个是BUFF的生成,第三个是BUFF的生效。要求从技能或者武器上都可以配置这些BUFF,攻击时产生相应的效果,只要实现了BUFF的模块化,就可以方便地将之配置在任何地方。
这里为方便讨论,只列举三个异常状态
石化:攻击时,有一定概率使敌人进入石化状态,石化,见文知意,会让敌人做不出任何动作,解除时会造成一定伤害
破甲:攻击时,有一定概率使敌人进入破甲状态,破甲指随机脱掉对手一件防具
诅咒:攻击时,有一定概率使敌人进入诅咒状态,会减少敌人的力量,智力属性
下面介绍项目中当前的BUFF基础配置:
<buff id="1" kind="2" name="石化" type="1" targets="1" coverType="0" moveLimit="1" attackLimit="1" faceLimit="1" hitLimit="1">
<level id="001" duration="1500" procs="0.01" superimpose="0" interval="0" attack="0" value="0" />
<level id="002" duration="1600" procs="0.01" superimpose="0" interval="0" attack="0" value="0" />
</buff>
<buff id="2" kind="2" name="破甲" type="1" targets="1" coverType="0" moveLimit="1" attackLimit="1" faceLimit="1" hitLimit="1">
<level id="001" duration="1500" procs="0.01" superimpose="0" interval="0" attack="0" value="0" />
<level id="002" duration="1600" procs="0.01" superimpose="0" interval="0" attack="0" value="0" />
</buff>
<buff id="3" kind="2" name="诅咒" type="1" targets="1" coverType="0" moveLimit="1" attackLimit="1" faceLimit="1" hitLimit="1">
<level id="001" duration="1500" procs="0.01" superimpose="0" interval="0" attack="0" value="0" />
<level id="002" duration="1600" procs="0.01" superimpose="0" interval="0" attack="0" value="0" />
</buff>
对各个配置项的解释:
Buff:
Id: buffID
kind:效果种类(1-增益效果 2-减益效果)
type:类型(1-持续作用 2-每间隔一段时间作用一次)
targets:buff默认作用目标(可用值:1-被技能击中的对象 2-自己; 格式:多种对象使用逗号分割)
coverType:可覆盖类型(可用值:0-不能覆盖 >0-可覆盖相同值的buff)
moveLimit:是否限制移动(0-否 1-是)
attackLimit:是否限制攻击(0-否 1-是)
faceLimit:是否限制朝向(0-否 1-是)
hitLimit:是否限制被击动作(0-否 1-是)
level
id:等级
duration:基础持续时间
procs:基础触发几率
superimpose:是否有伤害叠加效果(可用值:0-否 1-是)
interval:类型为2时的间隔时间(单位:毫秒)
attack:基础攻击力
value: 配置影响的属性值(比如力量加多少)
refreshDuration:是否刷新持续时间(superimpose设置为1时,此属性无效)
上述配置十分高大上,几乎列出了关于BUFF的所有基础属性,一看非常全面,只要程序设计人员正确的处理了上面所有的配置项,策划人员就可以按既定规则随意配置,但这只是一个理想化的设想,很难达到设计者的目的:
a、配置项多,增加了使用者的理解难度,提高了时间成本(特别对初次使用这个配置表的人来说,看到这么多配置项,难道不会感觉痛苦?)
b、不利于扩展。如果加入新的buff类型,有可能会增加新的配置项,即使其他buff并不需要这个配置项。增加新配置项也意味着有可能影响到原来的BUFF类型的功能,不利于程序的稳定。
c、不利于面向对象的程序设计。我觉得最大的问题也在此,虽然从需求出发存在三种不同类型的BUFF,但对系统来说,却只是在无差别的处理一种BUFF,它们的不同只是通过冰冷的id以及其他配置项的不同值来体现,对这些配置项的处理是一种面向过程的方式而不是面向对象(对一个大型系统,我认为面向对象的处理方式当然是要占绝对优势的,我们可以脑补一下那种函数式的、面向过程的,一大堆if...else...语句所带来的抓狂)
说到面向对象的程序设计,这个概念对IT行业的人来说,应该是耳熟能详了,但真正的把这个思想运用到实际的项目开发中的人,恐怕不算太多吧(包括我在内,也不是任何时候都会想到这个概念),其实很多时候,我们如果从对象的角度来看待程序,事情就会变得简单得多,程序不再是干瘪的一行行代码,它会富有生命力,因为面向对象实际上更接近自然语言,更接近人的思维方式。
回到上面的配置上来,通过对三个BUFF的功能描述,我认为可以剔除一些配置项目,下面列出简化后的配置表:
<buff id="1" name="石化">
<level id="001" duration="1500" procs="0.01" superimpose="0" interval="0" attack="0" value="0" />
<level id="002" duration="1600" procs="0.01" superimpose="0" interval="0" attack="0" value="0" />
</buff>
<buff id="2" name="破甲">
<level id="001" duration="1500" procs="0.01" superimpose="0" interval="0" attack="0" value="0" />
<level id="002" duration="1600" procs="0.01" superimpose="0" interval="0" attack="0" value="0" />
</buff>
<buff id="3" name="诅咒">
<level id="001" duration="1500" procs="0.01" superimpose="0" interval="0" attack="0" value="0" />
<level id="002" duration="1600" procs="0.01" superimpose="0" interval="0" attack="0" value="0" />
</buff>
之所以这样简化,我遵循了这样两个原则:
1、对于可能需要经常调整的、数值类的配置项,应该放在配置表中。
2、对于配置一次基本不会更改的、属于事物本身固有属性或者特定功能的配置项,都直接写进代码实现里,我认为将它们放在配置表中,只能带来困惑增加理解难度。
对于这些删除的配置项:
kind:效果种类(1-增益效果 2-减益效果)
type:类型(1-持续作用 2-每间隔一段时间作用一次)
targets:buff默认作用目标(可用值:1-被技能击中的对象 2-自己; 格式:多种对象使用逗号分割)
coverType:可覆盖类型(可用值:0-不能覆盖 >0-可覆盖相同值的buff)
moveLimit:是否限制移动(0-否 1-是)
attackLimit:是否限制攻击(0-否 1-是)
faceLimit:是否限制朝向(0-否 1-是)
hitLimit:是否限制被击动作(0-否 1-是)
比如moveLimit,对于石化BUFF,限制角色移动本来就是这个BUFF的功能,根本没有必要交给策划人员去配置,也不会出现这样的情况:此时将石化BUFF的这个属性配置为限制角色移动,另外一个时间却配置为不限制角色移动,这个属性对石化BUFF来说应该是确定的,不会随意更改。而对其他两个BUFF也不需要这个配置项。去除其他配置项的理由以此类推。
基于简化后的配置,我们就可以设计相关的类,如下图:
Character:人物,角色
BuffConfig:buff基础配置数据
AbstractBuffGenerator:buff生成器抽象类,因为例子中的buff有复杂的生成逻辑(buff是否可以叠加,触发几率公式计算等),因此设计了专门的生成器来生成相应的buff,在武器或者技能中直接调用生成器生成buff,对于没有复杂生成逻辑的buff,也可以不用实现这个生成器,直接实例化buff即可。
AbstractBuff:buff抽象类,其成员数据包括buff生成方,中buff目标方,buff生成时间,持续时间
成员方法:
onActivate():buff开始生效时需要执行的操作,比如破甲时脱去相应装备,诅咒时减去属性值等,抽象类中默认实现生效时通知客户端的操作
onExpire():buff失效时需要执行的操作,比如石化最后对角色的掉血,诅咒最后恢复相应的属性值,破甲穿上脱去的装备等,抽象类中默认实现失效时通知客户端的操作
isExpire():判断buff是否失效,超时时即为失效
另外还可能有间隔一段时间产生的buff效果,声明相应的处理方法即可。
StayBuff:石化buff实现类,成员数据为对角色的伤害值
CurseBuff:诅咒buff实现类,成员数据为具体的减去属性的值
BreakDefendBuff:破甲buff实现类,成员数据为需要脱去的装备
处理系统中角色的buff超时,间隔伤害等,可以单独启动一个线程。实现上面buff的处理架构后,需要定义新的buff类型时,只需要继承AbstractBuff,在相应的方法中实现相关特性功能,很方便扩展,也不会影响到原有buff的功能,有效避免了buff之间功能的耦合,给测试也带来了很大便利。