适用于所有ARPG游戏的刷怪机制

2023-07-25 19:42发布

相信做过ARPG游戏的人,尤其是负责刷怪的策划,都多多少少会有一个感觉——最后我们游戏做出来了,怪也刷出来了,但是总觉得哪儿不对劲。这个不对劲,通常包括:一些脑
1条回答
1楼 · 2023-07-25 19:57.采纳回答

相信做过ARPG游戏的人,尤其是负责刷怪的策划,都多多少少会有一个感觉——最后我们游戏做出来了,怪也刷出来了,但是总觉得哪儿不对劲。这个不对劲,通常包括:一些脑洞的功能最后没能实现;不论什么怪物刷出来都像丧尸一样“没有脑子”;或者是刷个怪真不容易,海量的数据要填写。是的,这些感觉都没错,因为我们总认为刷怪是个简单的小功能,从来不重视他,甚至会把一些真实的需求给搪塞了,以至于最后刷怪就被默认为是这么一件做出来效果不咋的的体力活。

刷怪设计的基本需求
  • 设计背景

假设这是一个Diablo-Like的ARPG游戏,也就是2.5D的视角,不管渲染是3D的还是2D的,总之它有一个特性——你一眼望去是可以看到地图的“一小块区域的”

就像这样,你的镜头可以看到地图上的一些景物,和部分的怪物。

我们现在正在一张地图中某个区域(而不是完整的地图,其实设计的时候你的确也应该去分块设计,当然这不是本文的重点),假设这个区域是一个山贼的寨子,而你的工作是为这个寨子设计刷怪规则。

  • 设计思考

首先一些常规的思考——会刷什么兵种在这里;有多少个刷怪点大约分布在哪儿;刷的怪等级多少等等的问题,这个毫无疑问。在完成这些基础元素的思考之后,我们会,也应该去开脑洞想一些问题:

  1. 刷怪的时候,是不是应该有个条件?比如雪天的时候刷的是一批山贼,而大热天刷的是另外一批?再比如白天刷的山贼数量应该比晚上要多?
  2. 同一个刷怪点上的怪一定只有一种吗?这个点上是不是在一定条件下刷的是刀斧手,而当另一些情况下刷的是弓箭手?虽说我们可以用一句“随机”概括掉,但是这样的条件真的应该存在吗?
  3. 刷出来的同一种怪应该是完全一样的吗?刷在寨子门口作为门卫的长矛兵,和刷在火堆边用来表现一群山贼在“吹牛逼”的长矛兵,在没有进入战斗(或者说没有发现敌情的时候),他们的行动应该是一样的吗?是不是至少门卫应该是站着不说话的,而火堆边的应该是坐着话很多的?
  4. 如果我们做的恰好是一个MMO,或者即使只是CO-OP的,也有这样的问题——是不是玩家多的时候怪应该刷的多点快点?而玩家少的时候怪刷的少点慢点?

想到这里,是不是发现想法越来越精细了?这是个好事情,当你想得越精细的时候,这个游戏的品质就越高,先不要被“程序哥哥”的“这做不到”难住了,我们继续开脑洞:

  1. 是不是火堆边上只有一个怪的时候他是不会吹牛逼的?毕竟至少得有一个听众?同理,是不是2个小兵一组对戳才能练兵?而当中一个还没刷的时候另外一个是不练兵的?
  2. 是不是一旦下雨了,门卫小兵还得站在那里,但是围坐着吹牛逼的小兵就该回帐篷了?或者夜幕来临的时候,他们还会换岗?

毫无疑问,你还能想到更多的细节,这些细节都做出来,至少刷的怪就不会像丧尸一样,不管刷在哪儿都是在那里漫无目的的踱步。但是为什么我们很少在国游里看到这些落实呢?因为背后的代价太大了,要做这个效果,在没有好的设计的情况下,简直是天方夜谭。所以我们接下来就要开始提炼需求了。

需求的分析和提炼
  • 所需数据块

要提炼出整个刷怪功能需要些什么数据,我们首先还是应该深入的分析一下这个刷怪功能。我们一度把这个问题简化到:

从这个脑图可以看出,通常我们认为,一个地图上有N个刷怪区域(甚至有些游戏用的是点),每个区域里面有刷多少个怪(点的话就是1);然后有一个刷什么怪的List,他的数据是这个怪物的id和这个怪物被刷出来的概率和权重;最后是怪物死后多久刷新,甚至可能这个属性也会丢到怪物数据里。

