Contents

游戏伴随计算机图形技术的发展,从消除类到现在形形色色各种类型游戏杂交的xxx向的yyy类型游戏,已经发生了从草履虫进化到牙博士的巨大变化。

大型游戏软件因而也成为了软件工业中最具挑战的项目类型之一,而在大型游戏中,RPG无疑仍然占据着统治级地位。

作为一名unexperienced程序,开发一个RPG游戏将会面临各种各样的难题,其中的重头戏——技能系统,更是从古至今一直都是扮演磨人的小妖精的角色,一方面人们在不断的提升对它多样化的需求,另一方面,在开发商这里,它一次又一次冲击着既定的系统架构,常常让人焦头烂额。

在这个玩家已经无法从简单的特效表现和数值变化中得到满足的年代,我们如好找到一种能够平衡程序、策划工作效率并且兼具灵活性和易用性的技能制作手段?下面列举几种常见思路进行讨论。

“对于重复的、规律性的逻辑,我们可以写出通用的代码去完成它,但对于特殊性很强的逻辑,则只能单独去完成。”

——圣经里这样写到。你也可能因此会悲观的得出仅此华山一条路,别无它法的结论。

在制作角色技能时,为了凸显角色个性和技能特点,使得技能系统一跃成为了众多游戏系统中逻辑最复杂多变的系统。而它的重要性同样不言而喻,在ARPG风格的游戏中,他是展现游戏表现力的重头之一,甚至一些游戏广告里常常以一些特色技能为卖点吸引用户。

然而技能系统这种变化多端的特点恰恰是研发狗的砒霜——由于不知道如何去抽象和规划这些系统,写代码时常常觉得无所适从,以至于许多游戏项目中的技能都是程序挨个敲代码敲出来的(我所知不多的几个项目里就有好几个是这样处理的)。

必须承认逐个技能手写的方案是最具灵活性的,我们可以写出一个个

int SkillFunc_XXX(CRole* pRole,CRole* pTarget);

或者一个个

Skill_XXX.script

可以把其中不多的公共逻辑抽出来放到外部,可以把技能逻辑中常用的功能进行封装提炼,甚至可以培训策划来写技能代码。但其中的沟通、调试、编译、反复修改,种种问题,逐步吞噬研发的耐心和毅力,最终它成为了所有人的噩梦。

作为游戏研发的老司机,相信所有人都会觉得对于技能这样的数据型逻辑,只有配置表才是它真正的归宿。然而呆板的配置表,究竟要怎样才能支撑起一堆堆毫无规律的技能的天空呢?

难道把代码写进配置表

必须要说,这样的确也是一种可行的方案,应该就有团队吃过这只螃蟹。

好的,既然想到了这一点,那么问题来了:是DSL(专用语言)还是原生脚本语言?

如果项目是纯编译型语言写出来的,也不打算引入任何脚本,那么除了DSL也别无选择,为此你需要专门写一套解析程序并在需要扩展功能的时候维护它。如果项目原来就大量使用py/lua之类的脚本语言,那么在配置表中内嵌原生语言代码就是个简单易行的方案。内嵌原生语言的问题是自由度太大,写的时候容易犯错,使用者的水平参差不齐,有时还得把人拉过来进行系统的语言学习。

还有一个要考虑的问题:代码谁来写

如果是程序来做,实际上和原来提到的方式相比也没有减轻多少负担。如果是策划来完成,那么这种工作是有些门槛的,新人要经过适当的培训才能开展工作。由于缺乏编程素养,策划常常要为自己的想法如何转化为代码耗费额外的精力(有的策划还有代码恐惧症),而且制作过程中还容易犯错,流程卡住只能等程序GG来看,一来一回耽误2个人的时间。

解决之道依稀仿佛在逻辑与代码之间。

不妨想想代码诞生的初衷,为什么我们不用自然语言(比如各国母语)和计算机沟通,然后让它按我们的想法做事情呢?因为自然语言并不是一个很好的逻辑表达媒介,冗余的东西太多,结构复杂,歧义也太多,无法清晰准确描述逻辑——于是专门用于表述逻辑的机器语言诞生了。

而机器语言的核心是什么呢?我觉得是控制流——什么情况下要做什么,以及做的先后顺序。归纳起来,也就是老三样:顺序、分支、循环,在实际工作过程中,它们是以指令代码和操作数的形式出现,然后被打包成一根根面条运往硬件厨房各自料理的。

后来由于机器语言的反人类性激怒了上帝,一道闪电落下——汇编语言诞生了:

汇编语言以其规范的板式和浅显易懂的英文单词迅速征服了地球人。

当我们把它填在excel中的时候…咦?为什么这么合身?以前的汇编代码是excel上写的?

既然汇编作为一种程序语言,什么都可以做…那么以此为灵感,我们如何将它改造为适合制作技能的利器呢?一步一步来。

首先,为了更加简单干脆的制作逻辑,我们试着把控制流交给配置表,把逻辑的基本工具以某种特定格式固化在配置表中。思考构成一条简单操作的基本要素:行为、顺序、条件、对象。确认是否具备了这些要素,我们就能准确表达任何自己想要进行的操作。接着按照这种格式,我们初步设计的配置表字段格式如下:

操作名|执行条件|作用对象|效果名|效果参数

