游戏编程-可扩展技能系统

| 分类 GameDev  | 标签 thinking 

在具有战斗系统的重度游戏中,技能系统应该是是其中最重要最复杂的子模块。它的重要性在于其影响着游戏战斗最直观的视听觉表现;而复杂性表现于其制作过程中将设计、美术和程序三者一直绑在了一块。一个优秀的技能系统需要三者在游戏开发过程中不断磨合不断修正而成。这里记录的均是笔者在实践过程中的一些思考。


玩法与功能

从游戏设计的角度来看,游戏可分成游戏功能和游戏玩法。前者负责向玩家展现游戏的丰富性多样性,后者向玩家展示最核心的游戏内容。简单的小游戏自然功能简单,一般几个UI引导就算完成,然后直接进入核心玩法;而复杂的游戏就需要抽象出各种各样的玩法系统,如pve、pvp,任务系统、装备系统、合成系统等等,然后经过层层指引最终展现给玩家核心玩法。比如炉石传说,它的游戏玩法就是从一套卡牌中抽卡互相博弈,而获取卡包多样的战斗模式就是游戏的功能。简而言之,游戏玩法提供核心的内容,多样的游戏功能则负责变着花样的让玩家进入核心玩法中。现阶段,各类重度游戏几乎都把战斗列为核心玩法。而战斗系统简单的说就是人物A通过什么东西人物B进行战斗。然后游戏设计者就在这三者上进行扩展设计。当然,最简单直接也是最有效的就是从什么东西进行扩展,在另外两个上面进行扩展无非就是PVE、PVP、人人对战、人机对战、一对多、多对一之类扩展游戏玩法。一般来讲,技能系统就是战斗系统中那个什么东西。利用技能系统能最直接地表现出一个游戏战斗的丰富多样性,试想有多少玩家沉浸于MMORPG(如WOW)和MOBA(如DOTA)游戏中几百上千的技能中。

上面从游戏设计的角度引出了技能系统的设计缘由,现在我们从程序的角度来解剖它。


拆解技能描述

先从一个具体的技能说起。

一个技能的诞生是由设计人员决定的,设计人员根据游戏的属性与玩法,描述出了一个技能。在设计人员眼中,一个技能就是一堆属性与对应的值,增删属性,更改值就生成了不一样的技能。所以,在mud或者桌游中,游戏的进行可能就是这样的:

...
2015.07.19 15:20:10  A 向 B 发出了 火球术
2015.07.19 15:20:11  火球击中了 B,造成伤害 123
2015.07.19 15:20:12  B 触发了天赋技能,反伤
2015.07.19 15:20:13  B 向 A 发出了 野蛮冲撞
...

在没有图形的表现下,技能的表现仅在于数值计算上。这个时期对于技能的表现在文字描写上(火球轰击在B身上,爆裂开来,火星附在B的身上久久不散)。当然,为了更形象,你可以描述的更详细点,从技能开始出现的时候就开始描述。

(只见A摇动法杖,无数火星凭空出现,纷纷扑向法杖头部,须臾间,一颗硕大的暗红火球凝聚在法杖之上,
一声低语,烈阳九重火球术。A法杖一指,火球带着熊熊烈焰扑向了B。不及有所反应,火球转瞬即至,
狠狠的轰击在B身上,爆裂开来,火星四射,余焰附在B身上久久不散)。

相信有了上面这一段描述后,玩家对这个技能,以及刚才的战斗有了极大的游戏体验提升,这就是设计人员心中的描述。

而今的游戏,这一大段战斗描述,换成了更直观的图形图像表现。所以,设计人员将他们心中的技能描述转交给效果创作人员。将文字体验转化为视觉体验。

(模型A切换至施法动作,附上火球施法的特效,在0.5秒后,在法杖头部附上火球特效,火球特效移动,
撞击在B身上,火球消失并在B身上附加一个火焰灼烧的特效。)

我们通过对比文字描述发现,这一段视觉描述就将视觉表现完全交给了特效,而展现的就是单纯的技能表现逻辑。简而言之,一个技能视觉表现就是,在正确的时间点播放正确的美术特效。然后,我们把这一段描述换成程序描述。

actorA.switchToAction(act_fire)
EffectManager.CreateEffect(actorA, effect_fire_begin)
Delay(0.5s)
EventManager.DispatchEvent(function onAttackBegin(){ 
	EffectManager.CreateEffect(actorA, effect_fire)
	TimerManager.CreateTimer(function fire_timer(){
		effect_fire.Move()
		effect_fire.Collide(actorB)
		}, 1)} )
CalculateDamage()
EffectManager.CreateEffect(actorB, effect_fire_end)