这里就出现了一个非常严重的逻辑错误:搞混了怪物表数据(Model)和怪物实体数据(Obj)或者叫怪物的运行时(runtime)数据。相信你有遇到过类似这样的问题——不同等级的长枪兵就是怪物表里2条数据;同一个等级的2个长枪兵,因为掉落甚至是AI不同就可以又是2条数据。但是我们仔细想一下,这样一来,怪物表的作用还对吗?我相信绝大多数人在最初设计怪物表的时候,他所想做的事情就是把“怪物分类”做一个表出来——所有的长枪兵都是同一条数据,如果还有个弓箭手,他会是第二条数据,但是不管是52级的弓箭手还是87级的弓箭手,他们应该都是一条数据,通过f(等级,怪物model)这个函数,我们可以得出这个怪物在任何等级时候不同的数据。

我们先把这个问题丢在一边,来看一些更严重的问题——目前我们游戏中有300种怪物,现在到了圣诞节了,我增加了圣诞长枪兵,圣诞弓箭手,他们不同于长枪兵和弓箭手,所以不管你的怪物表怎么用(即上面说的问题),他们都是新的数据。但是,需求是,我原本刷长枪兵的地方有几率刷(而不全部换成)圣诞长枪兵。这个需求听起来问题不大吧?非常合理!但是如果我们发现一个地图里面有120个点,其中大约30-50个点有刷长枪兵,这时候,第二个问题就暴露出来了——后期维护数据的时候,这是一个几乎不可能的任务,甚至因为上面说的怪物表作用模糊问题,还会导致因为增加掉落物品,而让追加刷怪变成日常行为。

因此这样一个刷怪的数据是错误的,不光之前我们脑洞的一些需求不太好加,就连本身维护都非常困难,所以我们要重新思考:

可以看出,经过仔细思考,我们刷怪需要的数据其实比我们之前随意想的要多出很多,这些数据及其主要作用:

  • 当前地图Runtime数据

即当前服务器(或者单机就是内存里)上实际在跑的这张地图的运行时数据,在这个举例中它包含的信息包括:

  1. 天气:即我们之前思考的,不同天气的时候刷怪是不同的,比如夏天刷蚊子,到冬天就刷战斗机了,这样的设计,只要策划这里通过了,那就一定是合理的。或者我们还可以更精细的分出当前是雨天、晴天还是什么等,这些都是逻辑信息,不要荒废了渲染程序员辛苦做的下雨下雪的效果,他们用的好也很好玩。
  2. 时间:之前我们想的时候也考虑过,当前服务器上这个地图的时间,下午1点还是早上8点半?每一个地图都可以是一个“不同的星球”,有自己的时间系统,我们只是依赖这个数据来决定刷怪结果。
  3. 地图上玩家的信息:这个是一定有的东西,至少我们在这里也会关心地图上有多少个玩家在进行游戏,这也是之前我们脑洞中说过的“玩家多的时候刷怪多且快”这个需求的根本。
  4. 其他数据:根据策划设计,我们还应该考虑有其他数据在这里维护,但是最重要的是,我们很多项目里面可能压根就没有给一个正在运行的地图做这样一个数据块,甚至所谓的“地图”也是根据美术数据来的,而不是依赖于逻辑数据的(即美术做了一张地图,所以全世界就有了这么一张地图,而不是一张地图可能是有2张策划设计的地图,以及服务器运行时候n个副本)。
  • 地图区域数据

地图区域数据即当前这张地图中正在工作的地图区域的数据,是一个运行时的数据。这个名字其实起的不好,更确切的叫法应该是“怪物刷新组数据”,因为他描述的是地图中每一个刷怪区域,与实际理解的地图区域是有偏差的——不同的“怪物刷新组数据”中指向的“区域信息(locationModel)”可能是同一个。

locationModel则是静态的数据,是策划事先在地图上找好的坐标组,它的信息非常简单,通常只需要包括:

  • id(string):这个区域的名称,被其他一些业务引用。
  • tag(array<string>):这个区域的tag,如有必要可以有tag,被其他业务所引用。
  • area(polygon):当然也可以是rect,这个具体看游戏设计,是地图上一个坐标区域。

值得注意的是,因为它的功能就是把坐标管理起来,而没有任何其他逻辑作用,所以不要想反了——什么天气下激活这个区域跟这个区域本身毫无关系,那是地图的逻辑,这个逻辑是否存在取决于策划是否设计了。

“隶属于这个区域的怪物”这个索引信息也是非常有用的,因为刷怪的逻辑会非常依赖于这个,常见的用法是:限定这个区域的怪物总数,比如有Boss的时候这个区域最多只能有10个怪,没有Boss的时候可以有20个怪,类似这样的设计并不是不允许的;以及限定这个区域某些怪物的数量,比如这个区域虽然这个时候可能有15个怪物,但是要确保最多只有6个哥布林,就需要这个数据作为依据了。

