游戏开发论坛

 找回密码
 立即注册
搜索
查看: 192936|回复: 11

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

  [复制链接]

98

主题

784

帖子

4493

积分

版主

Rank: 7Rank: 7Rank: 7

积分
4493
发表于 2014-7-16 22:10:15 | 显示全部楼层 |阅读模式
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,战斗结束,玩家战败。

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

这个相比一条线下来的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>;  //我的后续动作数组
}


cs1.png

值得注意的是,除了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机制在回合制游戏中的完美表现。当然这套机制中,起到核心作用的仍然是策划对于游戏系统、功能的归纳能力,假如一个策划只能提需求,是做不了这种项目的










0

主题

26

帖子

535

积分

高级会员

Rank: 4

积分
535
发表于 2014-7-17 11:43:06 | 显示全部楼层
假如一个策划只能提需求,是做不了这种项目的。


这恰恰是目前手游的现状吧。只会提需求,不会设计。

0

主题

3

帖子

10

积分

新手上路

Rank: 1

积分
10
发表于 2014-7-17 18:18:37 | 显示全部楼层
我也觉得我欠缺这方面的能力。

21

主题

178

帖子

382

积分

中级会员

Rank: 3Rank: 3

积分
382
发表于 2014-7-17 20:52:49 | 显示全部楼层
文章看到“当我们在服务器上作数据计算的时候,又如何让客户端来重演一次服务器的计算呢?”这一段的时候,我脑海里浮现出来的就是楼主,向上一翻,果然啊!


mt相对简单,复杂一点,看看万智牌,状态更加丰富、关联更加紧密,你说的buff机制来做,实际上并不完全,说状态机会更合适一些。


例子里的buff,基本上都是单独执行,例如中毒buff执行后,定时驱动扣血计算、动画播放等等,可以继续向下包装。

但是以万智牌这种复杂的卡牌游戏来说,其buff之间关系非常复杂,例如加血buff过程中,可能打断xxbuff,激活xxbuff,强化xxbuff,且buff在启动、运行、结束后都有可能对其他buff产生联系。

所以,单纯的buff不够用,需要把这些buff之间的关系也进行管理,进一步减少逻辑的复杂度,所以现在更多的需要用到状态机。

楼主你是个有想法的人,水平眼界意志力都很好,但是我个人非常看不惯把一个简单机制上升到很复杂很高深的地步

0

主题

14

帖子

65

积分

注册会员

Rank: 2

积分
65
发表于 2014-7-17 22:38:18 | 显示全部楼层
字体,特效名什么的为什么也要服务器传。。。?传个ID配合本地配置就好了吧。
特效坐标客户端可以根据对象ID自己来算。
总感觉讲的略复杂了。
我还是比较建议在有专业的程序的时候,策划先把所有技能/组合类型,效果想清楚了想明白规划好了,传达给程序,让程序那边做最优实现方案,不要过度设计浪费成本。
世界上没有最完美,只有更完美,要想把一个机制做的高大全完全没问题,只要能付出相应的成本。

0

主题

8

帖子

44

积分

注册会员

Rank: 2

积分
44
发表于 2014-7-18 11:58:22 | 显示全部楼层
感觉得到作者还是费了蛮多心思,举例挺有意思的
至于归纳方面还是需要继续推敲
给你一个建议,不要简单的做BattleAction的继承归纳,采用行为树来做归纳:
1、根据手机回合常见情况归纳一些分支节点(例如串、并行节点)和叶子节点(也就是你归纳的几个行为节点)
2、既然你说是客户端展现服务器产生的战斗记录,那么可以再补充一些行为树的序列/反序列化方面的思考

7

主题

75

帖子

286

积分

中级会员

Rank: 3Rank: 3

积分
286
发表于 2014-7-18 14:58:25 | 显示全部楼层
大概看了一眼,给客户端的数据没有必要做成树吧,只需要知道每个动作或特效的开始时间,到点就执行,自然就出现顺序或同时的效果了。

0

主题

1

帖子

8

积分

新手上路

Rank: 1

积分
8
发表于 2014-7-22 01:10:50 | 显示全部楼层
网易的产品《迷你西游》就是这套回合内状态机机制。早年端游的手动回合已经存在这种东西了。页游上带来的自动回合,以及配套懒人跳过战斗要结果的机制,看似会给程序的实现上带来难度。但事实证明我想多了,当时我把案子给程序一看,没什么异议反馈。本质上,这套我倒觉得程序的实现是小事,状态机的多样导致了技能多样,安插到市场上卡牌手游角色多样带来的数值才是最头痛的。

0

主题

1

帖子

4

积分

新手上路

Rank: 1

积分
4
发表于 2015-4-13 11:05:00 | 显示全部楼层
ilymyq 发表于 2014-7-17 20:52
文章看到“当我们在服务器上作数据计算的时候,又如何让客户端来重演一次服务器的计算呢?”这一段的时候, ...

我的方式是,给玩家添加buff时,会检查玩家身上的buff,
然后执行逻辑。即楼主的buff机制:buff添加时的回调点。

想问阁下的状态机方式,
玩家身上有多个buff,如何表达这种状态?
状态机又如何简化“添加buff”时的处理?
和我的方式有何不同?

4

主题

23

帖子

639

积分

高级会员

Rank: 4

积分
639
发表于 2015-6-2 10:52:14 | 显示全部楼层
GameRes为什么没有收藏文章的选项!!想法好棒
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

作品发布|文章投稿|广告合作|关于本站|游戏开发论坛 ( 闽ICP备17032699号-3 )

GMT+8, 2024-12-22 12:18

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表