(模型A切换至施法动作,附加火球术施法前置特效,在0.5秒后,触发施法事件,相应的事件相应中,
加载火球特效,为火球特效附加计时器,移动其位置并做碰撞检测。在火球的计时器中,检测到了与B的碰撞,
触发碰撞事件,相应的事件响应中做数值计算,并在目标B上加载一个火焰灼烧的特效。)

这一段四不像的伪代码,表述的就是程序逻辑,程序需要一套严格处理逻辑来自动化完成整个技能表现,来完成所谓的在正确的时间点播放正确的视觉效果。我是倾向于事件驱动的,不光是技能系统,整个战斗战斗系统都应该以事件驱动。事件驱动的好处在于融合了统一性和多样性。统一的事件,多样的action。可以看出,要在正确的时间点表现出正确的技能特效需要不少的管理容器(EffectManager,EventManager,TimerManager)。

对于这么一个具体的技能,我们对他最大的要求就是准确性。换言之,如果一个技能在正确的时间点,播放了合适的技能效果,造成了准确的反馈,那可以说,这个技能表现是对的。在换言之,对于如今技能表现基本靠模型动作和技能特效的游戏来说,如果模型动作与技能特效出现的时间点和位置符合心里预期的话,那这个技能表现就具备一定的真实性,俗称打击感。

我们已经提取出来了关键点,在这个特效横飞的时代,把握好何时何地播放技能特效就迈向了打击感的第一步。

  • 时间点 整个技能过程,从起手到反馈,有两个最重要的时间点,出手点反馈点,前者表示技能效果出现的时间点,后者表示技能作用的时间点。一般而言,在出手点会做一些技能数据读取,比如上述火球的飞行速度与攻击力,然后是根据数据加载播放正确的技能特效;而在反馈点做大量的判定与计算,比如上述火球击中B时,判定B是否闪避,是否火免,然后在根据相关数据进行数值计算。当然,如果有的话,不要忘了加载播放火球的击中效果与余焰的燃烧效果。

如何来准确确定这两个时间点呢?

对于出手点,一种是由设计人员将数值写在配置文档中,另一种是由美术人员在模型动画里做标记位。直觉上讲,由美术人员在模型的动作里标记是最精确地,但更理智的做法是,尽可能让美术资源单纯的作为一种资源存在,不要与任何游戏逻辑扯上关系。面对游戏这类高频度修改的工程,改配置中的一个数值远比改美术资源来的代价低,效率高。所以这个点,在配置中,叫技能前摇时间,游戏中,通过计时器,该时间段之后就是所谓的出手点

对于反馈点,这个时间点的确认基本上是由程序判定的。最简单也是最实用的就是碰撞检测点,发生碰撞之时就是技能反馈之时。碰撞检测是一个持续的过程,一个高效的计时器不可或缺。

  • 位置 简单的说,技能特效播放的位置在2D里面就是一个(x,y),在3D里面多了一个Z(x,y,z)。细说开来,可以精确到人物的部位,头身手腿脚,就看游戏的精细程度,越是精细,对人物制作(模型创作),技能特效的要求就越高,越统一规范。

配置+脚本+接口=无限扩展

正确释放了上述一个技能之后,我们就需要考虑一下如何抽象出一个方案,能以不变应万变,正确释放设计人员脑子里无法计数的其他技能。这就是所谓的技能系统的灵活性,也叫可扩展性。

通过解剖上述一个技能的全部施放过程,我们可以抽离出一个重要的信息。

技能系统是天生的矛盾结合体,同时具备统一性多样性。因此,如果我们设计的技能系统方案能满足这两个性质,则说明我们的技能系统是可行的。

一款游戏中的技能是与这个游戏强相关的,从设计角度来讲。如果一个技能引入了该游戏根本不存在的效果,则说明这个技能是“错误”的,比如在没有抗性的游戏中某技能有所谓的抗性加成效果,这属于设计错误。因此,游戏中所有的技能都是存在大致相同的属性与效果,准确的说,一个技能的属性应该是全部技能属性池的子集。从设计角度来看,做一张属性池(总表),然后通过属性的搭配与值的配置就能生成各式各样的技能。同时,从程序的角度来看,这很容易做出面向对象的抽象。

然后关键就在于技能效果的表现手法上。直觉的做法是,好像大部分技能都类似,直接写一个大而全的函数,在里面读值加特效算伤害。这种做法在游戏初期还好,但随着时间的推移,随着一个个新的技能出来,你不断地往这个函数里面加上特殊的条件判断,特殊的计算。然后又把这个方法拆成几个小函数。周而复始,直到最终不堪重负。这种也算一种抽象,但这个抽象很明显层次不够高。简单说来,这种想一锅端,一段代码包容架构与实现两个极端的做法是非常不可取的。对于技能系统这种天生具备多样性的系统,大而全的做法只会顾此失彼,要么牺牲多样性,要么大量分支判断最终代码成浆糊,理都理不清。

