战棋类 SLG 是一个在游戏领域历史比较悠久的游戏类型,著名的有《火焰之纹章》、《梦幻模拟战》、《机器人大战》、《皇家骑士团》、《炎龙骑士团》、《高级大战争》、《风色幻想》等等系列,30 多年来战棋类游戏的佳作层出不穷。相信有很多游戏开发者入行时的梦想之一,也是开发一款好玩的战棋游戏。那么一款战棋游戏到底要怎么设计和开发呢?游戏策划和游戏程序员在开发一款战棋游戏的时候都要做一些什么事情呢?有没有好一些的战棋游戏的框架呢?
本文就将向大家介绍一套战棋类游戏的开发和设计思路,以设计一个灵活好用的战棋类游戏框架为基础展开,详细说明战棋类游戏的核心玩法开发过程中的内容。
游戏中的数据设计
首先我们还是要定义游戏中的数据结构,正如我经常说的“数据结构设计出来,游戏就设计完了一半”,因为数据结构来源于游戏的所有元素。当然我们知道不同的战棋游戏,在很多的细节上是有所不同的。比如火纹系列中,在后期作品中强调了邻接单位支援的特色,而在梦幻模拟战(非国产手游)系列中则强调了每个单位只有 10 滴血和指挥官带领士兵的感觉。但实际上他们的区别,仅仅在于策划在很多细节环节上的设计。
战棋类游戏的核心数据
在战棋类游戏的核心玩法,也就是战棋模式是由 4 大类数据支撑起来的,他们分别是关卡数据、地图数据、角色数据和动画数据。
动画信息
之所以会把动画信息放在第一位,是因为游戏的表现也是一个非常重要的环节,而我们在游戏中的任何设计,最终都应该返回出一段动画,供游戏表现用。比如每个回合开始的时候,我们就会运算一下,如果这个回合需要开始下雨了,那么就应该有一个开始下雨的动画表现,这就产生出一条动画信息,动画播放系统凭借这条信息开始播放下雨,播放完下雨之后才进入真正的回合开始状态。
动画信息的本质
动画信息在回合制游戏中,本质上都是一个 Timeline,即一个时间轴,这个 Timeline 是由若干时间轴上的节点组成的,开始播放 Timeline 的时候,时间推进,当时间推进到某个节点的时间点的时候,就触发这个节点的事件,如果这个节点是一个结束节点,那么 Timeline 就算完成了,进入下一步流程。
不论什么回合制游戏,Timeline 节点所必需的数据有:
时间:单位通常是 Tick,即第几次 Update(在 Unity 中为第几次 fixedUpdate),游戏开发与现实世界不同,游戏中 Tick 是最小时间单位,而现实中我们通常用秒来作为最小时间单位。
事件:即这个节点要做的事情,这些事情通常包括几类:
创建一个视觉单位:比如角色、特效、跳出来的数字等,在画面的某处创建一个这个。
删除一个视觉单位:停止掉某个存在的视觉单位。
移动某个视觉单位:让某个视觉单位进行位移,比如我们创建了一个火球,要从发射者飞向攻击目标,这就只是一个火球状的视觉特效,经过一个轨迹,到达了目标位置,然后被移除掉,同时又创建了一个爆炸(即命中)的视觉特效单位。
改变某个视觉单位动作:比如角色更换一个动画序列。
在游戏程序运行的过程中,许多的计算结果,最后都应该会产生一个 Timeline,然后根据 Timeline,播放一遍动画,将结果展现给玩家看。正如我们上面说的那样,玩家肉眼看到的是一个火球被发射出来打中了目标,玩家就会联想到火球是一个实际存在的单位,他被一个角色发了出来,最后打中了目标产生了效果,但实际上这个火球和逻辑层运行的火球并不是一个东西,这只是做游戏最基础的视觉欺骗罢了,在动画播放之前,“火球”早已击中了目标,不然就不会有这段动画 —— 这就是回合制游戏的表现做法。
地图动画与对战动画
之所以要区分地图动画和对战动画,实际上仅仅是因为 Timeline 的事件取值以及处理对象不同。因为在一些游戏中,战斗都有一个特殊的表现模式,他更像是我们的国产手游中的自动战斗的战斗系统,只是表现一系列画面,将结果呈现给玩家,但是这个呈现模式却和战棋核心模式的呈现方式完全不同。当然也有一些经典的战棋游戏系列还支持地图上直接战斗,比如火纹系列、机器人大战系列、《七英雄物语》等等,如果一个战棋游戏的角色对战没有特别的表现模块,就不需要对战动画了。
关卡数据
战棋游戏的核心玩法,通常描述的是一场战斗的过程,而一场战斗的完整过程,也通常是很多战棋游戏的一个关卡。在游戏运行过程中,我们需要一些数据来作为这场战斗的信息:
回合数
我们知道战棋类游戏必定是回合制的,所以会有一个回合数的概念,但是回合数这个概念也并非战棋游戏所通用。在传统的战棋游戏中,通常是一方阵营行动完毕之后轮到另一方阵营行动,比如在火纹、高战、机器人大战、梦幻模拟战系列中,都是如此 —— 玩家所有的角色行动完毕之后结束回合,轮到敌人行动,敌人行动完毕之后可能还有盟军行动等等。这样一圈下来轮到玩家再次行动的时候,就是回合数 + 1 的时候。
但是也有另外一些战棋类游戏,比如《金庸群侠传 OL》、轨迹系列的战斗模式、《梦幻模拟战 4》等都采用了 ATB 模式,即取消了传统的回合概念,根据角色的敏捷决定行动次序,一些速度快的角色可能行动了 3 次,速度慢的角色才行动 2 次,由此打破了传统的回合概念,因此在这里“回合数”这个属性就不存在了。取而代之的是 ATB(Action Time Bar)值,ATB 值也相当于一个“回合数”,只是这个数值会比回合数大很多,而且推进规则未必是每回合 + 1 的。
目标条件
即胜利条件和失败条件,尽管大多的战棋类游戏的大多的关卡胜利条件都是“敌全灭”,失败条件都是“我方全灭”或者“主角挂了”,但是依然会出现把所有人移动到某个单元格就能胜利、坚守多少回合就是胜利等等其他条件,一个关卡的胜负条件,是一条决定性的数据,因为我们要在每个角色行动完毕之后和每个回合结束(也可以是开始)的时候去检查这场战斗是否结束了(玩家胜利或者失败),也就是达成了目标条件。
当前行动阵营
通俗的说就是“现在轮到谁走”,通常战棋类游戏都是单机的,也有类似《英雄无敌》系列和《文明》系列有多玩家玩,这时候是轮流行动的。因此我们需要一个当前行动阵营,如 Player1,Player2 等来明确当前行动方。
关卡剧本
关卡剧本是一个比较容易被忽视的存在,但事实上绝大多数带有剧情的战棋类游戏都是有关卡剧本的。关卡剧本就是当游戏符合一定条件之后,会触发一些事件,这通常和整个游戏的数据都有关系,比如“在第 20 个回合,如果鲁大师还没被击败,则敌方骑兵部队将全体撤退”等,都是关卡剧本的内容。关卡剧本所必要的数据主要包括:
触发回合(ATB):在第几个回合的时候触发这个剧本,也可以是在第几回合到第几回合。我们上面举的例子中,触发回合 = 20。
触发条件:满足这些条件才会出发事件,我们上面举的例子中,条件就是“鲁大师幸存”这一条,而实际游戏制作中,应该支持多条件。
触发事件:当条件被满足的时候,会触发一些事件,触发的事件对于程序来说主要做两类事情 —— 其一是做一些数据处理,比如添加一些角色到场上或者删除一些角色等,其二便是生成动画信息,正如我们上面所说的,游戏的逻辑执行之后,应该生成一个 Timeline,然后根据 Timeline 播放动画展现给玩家。
天气等一些游戏特有数据
除了上面这些主要信息之外,不同的游戏也可以在游戏中扩展一些其他的信息给关卡,比如《三国志英杰传》《三国志孔明传》《三国志曹操传》等中有天气系统,下雨天的时候不能施展火计,那么我们就需要一个天气属性来支持这个设计。
地图数据
地图相对于战棋类游戏,就像棋盘相对于《中国象棋》,是一个非常核心的存在。通常在战棋游戏中,地图都是一个二维数组组成,即地图的 (x,y) 坐标对应的单元格是什么地图块,这样做的好处是通过数组下标就能找到对应的单元格。尽管一些游戏从视觉上来看是立体的,比如《皇家骑士团》系列《最终幻想战略篇》等模仿《皇家骑士团》的作品,但实际上他们的地图还是二维的,所以用不到三维数组,它们仅仅只是在在地图块数据中加入了一个“高度”的数值,这个数值会被寻路等系统所关心。
地图块数据
地图的二维数组的每一个元素,都是一个地图块(Tile)数据,地图块数据在通常的战棋游戏中包含了这些信息:
贴图信息:这个地图块的图形信息,比如泥泞的地面、森林等,这是给玩家肉眼所能看到的,甚至不一定得有名称之类文字信息。
移动消耗:这通常是一个 key value 的结构,因为在战棋类游戏中,我们用来区别单位的基本元素之一是单位的行动方式,比如步行、轮胎还是履带,或者骑马、飞行还是游泳等,根据不同的游戏设计,会有不同的行动方式,每一个行动方式经过这个地图块的时候,需要消耗的移动力就是移动消耗,这将被用在寻路上。
属性改变:在大多的战旗类游戏中,不同的地形都会对地形单位上的角色带来不同的属性变化,比如防御力增加等。
其他信息:根据游戏的实际需要还会有一些其他信息,比如名称等,如果需要显示就得有;而一些游戏中,也存在传送门的概念,比如《火焰之纹章 IF 暗夜篇》中第 11 关的楼梯就是一种传送门 —— 角色走上去可以传送到另外一个非邻接的地图块上。
地图块数据是策划需要填表详细设计的数据,就如地图的数据一样,但是实际上在游戏运行时的地图数据并不总是一成不变的,根据游戏的进行,可能会触发一些变化 —— 比如村庄被山贼摧毁了(从一个好的村庄地形块变成了一个被摧毁的村庄地形块),宝箱被玩家拿走了(从一个关闭的宝箱地形块变成了一个开启的宝箱的地形块)等,这些信息都不应该被放在地图块里,而是放在地图信息里,其行为结果是改变了地形块数据。
AoE
AoE 即 Area of Effect 的简称,其概念来源于魔兽世界的玩家称呼。它表达的是在某个范围内的事件,比如陨石撞击地面,会产生一个十字单元格(中心格 + 上下左右一共 5 格)的火海,首先地图块将会变成被砸过的地面,然后火海的火焰就是 AoE,对于 AoE 范围内的所有角色造成伤害。因此在一个战棋游戏中,是有 AoE 存在的必要的,不光是因为技能,还会因为关卡设计,比如这个关卡会有火山喷发等,都会需要产生 AoE 来丰富游戏的内容。AoE 在战棋类游戏所必要的属性包括:
视觉表现:通常我们会理解为一个 aoe 总是要有个表现的,比如我们刚才举例的火海,还有火山喷发出来的岩石等。但事实上绝大多数 aoe 是不需要视觉效果的,比如某个角色施展了“鼓舞军心”的技能,使以他为中心的 8 格范围内的所有友方攻击力提高,他可能完全不需要任何视觉表达这个 aoe,只需要在角色身上播放一些特效表示被鼓舞了,而这个特效的“负责人”应当是角色被添加的 buff,而非 aoe(当然也不是不能用 aoe 来实现,方法是灵活的)。
范围:【运行时数据】,范围是一个运行时数据,而非策划填表数据。可能很多人看到这一点的时候会不能理解,为什么我策划不能在一开始定义好 aoe 的范围呢?实际上原因很简单,比如暴风雪的效果,他就是一个 aoe,但是暴风雪范围有多大?可能低级法师和高级法师是不同的,而装备了某本秘籍的高级法师范围会更大,因此 AoE 的范围,实际上是在创建 AoE 的时候决定的,而非从策划填表数据决定,因此策划不必因为一个技能可能打 1 格-16 格而去填写 16 条数据。而范围还有另外一个“令人惊奇”的概念 —— 那就是范围并非是地图单元格一种,包括在 diablolike 游戏中 aoe 范围也未必仅仅只是一个形状,比如“全场所有女性角色”也是一种范围。因为 AoE 的本质是抓取符合条件的角色,这些角色所在的位置(我们通常理解的“范围”)仅仅是条件之一。
位置:和范围呼应形成实际范围,并不是所有 aoe 都需要一个位置的,但是指向地图区域的 aoe 应该都有一个坐标。位置是可以运动的,比如《三国志 11》中火焰会被风吹走,就是一个位置变化。
每回合工作事件:aoe 允许在每个回合运行一次,因此会有 OnTurn 的这个触发点,这里指向了一段策划写的脚本 (aoeObj, targets, turnId)=>Timeline,即传递给策划脚本的参数是这个 aoe 实体、aoe 范围内的所有角色和当前处于第几个回合,脚本根据这些信息执行内容,并且返回一个 Timeline 用于表现。比如钉刺地形会在每回合给范围内的所有 targets 造成 5 点伤害,就在这里写这个逻辑。
角色进入时事件:当有角色落定在这个 aoe 的范围内的时候(或者途径此处,具体看游戏设计,因此这也是一个策划要设计的细节内容),会触发这个 aoe 的角色进入事件,执行 (aoeObj, character, targets)=>Timeline,即给脚本的参数为这个 aoe,进入 aoe 的角色和已经在 aoe 中的角色(不含 character)。比如这是一个地雷,角色走上去就会爆炸,则脚本中执行的是对 character 造成伤害;再比如这是一对机关,一共有 2 个同 id 的 aoe,每一个进入事件都是检查地图里这个 id 的 aoe 范围内 character 是否达标(比如需要至少有一个单位就是 targets.length>0),都达标了,就会激活机关开启,比如将某个地形块变成宝箱。
角色离开时事件:当有角色从这个 aoe 的范围离开时触发 (aoeObj, character, targets)=>Timeline,在这里 character 指的是离开的角色,而 targets 中也不会包含这个 character。
AoE 是一个非常核心的设计内容,如果使用得当会让游戏的内容极大程度的得到丰富。当然完全不用 AoE 也不是不行,经典的战棋游戏中的确有很多是没有用到 AoE 的。
角色数据
角色相对于战棋类游戏,就如棋子相对于中国象棋。角色是非常核心的元素,不论是敌人还是玩家的角色或者是 npc 什么的,但凡是可以参与游戏中的对战等角色规则的元素,都是角色。
阵营
阵营往往是被策划所忽略的一个设计,但实际上在战旗类游戏的每一个关卡中,这又是一个不得不设计的元素,尽管它本身非常简单。所谓阵营,直白的说就是“哪些人是一伙的”,比如玩家所在的就是一个阵营,而敌人所在的也是一个阵营。但是一个战棋游戏,往往并不是只有两方,可能还会有类似“我方援军”的存在,这样就形成了至少 3 方对战的可能性,但是作为我方援军,为什么不会攻击玩家而只会攻击敌人呢?这就是由阵营关系决定的,阵营关系决定了跨阵营角色之间的一些权限,比如是否可以攻击等,常见的阵营关系(权限)包括:
攻击权:A 阵营是否可以攻击 B 阵营的角色,比如游戏中某一关刷出一批村民,剧情中这批村民受到帝国的迫害,然后玩家军队来消灭帝国军队。那么在这里村民阵营是不会攻击帝国阵营和玩家阵营的,这是为了表现村民的弱势;而玩家阵营不能攻击村民阵营只能攻击帝国阵营,这是为了表达玩家是来拯救村民的;但是帝国阵营可以攻击村民和玩家两个阵营。
治疗权:A 阵营是否可以给 B 阵营进行治疗。在很多经典战棋类游戏中,都会有这么一些特殊的援军,他们是帮助玩家的,并且在战斗胜利后,如果还存活,就会加入玩家队伍,但是让玩家十分捉急的是,这些角色不能被治疗,保全他们的生死就会变得更加困难,这也是这类关卡有趣的地方之一。
跨越权:A 阵营的角色是否会把 B 阵营的角色当做障碍物。这通常是在角色移动的时候才有的问题,如果不把 B 阵营的角色当做阻碍物,那么寻路的时候会把 B 角色所在格子视为可过单元格,尽管不能最终落在这个格子上。而在一些经典战棋游戏中,还有敌人会影响身边单元格移动力的设定,即当一个角色要绕过一个“敌对”单位的身边的时候,会消耗更多的移动力。
阵营关系是双向的,即 A 阵营可以攻击 B 阵营的时候,B 阵营未必有权攻击 A 阵营。当阵营关系确定的时候,通过角色属性的“阵营”值,就能确定角色之间的关系了。
角色
角色的数据根据不同的游戏会有所不同,但是基本只要是战棋游戏,都应该有以下几个类型的属性:
造型:角色在地图上使用的精灵的信息,有些游戏里用的是角色特有造型,有些用的是代表职业的形象,总之造型是需要的,毕竟是一枚“棋子”。
角色属性:这其实应该是一个 struct,具体如攻击力、防御力等都是这个 struct 的属性,当策划需要增加属性的时候,只需要在 struct 里面修改即可。角色属性在角色身上细分为好几个属性,具体包括:
角色基础的属性:即角色在当前等级下的“裸体”属性,有很多战棋游戏,比如高战系列,角色总是“裸体”的,因为没有但未装备系统。
来自装备的属性:在一些战棋游戏里,角色依然有装备,来自装备的属性之和。
来自 Buff 的属性:来自角色身上的 buff 的角色属性,这里可以有若干个,比如用于加法的和用于乘法的等,这些都跟具体游戏的数值设计有关。
最终我们把这些属性传给一个脚本函数,由这个脚本函数返回一个角色属性 struct 回来,作为游戏中角色所使用的“当前属性值”,而这个脚本函数,也正是数值策划要设计的角色属性关系。
移动力:包括一个移动类型和一个数字,对应地图块中的移动类型消耗。一个角色只能有一种移动类型,即便是类似机器人大战系列中的盖塔,可以变形而导致移动类型不同,那也是因为变形之后角色的移动类型发生了变化。
阵营:角色所属的阵营,结合阵营设计,就有了角色在这局游戏中的一些权限。
Buff:这取决于游戏的复杂程度,但是现代的游戏中,应该都有 buff。比如角色的被动技能、角色的临时状态(流血、中毒、攻击强化、风怒等等)都是 buff。
生命值:当前生命值,如果低于 0 就会挂掉,相信绝大多战棋游戏的角色都需要这个属性。
本回合行动完毕 / 当前 ATB 值:对于传统的战棋游戏,即每个阵营行动完毕之后下一个阵营行动的游戏,角色应该有一个标记(运行时的)本回合是否行动完毕,行动完毕的角色无法再继续行动,除非有条件把这个标志再次设为 false。而对于 ATB 游戏来说,则是一个角色的 ATB 值(或者剩余 ATB 值),当 ATB 值达到标准的时候就可以行动,否则角色无法行动。
根据游戏的具体设计不同,还应该扩展出很多很多属性,在这里就不一一列举了。
Buff
通常在玩家的理解中,buff 是指一个角色的增益或者减益状态,甚至可以简单到只有角色属性的变化。但是对于游戏逻辑本身来说,buff 更像是一种特殊的标志,在角色身上存了的这些特殊标志,将在一些流程中起到一些逻辑作用。比如最简单的中毒状态,就是在每个回合开始时这个时间点,对于 buff 的携带者这个角色,造成某个公式算出来的一个伤害值。通常初级游戏策划和玩家所“设计”的一些技能中,常带有“暴雪更新文档”式的描述,比如“每回合对角色造成 50 点伤害”,但是作为专业的设计师,我们的概念里不应该是“暴雪更新文档”一样的东西,而应该把它们转化为可以用来实现的“buff 机制”。就比如“烈焰对于燃烧中的角色造成双倍伤害”,这是标准的“暴雪更新文档”范式,而我们设计师的大脑中应该是怎样的?是“有一个 buff,这个 buff 是一个标志,这个标志的作用是,在被击(BeHurt)的触发中,如果触发源(详见伤害信息)是烈焰,那么就将伤害翻倍”。
在战棋类游戏中,buff 通常需要的属性包括:
buff 释放者:运行时数据,一个角色信息,当然可以为 null。即造成这个 buff 的施法者是谁,由于一些 buff 来自于场景的脚本创造等各种因素,他们是没有“负责人”一说的,或者策划在设计的时候故意让一些 buff 不需要释放者,那么 buff 的释放者应当为 null。
剩余回合 / 剩余 ATB:运行时数据,即 buff 的生命周期(duration),每回合减少 1(ATB 中则是每个 ATB 减少 1)。当一个 buff 的生命周期结束时(为 0 时),就会先走进 buff 的移除事件,如果移除事件返回的是 true,则这个 buff 将被移除。
Tag:字符串数组,这里要表达的是这个 buff 是什么,这完全是由游戏设计决定的,正如很多网站上的内容有个 tag,比如“鞋类”,再比如“Capcom 出品”等。Tag 的内容虽然是“自由”的,但用途是严肃的,比如当我们要设计“移除角色所有的中毒状态”的时候,那么什么是中毒状态呢?我们可能有很多 buff(他们的 id 必然不同)在设计的时候都被认为是“中毒”,那该怎么移除呢?那就是他们的 Tag 里都有一个“poison”,我要移除的是 GetBuffsByTag (character):Array<BuffObj > 的返回内容。
自定义记录参数:这是一个开放的 Object,用于记录一些 buff 特别需要的数据,比如说护盾类 buff 还能吸收多少伤害,都记录在这里。举个例子,我们给角色上了一个“炼狱护盾”,这个护盾可以吸收 100 火焰伤害和 100 物理伤害,我们不需要去做 2 个护盾 buff 来实现,只需要 {"物理":100, "火焰":100} 来记录就行了。
运行间隔、运行事件与运行次数:这 3 个属性是联合工作的,运行间隔是指每多少个回合(每过多少 ATB)执行一次运行事件,运行事件则是一个脚本函数 (buffObj)=>Timeline,即把 buff 实体抛给脚本,由脚本执行对应的功能;而每次执行之后,运行次数会自然 + 1。运行次数是一个运行时数据,而运行间隔和运行事件则是策划填表时产生的数据,在创建 buff 的时候克隆 (Clone) 到了 buff 实体上,之所以是克隆,因为我们可以有其他因素来改变,比如原本一个灼热效果是每 2 个回合对角色造成 50 伤害,当我们对他使用了催化魔法之后,变成了每 1 个回合造成 25 伤害,这时候运行间隔和运行次数都发生了变化。而之所以要运行次数这个,是策划可能会设计出类似魔兽世界中痛苦诅咒法术的效果 —— 每回合对角色造成伤害,这个伤害值会逐渐递增。
发起攻击时(OnHit):在发起攻击时产生的事件,(buffObj, damageInfo)=>Timeline,将 buff 和伤害信息传递给脚本,由脚本来改写 damageInfo 以及创建要执行的 Timeline。这个最常见的用法是“有 30% 几率造成双倍伤害”,即随机数 < 0.3 时伤害信息的对应伤害值翻倍;“对于有燃烧效果的敌人造成额外 40% 伤害”,即目标如带有含“燃烧”Tag 的 buff,伤害值乘以 1.4;“攻击不会落空”,即伤害信息中的是否命中设置为 true。通过攻击发起时,可以给角色赋予很多特性,比如攻击女性角色时伤害降低 20% 等。
被击时(BeHurt):在角色受到攻击的时候触发的事件,(buffObj, damageInfo)=>Timeline,与发起攻击时相似,不同点在于流程(详见后文的伤害流程)。这个最常见的是“受到伤害降低 50%”,“受到伤害时反弹伤害”等。
在这里,我列举了 3 个最常用的触发点,这不代表 buff 只有这个 3 个触发点,根据游戏设计的具体需要,设计者应该在流程中去定义清楚触发点,比如我要免死金牌,就得有角色被击败前(BeDefeated)的触发点,如果想要击败敌人经验翻倍,就要有 (OnDefeat) 等等。但是值得注意的是,千万不要定义太多,想清楚了再要(比如需要某个触发点,先想 3 个用法在增加),因为触发点越多,尽管设计越灵活,但是游戏运行效率也就越差。设计好触发点,是游戏设计早期的核心工作之一。
伤害信息
伤害信息既是一次即将发起的伤害(通常来自于攻击)的详细信息,通过这个信息,伤害处理系统走一次伤害流程,产生伤害和结果。这通常是一个被完全忽略的存在,即便是很多老的经典游戏,都会直接运算伤害而不需要这么一个“多余的结构”。但是随着游戏开发经验的积累,这个伤害信息会变得越来越必要。它的必要性在于游戏中拥有这么多的变数(由策划设计产生),而当这些变数组合在一起形成“build”的时候,会非常可怕,比如策划给一个角色设计了“风怒”,攻击的时候有 30% 的几率产生额外 2 次攻击,而他攻击的目标也被添加了一个 buff,受到伤害的时候,会把这个伤害分散在“灵魂链接”的角色身上,而他“灵魂连接”的角色有一个 buff,是受到伤害时,攻击发起者也会受到相当于这个伤害 30% 的伤害…… 类似这样一环扣一环的设计,玩法上是存在乐趣的,所以策划的想法应该被支持,因此我们需要一个很好的管理机制。
伤害信息 (DamageInfo) 中的主要属性包括:
攻击者:即伤害的制造者,这可以是 null,因为很有可能这次伤害来自于一些剧情或者事件触发,我们期望伤害能走一次 buff 流程,而非直接写死的扣除多少血,那么此时伤害的攻击者应该是 null,而不是受击者自己。
受击者:最终伤害的承受人,这是必须存在的,不然一个 DamageInfo 将没有意义,而在实际运作流程中,如果受击者已经被击败等因素导致不必再对他进行伤害,那么就可以“优化”掉受击者是这个角色的大多伤害信息。
是否命中:通过脚本函数 (attacker, defender, source)=>boolean 获得命中结果,抛给脚本攻击者、受击者和伤害源(source,如“普通攻击”,“Buff 反弹”等)。由脚本返回一个是否命中,值得一提的是,即时是否命中是 false,也必须获取伤害值和是否暴击等数据,因为“是否命中”在这里只是一个参考,而非流程走完了,是否命中和伤害之所以没有关系,还因为在 buff 的流程中可能改变这项值(详见后文伤害流程),所以一事一议,命中是命中、伤害是伤害、暴击是暴击(而经典争论之一的“因为命中了所以才可能暴击”还是“因为暴击了所以命中”,则是由策划根据伤害信息和游戏设计规则来决定,无非是 if 的问题)。当然,在这里未必是一个 boolean,如果策划设计中很多 buff 会有条件式增加命中率,比如“对生命值高于自己的目标命中率提高 20%”,这里可以是一个 number,即命中率,在最终结算的时候再去计算。
伤害值:通过脚本函数 (attacker, defender, source)=>Object 获得的一个伤害值,抛给脚本攻击者、受击者和伤害源。之所以返回值是一个 Object,因为一个游戏中的伤害类型可能是多样的,比如 {"物理":30, "火焰":20},而在 buff 中可能有一些 buff 是“提高火焰伤害 50%”,结果就是让这个伤害信息的伤害值的“火焰”从 20 变成 30。这个伤害值是最后“扣血”的依据,而非由这个伤害值直接去扣血。
是否暴击:通过脚本函数 (attacker, defender, source)=>boolean 获得暴击结果,原理同“是否命中”,也同样可以是一个 number,具体取决于具体的游戏内容设计。
除了这些必要的信息之外,根据游戏具体设计,还可以扩展一些其他需要的信息,这里就不展开说明了。
值得一提的是:游戏中任何一次“正常的”伤害请求,都应该产生一个“伤害信息”,而非直接产生伤害,因为这样可以避免一个错误的递归。我们举个例子:一个目标身上有 6 个 buff,其中第三个“受击时”效果是可能遭受额外一次攻击,伤害力为本次的 40%,这时候这个“受击时”要做的并不是直接“扣血”,更不是直接把伤害信息的伤害值提高 40%,这都不是策划所想,策划想的时候再产生一个伤害,这个伤害也走一次所有的 buff 的流程,而更重要的是,这次伤害应该发身在另外 3 个 buff 执行“受击时”之后,以及其他已经注册的伤害信息之后,这是一个流程问题,所以此时应该是产生出一个新的伤害信息。
游戏的状态和流程
当我们定义完游戏的数据之后,游戏所有的元素就有了,思路都清晰了,接下来我们就要把他们串联起来。在这里开始,就要进入程序去实现这个玩法的流程了,但是这并不代表这其中没有需要策划去设计的部分,也因此策划应该清楚的了解游戏是如何实现的,尽管不必去 coding。接下来我们就从状态到流程去详细说明一下战棋类游戏的开发细节。
在这里,我们以传统的阵营轮流的战棋游戏为主展开,ATB 类将不做完整说明。
战棋类游戏的主要状态
回合开始状态
在每个回合开始时候的状态,在这个状态主要的工作有几项:
播报回合数以及哪个阵营行动:当然这更多的是 UI 设计问题,如果不需要播报,也可以完全忽略这一步,具体还是看游戏设计的需要,但是绝大多数经典的战棋游戏,都是由这一步的,从 UI 合理性来说,至少得告诉玩家轮到谁了。
回合数增加:根据回合数增加规则来增加回合数,并不是每个回合都会增加回合数,通常来说只有轮到第一个阵营(一般来说是玩家阵营,即轮到玩家行动)回合数才 + 1。但是“回合数”中的“回合”往往和这里所说的“(逻辑)回合”不是一件事情,请注意区分(下文提到的“回合”大多都是逻辑回合)。
执行关卡剧本:如果有关卡剧本到了需要执行的条件,就要执行这个关卡剧本,并产生 Timeline。
执行地图上所有 AoE 的“每回合工作”事件。
遍历并执行所有角色的 Buff 中运行间隔(如果符合运行回合的话),一般来说,都是执行当前行动阵营的所有角色(即角色阵营 == 当前行动阵营)才执行。这如何执行最终取决于策划设计的游戏规则。
执行天气变化等游戏特有的、回合开始时候需要做的事情。
我们可以看到上面这些事情都是在每个回合开始的状态下要去做的,而这些事情的先后顺序是有讲究的,策划应该设计好事情执行的先后顺序,因为不同先后顺序会带来不同的执行效果。最常见的例子是一个在“恢复点”上的中毒的角色 —— 他需要受到治疗和损失生命,那么先损失和后损失,就取决于这个顺序。“恢复点”的本质,是因为这一格坐标上有一个范围为 1 格的 AoE,他每回合都会治疗范围内的角色,而中毒则是角色身上的 buff,在每个回合对角色进行伤害,因此先执行 AoE 还是先执行 Buff 将会带来完全不同的结果。同样的如果角色身上有着火 Buff,每回合要受到火焰伤害,而游戏有个天气系统,如果下暴雨就会灭掉所有角色身上的着火 Buff,那么究竟这个回合着火会不会伤害角色,取决于策划设计的执行顺序,是天气先执行(不会伤害)还是 Buff 先执行(会伤害)。
当回合开始的所有事情完成之后,会产生若干个 Timeline(可能会很多),这时候需要一个 Timeline 的合并 (merge),即得有一个规则把这些 Timeline 串联起来,是一个接着一个,还是并行,或者符合一些条件的串联,符合另一些条件的并联 —— 这些也都是策划需要和美术共同精心设计的。
最终,程序这里会播放合并出来的 Timeline,播放完毕之后,进入下一个状态 —— 选择角色状态。
选择角色状态
选择角色状态实际上有 2 种情况:一是玩家行动,则是玩家开始下棋,观看地图选择角色的状态,是一个等待玩家发令的状态;另一个则是非玩家行动,那么在这里将会遍历地图的角色,找到第一个符合这个阵营尚未行动的角色,执行 AI,然后切换到后续状态,如果找不到这样条件的角色,就会进入回合结束状态。
这里需要进一步说明的是,玩家行动状态的显示(display),并非一尘不变的,这根据游戏的设计需求,会存在很多需要修改显示的细节,比如“显示当前选中角色的移动范围”,再比如“显示所有敌人攻击覆盖的范围”等,这些都只是显示的东西不同,并不需要一个特别的状态。
如果是玩家的行动,当玩家点选一个具体的角色的时候,就会进入角色详细信息的 UI,此时就需要切换到“角色详细信息状态”,这也仅仅只是一个“操作锁”的作用,具体需不需要“角色详细信息状态”可以根据实际的 UI 设计来决定。
当玩家行动并且点击一个自己的角色的时候,如果这个角色尚未行动,就会进入“选择移动范围状态”。而如果是 AI 脚本,他是不需要“选择移动状态”这个状态的,可以直接切入到角色移动状态。
选择移动范围状态
在选择角色移动范围的时候,我们就需要依赖地图、角色和阵营 3 大块数据,来获取一个角色可以移动的范围(事实上大多 UI 设计中,这在“选择角色状态”也要用到,这里只是具体介绍一下这个方法)。角色的移动范围是一个标准的 Dijkstra 算法,但是也有一些与常见的 Dijkstra 算法在游戏领域运用的文章中描述不同的地方:
首先“下一格”的选取,不仅仅是邻接 4 格,邻接 4 格是基础规则,如果设计的是大战略的蜂巢式,还分横六角和纵六角,即取的另外两格是 [x-1][y] 还是 [x][y-1] 之类的区别。除此之外,因为地图块可能存在传送门,那么传送到的目的地也符合“下一格”,也应该被加入算法进行运算。
在战棋中,通常来说“敌对目标”(由策划在“阵营”设计中定义何为“敌对目标”,或者在这里定义“穿透规则”)是“不可穿透”的,其他单元格可以穿透但不可落下,即寻路的啥时候是否会把这个单元格视为可过加入“下一格”,不可过就不会加入“下一格”数组里。
运算的时候根据角色的移动类型、地图块的移动类型和消耗、敌人位置(看游戏具体设定中敌人对周围格子移动力的影响,通常来说没有额外影响,作为阻挡已经足够了,这还是看策划设计需要)来生成一个临时地图来运算移动范围。
选择移动范围不光是一个显示移动范围的问题,除此以及确定能否移动以外,还有一个轨迹问题,可能存在一些“地雷”情况,因此角色走过的路径非常重要,如果碰触“地雷”,比如火纹、高战等系列中阴雨天看不见敌人单位,如果下一步会走到敌人单位所在地图块就会被强制停下,因此如果有这样的设计需求,就要记录玩家输入的走路过程。如果没有特别需求,则可以和 AI 一样,使用 AStar 寻路来行动。
AI 选择路线,则是根据一个类似行为树的 AI 脚本产生出移动数据。之所以说是“类似行为树”,因为他只是看起来像行为树,本质上完全不同。AI 脚本是由若干片段组合而成,每一个片段的内容都有:
条件:如果完成条件,则执行事件,算出结果(return)。
事件:计算方法得出结果数据。
否则:挂向另一个 AI 脚本片段。
最终会有一个 AI 脚本片段在最后,就是“无条件待机否则待机”。而“待机”究竟是怎么回事儿呢?从数据上来看,这个返回的移动数据(实际上玩家操作也是得出这个信息)包含的数据为:
移动目标单元格坐标:想要移动到哪儿。这是一个策划需要详细设计的地方,因为通常玩家和初级策划心中会有这样一个错觉 ——“我”的设计是“敌人血不多了就逃走”,这设计的很清楚了 —— 但实际上这什么都没有设计,因为这句“暴雪更新文档”中用了 2 个概括“血不多”和“逃走”,什么是血不多?就是条件里 HP / MaxHP <= 多少?那什么又是逃跑?这个问题就是这里的“移动目标单元格坐标”,哪一个格子算是“逃跑”呢?策划需要设计到一个能返回出具体坐标的函数的程度才算是游戏设计。
使用的行为:比如火纹系列中使用武器攻击敌人、使用法杖治疗友方;比如高战系列中攻击敌人等,都是这个行为,这些能使用的行为,也是游戏设计的核心内容之一。
行为的目标:根据使用的行为决定一个目标,大多时候行为的目标可能是 null。
当行动路径产生之后(玩家选择移动方案、AI 运算出结果),就会根据行动路径和角色,产生一个 Timeline,并且进入角色移动状态。
角色移动状态
角色移动状态,实际上是一个“操作锁”,在这个状态只是改变一些操作权限,然后播放一个角色移动的 Timeline。
选择目标状态
这个状态的有无,取决于游戏的 UI 设计。在一部分战旗类游戏中,角色行动完毕后选择“攻击”等类似指令之后,才能选择目标,因此要有一个状态让玩家选择。
信息确认状态
这个状态也取决于 UI 设计,但是大多战棋类游戏,在单位进行对战之前,都应该显示一下对战细节和一些简单的数据分析,提供给玩家情报,以让玩家来决策是否真的开始对战。
对战状态
通常来说对战状态是攻击者向目标发动攻击,然后进入一个对战画面或者在地图上直接表演战斗的过程。但是随着游戏设计的发展,许多战棋类游戏在对战环节也出现了很多新的花头,比如火纹系列的支援系统等,使得对战已经不再是简单的“单挑”模式了。在今天,战棋游戏的对战模式的复杂程度已经不亚于很多国内的手游产品的核心战斗系统了。那么一个对战系统需要做些什么事情呢?
首先是选择出对战的角色:绝大多数的战棋类游戏中,都是攻击者和挨打者 2 个角色。在火纹系列中,还有邻接的角色,会根据规则(或者选择)进入战斗形成最多 2 对 2 的战斗。这些是现有的游戏的情况,那么我们再来假设一些没有的“创意”,比如能不能是邻接的所有角色进行小团体战?再比如,这个关卡中有一个敌人的狙击手,他不在地图上,但是如果我方角色与敌人发生对战(不论谁主动),并且所在地性不是森林,我方角色又不是比如潜行者、忍者之类的职业,那么这个狙击手就会加入,帮助敌人向我方角色开火。这些设计,其实都是多对多的,由此对战的第一件事情,是先确定参与对战的角色。
确定每个人的行为以及顺序:根据游戏的设计来决定每一个人的行动规则,一部分的战棋游戏中,攻击方不会被反击,所以行为很简单,就是攻击者对受击者发动攻击;另一部分战棋游戏中受击方也会发动反击。另一层来说,有些游戏有 2 动设计,即双方交手之后,有一方可以再度出手。此时,我们需要确定每一个角色的行动行为,最简单的比如谁打谁,如果击败了,是否就结束这次对战。
当一切确定好之后,开始演算:演算的过程通常就是互相使用技能攻击和防御,这时候会产生出很多“伤害信息”数据,通过伤害流程处理这些伤害信息,最后得到逻辑结果(比如角色扣血、被击败等),以及一个 Timeline。
播放这个 Timeline:即展现战斗过程给玩家看,在这里的 Timeline 可能是与地图上的不同的一套,这取决于战斗模式的表现设计。
当对战结束后,会返回到选择角色移动状态,根据游戏的规则,决定这个发起攻击的角色是行动完毕(本回合行动完毕设置为 True),或者一些特殊条件角色可以继续选择移动(比如火纹系列一些作品中骑兵打完可以继续移动)。只有当玩家确定(或者自动帮玩家确定)这个角色行动完毕的时候,才会执行一次“检查关卡条件”的工作,来判断游戏是否分出了胜负。
伤害流程
伤害流程是对战的核心所在,同时伤害流程也是本文中的一个关键点。在绝大多游戏中,伤害流程总是开始被忽略的,到后面会被改的越来越复杂的,而在本文中归纳的这套被称为“buff 机制”的做法中,伤害流程会是一个非常简单而明确的流程:
在这个流程中:
开始一次攻击:即任何因素准备开始一次攻击,比如角色攻击角色,buff 产生伤害等,这些都应该是通过脚本接口 DoDamage 来进行伤害。在这里,如伤害量计算等策划设计的公式,都可以抛出脚本函数来,最终调用这个函数产生的结果填充给伤害数值就行了。
产生基础的信息:脚本接口 DoDamage 返回一个伤害信息 DamageInfo,这是作为交给伤害系统处理的“凭证”,由此“一次攻击”真正的产生了。
伤害系统开始处理伤害信息:即开始将 DamageInfo 经过 buff 流程来进行变化。
攻击者 isNull 判定:因为我们提到伤害信息的攻击者可以是空,所以在这流程里就反应出来了 —— 如果攻击者不是空,我们才会判定攻击者身上所有的 buff,否则将跳过攻击者的 buff 的判定。
遍历执行攻击者的所有 buff 的 onHit:即按顺序执行攻击者身上携带的 buff 中的每一个 onHit(攻击时事件)不为空的对应事件,由此来改变伤害信息和要产生的 Timeline。这个顺序是严肃的,必须从头到尾,而不能是 forEach 等优化的算法,因为 buff 被添加后插入的位置是有讲究的 —— 举个例子,一个 buff 是对目标产生割裂效果;另一个 buff 是对带有割裂的目标造成双倍伤害。这时候是否双倍伤害,就取决于 buff 顺序了。
遍历受击者身上所有的 buff 的 beHurt:原理同攻击者的 onHit,只是此时执行的是受击者的 beHurt(受击时事件)来修改伤害信息和 Timeline。这里需要强调的是,攻击和受击的 buff 先遍历谁的顺序看似可变,实则不可变,这与游戏设计本身逻辑有关,当然强行要调换位置也不是不行。
最终伤害信息处理:当伤害信息被最终过滤完毕之后,就可以根据伤害信息来决定最终的处理了,究竟是命中后才暴击还是暴击就命中,完全是看策划设计了。而这里的伤害信息,无非是这个伤害规则所必须依赖的数据。
当每一个伤害信息被处理完毕之后,也就是一场对战结束之后(这其实是一瞬间的事情,人类几乎感觉不到),就会产生一个 Timeline,播放这个 Timeline 就形成了对战动画,让玩家看到对战过程(因为是 Timeline,所以加速也好、跳过也好就很好实现了)。
整个伤害流程是这样的,我们将一些“额外特殊处理”,比如“攻击后吸血”之类的丢出伤害流程,丢给 buff 的脚本去完成。原本我们是判断 if (吸血标志)else if(其他标志),现在全都由这些 buff(即可以被理解为标志的数据 struct)来承担了,由此也跳过了很多不必要的遍历(比如不会吸血的人伤害流程里就走不到 if 吸血标志),所以性能上只高不低。而在这个流程中扩展出更多的 buff 触发点,也是丰富战斗的关键,比如在角色被击败之前再次执行一些 buff 来看是否能成为“免死金牌”等,这些都应该是策划在设计游戏开始时候去想清楚的。
回合结束状态
当玩家主动结束回合,或者根据算法(包括 AI)得到结束回合请求的时候,就会进入结束回合的状态,结束回合的状态主要做几件事情:
判断游戏胜负条件:一些游戏胜负条件在这时候有效,比如“坚守 30 回合”等。
确定下一个行动的阵营:根据策划设计的规则确定下一个行动的阵营是哪一个。
进入新回合的开始状态:如果没有结束游戏,那么就会进入下一个回合的开始状态。
回合结束状态是一瞬间的,玩家几乎无感,但逻辑上他是实实在在存在的。
结尾
由此,一个战棋类游戏的核心玩法框架设计就完成了,基于这个框架,可以扩展出许多更精细的玩法,这就是游戏要进一步涉及的地方,但是框架到此为止了,框架的意义在于后面的扩展不会对代码和逻辑结构产生破坏性的影响,而不是约束设计者创作。而到此我们也差不多该清晰的定义一下分工了:
程序在这个模块的工作
处理状态:实现每一个状态的流程和逻辑。
实现一些功能:比如 AI 脚本的支持、数值脚本的支持,同时实现一些寻路算法等。
提供脚本接口:如 DoDamage、AddBuff 等一些核心脚本,除此之外,还要根据游戏的具体设计提供一些基础的脚本功能支持。
策划在这个模块的工作
设计计算公式:包括伤害计算公式、AI 算法公式(比如选择敌人的优先级等)。
决策一些细节:如文中提到的“回合开始状态”中的执行顺序,这些都是为游戏规则定型而存在的。
关卡设计:整个游戏好不好玩,全在关卡设计了。
脚本实现:辅助关卡设计,制作必要的脚本,包括 buff、aoe 等功能的脚本,以及关卡剧本的脚本。
到这里,开发就可以顺利展开了,也真正的做到了“流程不通找程序,效果不对找策划”。
本文来自微信公众号:千猴马的游戏设计之道 (ID:baima21th),作者:猴与花果山
广告声明:文内含有的对外跳转链接(包括不限于超链接、二维码、口令等形式),用于传递更多信息,节省甄选时间,结果仅供参考,IT之家所有文章均包含本声明。