[技术交流] 手游回合制游戏战斗机制归纳式设计

>>>  技術話題—商業文明的嶄新時代  >>> 簡體     傳統

GameRes发布,文/猴与花果山,转载请注明作者和出处


手游的回合制游戏,我就不多介绍了,最近的《秦时明月》,早些时候的《我叫MT》等。从一个单纯的策划角度来说,你可能觉得开发这样的游戏,为战斗过程中加入一些有趣的buff、有趣的玩法会更有趣,但事实上在实现过程中,因为种种原因最终都没了下文,其实核心原因很多时候是在策划对于游戏设计的规划上出现了问题。

首先我们来畅想一下,一个秦时明月类型的游戏,或者我叫MT类型的游戏,其中有什么背景角色,无所谓,我增加一个卡牌可以吧?一个英雄——伊夫里特(我想这个够出名了吧,不知道就白度去吧),为了体现这个卡牌的价值,作为游戏设计师,我希望加入一些特殊功能,让这个卡牌或者英雄与众不同,那么如果游戏允许加入被动特技,伊夫里特的被动技能我想可以是:受到火焰伤害的时候,免疫受伤效果,将原本伤害的一半转化为治疗。这是一个很刁的效果,至于伊夫里特因此会被定义为多少星什么颜色怎么个价钱的英雄,这里不作讨论,核心是,要让这玩意儿发挥,我们还可以为游戏的战斗加入地形影响,你可以想象,当战斗进行到第3回合开始,场景发生了变化,开始着火了,每回合对所有角色造成40点火焰伤害,而此时你的伊夫里特,相当于每回合恢复20点生命,因为战场的需要,让伊夫利特再次增值。

我相信从设计的角度来说,这样的idea是绝妙的,因为它可以让游戏增色不少:

1,我们有了英雄的被动,让英雄除了数字以外,更加体现出独一无二性。
2,我们有了地形、天气系统,让战斗战场更具特色,火山口和城堡内相比,不再只是背景贴图变化了,火山口会着火,就像城堡内会有乱箭射出。
3,英雄被动配合地形,让养成更多的英雄有了意义。

以上这种脑袋随便一拍就能想到的主意缘何无人能做出来呢?事实上我们真的在动手的时候,会发现一个核心的问题——当我们在服务器上作数据计算的时候,又如何让客户端来重演一次服务器的计算呢

事实上这个问题大家解决的办法都是一样的——

1,策划归纳好战斗中会发生的事情。
2,客户端把这些事情都写好,就犹如写一段脚本一样,然后服务器告诉客户端发生了什么事情,客户端去重现,一个(或多个)回合的战斗,都能序列化为一个1维数组。

我们就拿MT来举例:

第2步策划工作,策划总结出来有这样几个情况:

1,角色发动攻击:造成单个角色受到伤害、死亡。
2,角色发动大招:根据大招造成若干个角色受伤、死亡。
3,角色获胜:战斗结束。

第2步显而易见,我们做个简单的演算:

1,A1角色攻击B1角色,造成300点伤害。
2,B1角色攻击A1角色,造成150点伤害。
3,A2角色攻击B1角色,造成200点伤害,B1角色死亡。
4,战斗结束。

很简单的1维数组做到了。的确,在这种结构下,在复杂一点增加大招也没问题,包括秦时明月也是如此。当然,我们很多策划都能用Excel做出这样的战斗模拟。那么仅仅使用这样的机制,是否能够实现我们之前说的伊夫里特的扩展呢?我相信这个没问题,因为那并不是很头疼的事情,因此我们让策划把想法更进一步的扩展一下:

我们的策划又设计了几个英雄,他们的数值我就不管了,我只来说说他们的被动:

1,盾牌哥哥,被动——格档:受到的伤害若小于300,则完全抵消。
2,骑士姐姐,被动——护卫:每回合我方后排角色受到的第一下伤害,都会被骑士姐姐援护掉,骑士姐姐受到该伤害50%的损伤。

你要知道,光是这两个英雄的被动,是无法满足大佬们的,因为大佬们知道,主流赚钱的卡牌游戏中,还有一个叫做“缘分”的东西,那么盾牌哥哥和骑士姐姐如果有缘我们怎么做才是有趣的?是他俩一起上互相增加20%防御力么?我觉得更好的设计是:

1,当骑士姐姐护卫盾牌哥哥的时候,若最终收到的伤害小于600将被完全抵消。
2,当盾牌哥哥在场时,骑士姐姐受到攻击后可以攻击3个目标,而不是单体,受到攻击的同时会提高盾牌哥哥防御力20。

假如一个程序员并不了解我很早以前就说过的buff机制及其实现原理的话,在看到这个设定的时候,他足够有判断能力的话,就会想到——你是一个异想天开的设计师,和你合作的话,会有更多意想不到的效果要去实现,你有了盾牌哥哥和骑士姐姐,那一定会有想都想不到的牧师弟弟和战士妹妹,他会果断的告诉你这玩意儿做不了,最后你的游戏成了MT,只有你拍一我拍一,所有效果都和伤害挂钩。

假如一个了解和熟悉我的Buff机制的人会去如何做这些逻辑呢?事实上很简单,回合制游戏中最佳的buff回掉点就只有这么几个:

1,回合开始时:在每个回和开始时执行,比如HoT技能、DoT技能,对于一个回合制游戏来说,回合开始时生效是最合理的(ATB游戏不在此讨论范围)。
2,角色攻击时:当角色攻击命中每个目标的时候回调,用于左右最后的攻击效果,如造成爆击时吸收造成伤害50%的血量恢复自己。
3,角色受击时:当角色被攻击时的回调,用于左右最后的攻击效果,例如盾牌哥哥的,受到伤害低于300时,受到伤害=0。
4,角色击杀前:当角色即将死亡的时候发生的事情,比如我们设计了一个技能叫手下留情,她的作用是永远不会把目标生命打到1以下(请别在这里思考为什么设计这么一个技能)。
5,角色死亡后:当角色被击杀时发生效果,如:每杀死一个角色获得一层狂怒,增加伤害30%。
6,Buff结束时:在Buff结束时候做出的回调,比如:3回合后召唤陨石攻击所有场上的角色。

事实上,Buff机制合理运用,在逻辑层上,是完美无缺的,这些效果都是轻而易举就能实现的,但是这里还是有一个核心的问题,也是最难解决的问题——我如何将这个回合的战斗告诉客户端?

我们继续演算盾牌哥哥和骑士姐姐的浪漫,他们在冒险的过程中遇到了3个怪物,史来姆A\B\C,史来姆A\B\C又都带有特技:

史来姆A:受到伤害时有20%的几率反弹30%的伤害量。
史来姆B:受到伤害固定为1。
史来姆C:每当一个史来姆受到攻击的时候,叠加一层粘液,每层粘液提高自身5%行动速度,但降低5%伤害,每3层粘液可以使攻击产生一层风怒,每层风怒可以使攻击产生额外一击。

战斗开始了,会发生什么?我们大概演算几步:

1,回合1开始,盾牌哥哥攻击,史来姆B受到了1点伤害(582点被吸收)。史来姆C获得了1层粘液。
2,骑士姐姐攻击,史来姆A受到了伤害447点,史来姆A发生了反击效果,骑士姐姐受到伤害134点,骑士姐姐得到了顺势劈的效果(下次攻击命中3个目标),盾牌哥哥得到了提起护盾效果(防御力提高20点)。史来姆C获得了一层粘液。
3,史来姆A攻击,骑士姐姐护卫,受到0点伤害(177点被化解),骑士姐姐获得了顺势劈效果,盾牌哥哥得到了提起护盾效果。
4,史来姆B攻击,盾牌哥哥受到了0点伤害(22点被化解)。
5,史来姆C攻击,盾牌哥哥受到了742点伤害,盾牌哥哥给跪了。
6,回合2开始,骑士姐姐攻击,史来姆A受到了491点伤害,史来姆B受到了1点伤害(442点被吸收),史来姆C受到了222点伤害。史来姆C获得3层粘液,史来姆C获得一层风怒(下次攻击2段伤害)。
7,史来姆C攻击,骑士姐姐受到615点伤害,史来姆C风怒攻击,骑士姐姐受到了667点伤害,骑士姐姐给跪了
10,战斗结束,玩家战败。

我们从上面这段演算,可以看到这样一个复杂的流程:


这个相比一条线下来的MT模式来说,复杂了太多太多。他完全是多条线发展的,那么我们在这个过程中再看看,我们通常所采用的那种归纳方式还有意义吗?我们仍然可以把每一个方块发生的事情归纳起来,因此我们有这些事件:

1,攻击:某角色攻击了另外一个角色。
2,反击:某角色反击了另外一个角色。
………………