既然技能是多样的,就应该为每一个技能单独成类。他有自己独特的属性,与更重要的独特的施放方法。更好的抽象在于提供统一的接口来运行这些不同的技能对象。这样的好处在于,可以将技能的所有配置全部转交给设计人员,让设计人员针对每一个技能做代码级别的微调。

如果我们将整个技能系统交给设计人员负责,意味着一个技能的属性配置与施放逻辑全部从核心程序里分离出去了。从设计人员来看,属性配置可以用表格来做,后面导出成程序需要的格式即可,而施放逻辑则必须是程序可执行的脚本逻辑。所以引擎自创的脚本语言与第三方的脚本语言(lua,python等)均可。程序只提供执行接口,这无意之中做到了解耦和

如此,一个技能就是一个属性配置表加上一段执行逻辑的脚本代码。程序在需要的时候读取配置表生成技能对象,在技能的几个时间点(出手点,反馈点)执行对应的脚本代码即可。而在脚本中,一般都是调用程序提供的工具接口来告知程序需要,需要在哪些对象上加载XX特效了,计算伤害了等等。

如此,可以发现,程序接口,属性配置表,逻辑判定脚本三者足以构成一个可扩展的技能系统。


buff != 技能

说完技能之后,就轮到与之息息相关的buff了。从设计的角度来看,技能的多样性有很大一部分就归功于buff。甚至在一些技能系统中,没有技能的说法,只有buff。一切技能都是buff这样的话语时有耳闻。

但我不这样认为。在我看来,技能不是buff,只是产生buff。因为buff本质上是可枚举的,它的总量就是该游戏中人物角色所拥有的最大基础属性。比如,人物有移动速度,攻击速度,魔法抗性等等基础属性,那与之对应的buff就是对这三者的修改,即,加/减移速,加/减攻速,加/减魔抗。当然生命值也算基础属性,所以伤害计算本质上也算一种buff。可以看到,由于buff数量是可枚举的,如果将技能看作buff,则很容易设计成上文中的大而全的技能系统,妄图以有限的抽象出来的buff来表达全部技能。而实际上呢,虽然技能最终产生的buff是可枚举的,但其产生过程是花样百出的。

所以,技能设计应与buff设计完全分离,而不是让技能继承于buff。

既然buff的数量是可枚举的,则可以直接在程序中写好,向外提供一串枚举值即可。由于是技能产生buff,所以对buff的调用应该在上述技能的脚本中。当然,buff数量可枚举指的是其属性作用,其美术特效还是可以丰富多彩的。同样的眩晕buff,不同的眩晕特效能增加游戏的表现力。这种凡是需要多样性的地方,一律由程序提供工具函数和调用接口,由脚本代码做具体控制。基本上对于buff来说,在脚本中要提供的一般有持续时间,buff枚举值(这里稍微扩展一下就可以实现多个buff共存),特效播放等等。而一般的判定是不会出现在buff里面的。因为,buff是技能产生的,是否作用在某对象上应该在技能脚本里判定


实现

可以参阅reddit上的讨论。过来人都是建议少用继承(inheritance),多用组合(composition)。一开始大而全的设计到最后会变成系统扩展最大的阻碍。比起一个ability继承多个父类而获得他们的effect,使用一个技能拥有多个effect具备更少的心智负担。


总结

  • 技能施放的正确与否,关注技能的出手点反馈点,前者由设计人员配置,后者由程序生成(如碰撞检测到)。
  • 技能的可扩展性表现在配置、执行脚本、程序接口三者分离。
  • 配置提供技能对象的属性,脚本提供技能对象的方法,而程序就是加载配置与脚本生成技能对象,并让技能对象运作起来。(这三者不只适用于描述技能系统)
  • 技能不是buff,只是产生buff

最后的最后,一个游戏一定要有一个强大的事件管理器,这个东西不只关乎技能系统,还关乎战斗系统,任务系统、UI系统、剧情系统等等等等。特别是当今游戏越来越自动化。感兴趣的可以看看星际的银河编辑器,那里面叫触发器(trigger),还有valve的source引擎编辑器,也有类似的事件管理器。


ps.通篇下来,基本全是文字,无图无代码,这类与业务逻辑强相关的系统设计,确实不好描述。难怪几乎找不到相关的参考文章。要说清楚已经很难了,还要脱离项目做个更高一点的抽象来描述,难上加难。

pps.游戏这种多领域协作的项目下,设计一个系统,永远不可能松耦合。即使代码上面松耦合了,你也得为设计与美术开发相应的工具。要想降低人力沟通修改的成本,只能通过工具让数据之间交流。但写工具这个活。。。。还得我来。。。


Previous     Next