由于一个技能常常由多条操作组成,所以它实际上不是一个操作,而是一个包含了多条连续操作的操作集,类似这样:

function skill_xxx()
	oper_1()
	oper_2()
	oper_3()
	...
	oper_n()
end

为了制作操作集,我们就需要配置表描述分组信息(这个操作属于哪个操作集),简单的做法是把所有一个操作集内的子操作都用一个名称命名,其中子操作在配置表中的上下排列位置用来表达它们的执行顺序

我们已经有了可以打包多条操作、描述执行顺序的方法,接下来的问题是,条件如何设置呢?

容易想到的方案是预设一些条件名称,举例来说,比如:“目标血量高于50%”、“处于xx场景”之类的,我们做上一批。但这种方案并不能cover所有类型的条件需求,灵活性也不够好,比如说如果想要“目标血量高于52%”这样的条件怎么办?是增加一个条件名呢?还是在“执行条件”这个字段后面加一列参数呢?再者,如果oper_3想要根据oper_2的结果来考虑是否要执行要怎么做呢?是代码里设置一些通用变量,再写相应的操作和条件设置和检测这个变量吗?

我觉得更好的方法是把条件判断视作一个效果,每个效果执行后记录一个执行结果,一个操作集内的其他操作可以访问这个结果。比如说,oper_2的效果是“目标血量高于百分比”,参数是“50”,那么oper_3的条件一栏可以写“oper_2,成功”或者“oper_2,失败”。这样就避免了在条件这里再扩充字段,同时也把一些特殊需求很好的容纳到了这个框架里。

如果依赖多种条件,我们稍微规范一下格式后,oper_3的执行条件可能会这么填:

"(oper_1,失败) and (oper_2,成功)"

于是只有当oper_1失败并且oper_2成功时,oper_3才会被执行。类似的,我们也可以开发 or 、not、优先级括号之类的东西放进去。并且操作的结果也并不是一定要是非成必败,这里可以定义更丰富的结果状态。

这里唯一的问题是,我们目前的一个操作集内操作名都是相同的,oper_3想要oper_2的结果时,我们无法描述哪个才做oper_2。这里就要取舍一下,到底是出于稳妥考虑额外加一个字段用来编号(比如说修改的时候中间突然插了一行操作,但是下面的条件判断的编号都忘了改,就会出错),还是简洁起见,用操作集内的排列的位置作为访问其他操作结果的ID。

处理了顺序和分支,按照相同的思路再看后面的问题,其实就都是些小问题了。

作用对象这里,我们依葫芦画瓢的预设一些获取对象的方式,常见的比如:当前选中的目标、我方全体、敌方全体、敌方叮当猫、随机目标、周围5米所有目标、上一个操作的目标,诸如此类。

由于偶尔会遇到需要针对某个具名的角色进行操作的情况,比如前面栗子里的“敌方叮当猫”,用这种方式枚举一遍的话,其中排列组合所产生的预设范围也忒大了一点。小技巧是借鉴Linux的系统必备工具的思路(管道机制):

cat xx.txt | awk ... | sort ... | head ...

于是我们可以做一些类似这样的预设:“目标|周围玩家,5米|随机,1”、“自身|周围NPC,5米|名称,叮当猫”,这样就可以组合完成一些复杂的目标选取,实现上也只需要把上一个预设的处理结果传递给下一个预设操作就可以了。

效果名可以说是这种方案真正的精华部分,效果名+效果参数的本质即function xxx(arg),是程序提供给策划进行控制的基本功能单元。我们几乎可以在这个部分做所有你想的出来的事情——包括让服务器自我终结。

常见的我们会预设一下类似这样的效果:“召唤npc(xxx)”、“切换地图(场景名,x,y)”、“增加攻击力(1024)”等等,调皮一点还可做一些更加怪异的举动,比如说刚才提到的“干掉服务器进程”、“执行脚本(aaa.lua)”之类的。而我们前面说的“循环”逻辑,也是要借由这个部分来完成,“执行操作(操作名)”、“执行多次操作(操作名,次数)”,这样就分别开启了递归式的循环和计数循环,它在某些需求下面会很有用(后面会有栗子)。

参数这里,由于我们填的是字符串,解析也是交给各个具体操作去写了,所以理论上支持任意格式和长度的操作,十分灵活。

通过这种逻辑组织形式,可以把五花八门的技能都容纳到这个体系里来。格式化后的逻辑编写,规避了直接编写代码容易出现的一些问题,降低了策划掌握的门槛,把命名中文化还可以让逻辑看起来非常清晰易懂。结合上一篇《浅谈公式计算兼谈LuaStringFunction的妙用》 就可以输出成吨的伤害。

从程序角度来看,我们写一个技能系统实际上是针对这种格式写一个解释器,真正和游戏逻辑相关的其实只有“效果名”下定义的一堆小功能而已。

后续扩展:
这个部分扩展的点主要是条件结果类型、作用目标类型、效果名,前面2个没什么好说的,需要什么做什么,至于效果名,原则上保持职责简单(类似linux小工具),复杂的功能通过组合多个简单功能来完成。

最后附上2个以《炉石传说》中的技能为例写的配置:

冰枪术:
。。。

奥术飞弹:
。。。

Contents