“刷怪倒计时”之所以是一个array,是因为会有多个倒计时,每个倒计时结束的时候会刷一次怪,当然这个可以更精细的记录下挂掉的怪物的信息和刷怪剩余时间,如果策划需要的话,但其实这样做是有点违背这个区域信息的逻辑的。

“刷怪条件(spawnInfo)”即这个区域的刷怪筛选条件,也就是“原本的简单设计”中的“刷什么怪”的“复杂版本”,或者更确切的说是“精确版本”。每次刷怪具体刷什么怪,其实都是走这里来决定的,当然这里有一个更简单的做法,就是抛出脚本函数给策划:

characterObj SpawnMob(刷怪需要的数据)

把这个刷怪信息抛给策划,让策划返回给我们一个characterObj,其实这是我们真正需要的东西。

  • 刷怪条件SpawnInfo

如果策划不想用代码解决问题(或者说代码用在更深的地方),那么我们就需要这个SpawnInfo,每一条SpawnInfo代表“这次刷怪的可能性之一”。它主要包含了:

  • 刷怪条件Array<Object>:这个当然是可以组织数据的,但是最好他们都指向一个函数名,而这些函数的返回值是Boolean,这样就是刚才说的“代码用在更深的地方”当这些条件全部被满足的时候,这条刷怪数据才有可能被启用。
  • 候选怪物信息MobSpawnInfo:这是这条信息里面可能刷的怪物的规则,依照这条信息,我们可以把一个怪物的填表数据MobModel变成运行时的怪物characterObj。所以除了怪物的模板索引(tag或者id),还有一些怪物的动态数据。这里要提到的是,如果用tag,那么就要有一个符合tag的怪物的筛选规则,也许还会引申出其他的数据,具体看需求。

最终这个List<SpawnInfo>也只是为了获得一个characterObj,即这个怪物,丢到地图上,以及“隶属于这个区域的怪物”数组里。

  • 角 {MOD}相关数据

如果仔细看这个流程图,你会发现不管是玩家角 {MOD},还是怪物,在这里都是characterObj,的确没错,因为在这个逻辑世界里活动的,都是角 {MOD},至于这个角 {MOD}受到谁控制,是控制层的问题,与这个数据逻辑没有任何关系。

虽然characterObj的数据都是一样的,但是他们的来源未必非得一样,比如玩家的角 {MOD},可能数据来自于数据库保存的信息;而怪物的数据则是由怪物表(mobModel)的信息,配合一些其他信息而产生的,这些信息的组合,在这个脑图里就是mobSpawnInfo。

所以,首先我们分清楚characterObj和mobModel这两个东西,characterObj是一个runtime的数据,即随着游戏变化,这个数据总是在变化的;mobModel是来自数据表的,静态数据,这些数据无论游戏怎么进展都是不会变化的。所以有一些非常动态的数据,他根本就不该属于怪物表,比如:

  1. 等级:最典型的就是等级,58级的长枪兵和52级的长枪兵,最直接的区别就是等级,以及由等级造成的一些数据不同。
  2. 掉落:在不同的地方的长枪兵,掉落可以是完全不同的,哪怕“不同地方”指的仅仅只是不同的刷怪区域,甚至是同一个区域不同概率的2种长枪兵。
  3. 所属阵营:这个在脑图里面没有,但是如果一个游戏够复杂的话,应该有这个,即刷出来的怪物属于什么阵营,阵营不是wow里面的阵营概念,而更像即时战略里面PlayerX的X,不同的X通常都是敌对的,当然如果游戏要在复杂一些,可以有一个类似星际争霸的Force的概念,即多个阵营之间有22关系(这个不详细展开)。
  4. AI脚本段:正如我们之前说的,不同时候刷出来的怪,他们可能在一些AI行为上是不同的,所以他们会被“插入”不同的AI脚本段,至于为什么用“插入”,怪物的AI应该如何设计?这就是另外一个话题了,相信我,篇幅不会比本篇短,故在此略过。
  5. Buff添加信息:在怪物被刷新后,要给他添加一些Buff,作为他默认的buff,我们需要添加这些buff的信息。这个用途很好理解,比如有些长枪兵刷出来的时候会带有“狂怒”状态。但是有另外一些不应该通过这里去刷,比如刷在下雨的地方,这些怪物就有“不会着火”的特性,就是应该通过下雨的aoe添加了“不会着火”buff(在怪物刷新出来的时候就触发了aoe.onCharacterEnter,在这里添加了这个buff,所以不该在刷怪的时候给他添加,当然就算添加了也就那样了,逻辑设计得好影响不大,写法和严密性问题),当然他们走到干燥的地方也可以通过aoe把这个buff移除了。
  6. 刷新时间:图中没有,而这也并不见得是一个简单的数字,因为他可能会依赖于其他运行时数据,比如当前的玩家数量等,一样,我们需要的是返回一个时间(float或Int)用来做倒计时就行了。