你会发现这样归纳,几乎每一个方块都是一个东西,目前只有5张卡牌,如果我们有个哥不林小分队,天知道天才的设计师们能想出什么花招。最要命的是,这么多东西,我们让客户端怎么去重现呢?事实上,最困难的地方,不是将逻辑重现给客户端,而是在逻辑的基础上,我们还有很多表现要传递给客户端,这才是最头疼的,也许一个方块内的事情可以归纳为:

一次攻击:谁攻击了谁一次,造成了伤害多少。

但事实上从一个优秀设计师的角度来说,这应该归纳的并不是一次攻击,而应该是:

1,攻击者发动了某个视觉特效(如果游戏规定攻击前要播放一下)
2,攻击者做出了攻击动作
3,受击者身上出现了受击特效,受击者同时做出了受伤动作,跳出了伤害数字。

根据这个归纳,我们就有了这么3个事情(姑且称之为事情,Thing吧)

1,某个角色身上播放了一个特效。
2,某个角色作了某个动作。
3,某个角色身上跳出了数字。

我们看,假如你这样归纳的话,用在其它地方是不是会更合适?我的意思是,你并不需要增加很多未知的东西,比如我们可以试试看最长的第2段:

1,骑士姐姐的攻击:播放特效-〉攻击动作-〉受击动作-〉跳数字-〉受击特效
2,史来姆C获得Buff:播放特效-〉跳数字(确切地说是文字)
3,史来姆A反弹:播放特效-〉反弹动作-〉受伤动作-〉跳数字-〉受击特效
4,盾牌哥哥获得Buff:播放特效-〉跳数字
5,骑士姐姐获得Buff:播放特效-〉跳数字
利用这样的归纳方法,我们很容易的就把整个一段很特殊的5个事情归纳成了3个已经归纳过的动作。

在这个基础上,我们很容易就能确定出,服务器应该如何整理这个结构用来告诉客户端(Haxe)
首先我们需要的是一个总管,来实现上面的树结构:

class BattleAction
{
       public function new(_thing:Dynamic)
       {
               thing = _thing;
               nextFunc = new Array<BattleAction>();
       }
       
       public var thing:Dynamic;   //一个动作的描述
       public var nextFunc:Array<BattleAction>;  //我的后续动作数组
}



值得注意的是,除了Root部分(也可以说是第一层枝)这里,我们需要按顺序来执行外,其他的地方,只要是同一支展开的,都是同时执行。我们通过这样一个结构,整理出所有的Thing,形成一棵大树丢给客户端。而BattleAction中的那个Thing(Dynamic),便是每个动作,还是借着刚才的举例:

1,角色做动作
class BattleThing_ChaDoAction
{

       public function new(_chaUid:String, _actionId:Int)
       {
               chaUid = _chaUid;
               actionId = _actionId;
       }

       public var chaUid:String;  //哪个角色
       public var actionId:Int;   //什么动作
}



2,角色播放特效
class BattleThing_PlayAnimOnCha
{

       public function new(_onChaUid:String, _effectName:String, _dummyPos:Int, _playTimes:Int = 1)
       {
               onChaUid = _onChaUid;
               effectName = _effectName;
               dummyPos = _dummyPos;
               playTimes = _playTimes;
       }
       
       public var onChaUid:String; //谁身上播放
       public var effectName;       //特效名
       public var dummyPos:Int;   //绑点
       public var playTimes:Int;        //播放次数,Magic:-1为循环
}


3,跳数字
class BattleThing_PopTextOnGuy
{

       public function new(_chaUid:String, _text:String, _fontName:String)
       {
               chaUid = _chaUid;
               text = _text;
               fontName = _fontName;
       }

       public var chaUid:String;  //谁身上
       public var text:String;      //什么内容
       public var fontName:String;  //什么字体
}


当客户端获得这棵树的时候,我们根据类型来解析Dynamic,然后作出不同的动作,就可以完美的重现服务器上复杂的逻辑,从而实现出Buff机制在回合制游戏中的完美表现。当然这套机制中,起到核心作用的仍然是策划对于游戏系统、功能的归纳能力,假如一个策划只能提需求,是做不了这种项目的



GameRes游资网 2015-08-23 08:38:50

[新一篇] 次世代:游戲機界的世界大戰

[舊一篇] 人生蠻荒記(3):識己
回頂部
寫評論


評論集


暫無評論。

稱謂:

内容:

驗證:


返回列表