这些都是非常典型的,应该属于运行时的数据,而这些数据的来源应该是根据游戏实际运行的状态去根据逻辑进行赋值的,他们绝对不该出现在怪物表(mobModel)里。

反过来验算一下

经过上面的思路整理,我们差不多已经可以确定了很多数据的结构,接下来在动手写文档、建表、写代码之前,我们还需要做一件事情——带着我们之前的脑洞,回过头来看看这些功能能否实现,以及一些相关的玩法功能能不能实现,要尽可能的刁难自己,因为越是刁难,越是会出现边际情况,越是可以催促我们返回去进一步设计。

  • 试试看脑洞满足了吗?

现在,我们把那些刚开始想过的脑洞拿出来看看

  1. 不同天气、不同时间刷不同的怪怎么做?刷怪条件(spawnInfo)->刷怪条件信息,完全可以满足我们这个微不足道的要求,有些条目在下雨的时候就不会被启用,而另一些相似的条目只有在下雨的时候开启。比如不打伞的步兵只有下雨的时候刷(条件为非下雨天),打伞的步兵只有下雨的时候刷(条件为下雨天)。
  2. 同一个区域刷不同的怪怎么做?因为刷怪条件是一个列表,所以这根本就不是一个问题了。
  3. 不同区域的怪物要不同表现怎么做?由于插入AI是刷怪条件(spawnInfo)->候选怪物信息(mobSpawnInfo)下的数据,所以火堆边的怪物和寨子门口的怪物的“插入AI段”数据不同即可做到。
  4. 根据玩家人数决定刷新数量和时间怎么做?既然我们可以获得玩家人数的数据,还可以设置刷怪条件,这就不是问题了,甚至如果玩家数据清晰,还可以根据玩家所选择的职业比例来刷怪,当30%+玩家是战士的时候刷弓箭手行不行?当然没问题,如果策划认为这个设计合理的话。
  5. 小兵的互动怎么做?既然一个“地图区域信息”中有“隶属于这个区域的怪物”的信息,那做这个本身就不是困难的事情,何况还有万能的buff机制,在刷怪的时候添加buff……都可以实现这样的需求。
  6. 天下雨了小兵行为变化了怎么做?这个问题于刷怪没有直接关系,但是可以通过AoE和Buff来轻松实现。

每一个问题,我们只需要代入性的思考1、2个情景就行了,因为具体的情况太多,如果卡死在验算的空想阶段就太糟糕了,不如等做完了我们实际遇到问题实际解决。

  • 周围玩法会矛盾吗?比如我的任务系统?

我们会最先想到的一个问题应该是刷怪和任务的关系了吧?如果刷怪是随机的,那么会不会影响任务呢?

回答是当然会。假如你设计的是杀长矛手多少个,那么刷出长矛手的数量就要通过这些数据控制好,不然玩家就会因为刷怪问题导致不同时候体验同一个任务的“难度”很不一样。当然我还是建议一点,也是最常见的游戏做法,把“杀死长矛手20个”变成“杀死山贼20个”,这样这个寨子里刷出来的人如果都带有山贼的tag,不管是长矛手还是弓箭手或者刀斧手,都算数量,这才是正常的玩家体验。

同样的,我们更深入的思考一个问题——这还让一种任务变成了可能,即“杀伤山贼的士气”,目标为把山贼的士气降低到0%,实际的做法是,玩家接受任务的时候获得一个buff,这个buff到达100层的时候,任务完成,屏幕上显示的是“山贼士气<100-buff层数>%。”,当你杀掉一个长矛手的时候叠加这个buff3层,弓箭手2层,刀斧手5层,是不是杀不一样的怪完成速度就不一样了?

  • 别再多想了,可以动手试试看了

想到这里就别多想了,该动手开始实现了,不过不论你从设计到实现的时候,一定不要忘记一个要点——猴叔的机制最大的不同是——设计这些做法为的是让人更容易发挥,所以从一开始就应该考虑的是如何更容易维护的开放式思维,而不是开始就想好有哪些约束,让别人只能在约束下设计,这样是有违设计精神的。

一周热门 更多>