相信做过ARPG游戏的人,尤其是负责刷怪的策划,都多多少少会有一个感觉——最后我们游戏做出来了,怪也刷出来了,但是总觉得哪儿不对劲。这个不对劲,通常包括:一些脑
相信做过ARPG游戏的人,尤其是负责刷怪的策划,都多多少少会有一个感觉——最后我们游戏做出来了,怪也刷出来了,但是总觉得哪儿不对劲。这个不对劲,通常包括:一些脑洞的功能最后没能实现;不论什么怪物刷出来都像丧尸一样“没有脑子”;或者是刷个怪真不容易,海量的数据要填写。是的,这些感觉都没错,因为我们总认为刷怪是个简单的小功能,从来不重视他,甚至会把一些真实的需求给搪塞了,以至于最后刷怪就被默认为是这么一件做出来效果不咋的的体力活。
假设这是一个Diablo-Like的ARPG游戏,也就是2.5D的视角,不管渲染是3D的还是2D的,总之它有一个特性——你一眼望去是可以看到地图的“一小块区域的”
就像这样,你的镜头可以看到地图上的一些景物,和部分的怪物。
我们现在正在一张地图中某个区域(而不是完整的地图,其实设计的时候你的确也应该去分块设计,当然这不是本文的重点),假设这个区域是一个山贼的寨子,而你的工作是为这个寨子设计刷怪规则。
首先一些常规的思考——会刷什么兵种在这里;有多少个刷怪点大约分布在哪儿;刷的怪等级多少等等的问题,这个毫无疑问。在完成这些基础元素的思考之后,我们会,也应该去开脑洞想一些问题:
想到这里,是不是发现想法越来越精细了?这是个好事情,当你想得越精细的时候,这个游戏的品质就越高,先不要被“程序哥哥”的“这做不到”难住了,我们继续开脑洞:
毫无疑问,你还能想到更多的细节,这些细节都做出来,至少刷的怪就不会像丧尸一样,不管刷在哪儿都是在那里漫无目的的踱步。但是为什么我们很少在国游里看到这些落实呢?因为背后的代价太大了,要做这个效果,在没有好的设计的情况下,简直是天方夜谭。所以我们接下来就要开始提炼需求了。
要提炼出整个刷怪功能需要些什么数据,我们首先还是应该深入的分析一下这个刷怪功能。我们一度把这个问题简化到:
从这个脑图可以看出,通常我们认为,一个地图上有N个刷怪区域(甚至有些游戏用的是点),每个区域里面有刷多少个怪(点的话就是1);然后有一个刷什么怪的List,他的数据是这个怪物的id和这个怪物被刷出来的概率和权重;最后是怪物死后多久刷新,甚至可能这个属性也会丢到怪物数据里。
这里就出现了一个非常严重的逻辑错误:搞混了怪物表数据(Model)和怪物实体数据(Obj)或者叫怪物的运行时(runtime)数据。相信你有遇到过类似这样的问题——不同等级的长枪兵就是怪物表里2条数据;同一个等级的2个长枪兵,因为掉落甚至是AI不同就可以又是2条数据。但是我们仔细想一下,这样一来,怪物表的作用还对吗?我相信绝大多数人在最初设计怪物表的时候,他所想做的事情就是把“怪物分类”做一个表出来——所有的长枪兵都是同一条数据,如果还有个弓箭手,他会是第二条数据,但是不管是52级的弓箭手还是87级的弓箭手,他们应该都是一条数据,通过f(等级,怪物model)这个函数,我们可以得出这个怪物在任何等级时候不同的数据。
我们先把这个问题丢在一边,来看一些更严重的问题——目前我们游戏中有300种怪物,现在到了圣诞节了,我增加了圣诞长枪兵,圣诞弓箭手,他们不同于长枪兵和弓箭手,所以不管你的怪物表怎么用(即上面说的问题),他们都是新的数据。但是,需求是,我原本刷长枪兵的地方有几率刷(而不全部换成)圣诞长枪兵。这个需求听起来问题不大吧?非常合理!但是如果我们发现一个地图里面有120个点,其中大约30-50个点有刷长枪兵,这时候,第二个问题就暴露出来了——后期维护数据的时候,这是一个几乎不可能的任务,甚至因为上面说的怪物表作用模糊问题,还会导致因为增加掉落物品,而让追加刷怪变成日常行为。
因此这样一个刷怪的数据是错误的,不光之前我们脑洞的一些需求不太好加,就连本身维护都非常困难,所以我们要重新思考:
可以看出,经过仔细思考,我们刷怪需要的数据其实比我们之前随意想的要多出很多,这些数据及其主要作用:
即当前服务器(或者单机就是内存里)上实际在跑的这张地图的运行时数据,在这个举例中它包含的信息包括:
地图区域数据即当前这张地图中正在工作的地图区域的数据,是一个运行时的数据。这个名字其实起的不好,更确切的叫法应该是“怪物刷新组数据”,因为他描述的是地图中每一个刷怪区域,与实际理解的地图区域是有偏差的——不同的“怪物刷新组数据”中指向的“区域信息(locationModel)”可能是同一个。
locationModel则是静态的数据,是策划事先在地图上找好的坐标组,它的信息非常简单,通常只需要包括:
值得注意的是,因为它的功能就是把坐标管理起来,而没有任何其他逻辑作用,所以不要想反了——什么天气下激活这个区域跟这个区域本身毫无关系,那是地图的逻辑,这个逻辑是否存在取决于策划是否设计了。
“隶属于这个区域的怪物”这个索引信息也是非常有用的,因为刷怪的逻辑会非常依赖于这个,常见的用法是:限定这个区域的怪物总数,比如有Boss的时候这个区域最多只能有10个怪,没有Boss的时候可以有20个怪,类似这样的设计并不是不允许的;以及限定这个区域某些怪物的数量,比如这个区域虽然这个时候可能有15个怪物,但是要确保最多只有6个哥布林,就需要这个数据作为依据了。
“刷怪倒计时”之所以是一个array,是因为会有多个倒计时,每个倒计时结束的时候会刷一次怪,当然这个可以更精细的记录下挂掉的怪物的信息和刷怪剩余时间,如果策划需要的话,但其实这样做是有点违背这个区域信息的逻辑的。
“刷怪条件(spawnInfo)”即这个区域的刷怪筛选条件,也就是“原本的简单设计”中的“刷什么怪”的“复杂版本”,或者更确切的说是“精确版本”。每次刷怪具体刷什么怪,其实都是走这里来决定的,当然这里有一个更简单的做法,就是抛出脚本函数给策划:
characterObj SpawnMob(刷怪需要的数据)
把这个刷怪信息抛给策划,让策划返回给我们一个characterObj,其实这是我们真正需要的东西。
如果策划不想用代码解决问题(或者说代码用在更深的地方),那么我们就需要这个SpawnInfo,每一条SpawnInfo代表“这次刷怪的可能性之一”。它主要包含了:
最终这个List<SpawnInfo>也只是为了获得一个characterObj,即这个怪物,丢到地图上,以及“隶属于这个区域的怪物”数组里。
如果仔细看这个流程图,你会发现不管是玩家角 {MOD},还是怪物,在这里都是characterObj,的确没错,因为在这个逻辑世界里活动的,都是角 {MOD},至于这个角 {MOD}受到谁控制,是控制层的问题,与这个数据逻辑没有任何关系。
虽然characterObj的数据都是一样的,但是他们的来源未必非得一样,比如玩家的角 {MOD},可能数据来自于数据库保存的信息;而怪物的数据则是由怪物表(mobModel)的信息,配合一些其他信息而产生的,这些信息的组合,在这个脑图里就是mobSpawnInfo。
所以,首先我们分清楚characterObj和mobModel这两个东西,characterObj是一个runtime的数据,即随着游戏变化,这个数据总是在变化的;mobModel是来自数据表的,静态数据,这些数据无论游戏怎么进展都是不会变化的。所以有一些非常动态的数据,他根本就不该属于怪物表,比如:
这些都是非常典型的,应该属于运行时的数据,而这些数据的来源应该是根据游戏实际运行的状态去根据逻辑进行赋值的,他们绝对不该出现在怪物表(mobModel)里。
经过上面的思路整理,我们差不多已经可以确定了很多数据的结构,接下来在动手写文档、建表、写代码之前,我们还需要做一件事情——带着我们之前的脑洞,回过头来看看这些功能能否实现,以及一些相关的玩法功能能不能实现,要尽可能的刁难自己,因为越是刁难,越是会出现边际情况,越是可以催促我们返回去进一步设计。
现在,我们把那些刚开始想过的脑洞拿出来看看
每一个问题,我们只需要代入性的思考1、2个情景就行了,因为具体的情况太多,如果卡死在验算的空想阶段就太糟糕了,不如等做完了我们实际遇到问题实际解决。
我们会最先想到的一个问题应该是刷怪和任务的关系了吧?如果刷怪是随机的,那么会不会影响任务呢?
回答是当然会。假如你设计的是杀长矛手多少个,那么刷出长矛手的数量就要通过这些数据控制好,不然玩家就会因为刷怪问题导致不同时候体验同一个任务的“难度”很不一样。当然我还是建议一点,也是最常见的游戏做法,把“杀死长矛手20个”变成“杀死山贼20个”,这样这个寨子里刷出来的人如果都带有山贼的tag,不管是长矛手还是弓箭手或者刀斧手,都算数量,这才是正常的玩家体验。
同样的,我们更深入的思考一个问题——这还让一种任务变成了可能,即“杀伤山贼的士气”,目标为把山贼的士气降低到0%,实际的做法是,玩家接受任务的时候获得一个buff,这个buff到达100层的时候,任务完成,屏幕上显示的是“山贼士气<100-buff层数>%。”,当你杀掉一个长矛手的时候叠加这个buff3层,弓箭手2层,刀斧手5层,是不是杀不一样的怪完成速度就不一样了?
想到这里就别多想了,该动手开始实现了,不过不论你从设计到实现的时候,一定不要忘记一个要点——猴叔的机制最大的不同是——设计这些做法为的是让人更容易发挥,所以从一开始就应该考虑的是如何更容易维护的开放式思维,而不是开始就想好有哪些约束,让别人只能在约束下设计,这样是有违设计精神的。
最多设置5个标签!
相信做过ARPG游戏的人,尤其是负责刷怪的策划,都多多少少会有一个感觉——最后我们游戏做出来了,怪也刷出来了,但是总觉得哪儿不对劲。这个不对劲,通常包括:一些脑洞的功能最后没能实现;不论什么怪物刷出来都像丧尸一样“没有脑子”;或者是刷个怪真不容易,海量的数据要填写。是的,这些感觉都没错,因为我们总认为刷怪是个简单的小功能,从来不重视他,甚至会把一些真实的需求给搪塞了,以至于最后刷怪就被默认为是这么一件做出来效果不咋的的体力活。
刷怪设计的基本需求假设这是一个Diablo-Like的ARPG游戏,也就是2.5D的视角,不管渲染是3D的还是2D的,总之它有一个特性——你一眼望去是可以看到地图的“一小块区域的”
就像这样,你的镜头可以看到地图上的一些景物,和部分的怪物。
我们现在正在一张地图中某个区域(而不是完整的地图,其实设计的时候你的确也应该去分块设计,当然这不是本文的重点),假设这个区域是一个山贼的寨子,而你的工作是为这个寨子设计刷怪规则。
首先一些常规的思考——会刷什么兵种在这里;有多少个刷怪点大约分布在哪儿;刷的怪等级多少等等的问题,这个毫无疑问。在完成这些基础元素的思考之后,我们会,也应该去开脑洞想一些问题:
想到这里,是不是发现想法越来越精细了?这是个好事情,当你想得越精细的时候,这个游戏的品质就越高,先不要被“程序哥哥”的“这做不到”难住了,我们继续开脑洞:
毫无疑问,你还能想到更多的细节,这些细节都做出来,至少刷的怪就不会像丧尸一样,不管刷在哪儿都是在那里漫无目的的踱步。但是为什么我们很少在国游里看到这些落实呢?因为背后的代价太大了,要做这个效果,在没有好的设计的情况下,简直是天方夜谭。所以我们接下来就要开始提炼需求了。
需求的分析和提炼要提炼出整个刷怪功能需要些什么数据,我们首先还是应该深入的分析一下这个刷怪功能。我们一度把这个问题简化到:
从这个脑图可以看出,通常我们认为,一个地图上有N个刷怪区域(甚至有些游戏用的是点),每个区域里面有刷多少个怪(点的话就是1);然后有一个刷什么怪的List,他的数据是这个怪物的id和这个怪物被刷出来的概率和权重;最后是怪物死后多久刷新,甚至可能这个属性也会丢到怪物数据里。
这里就出现了一个非常严重的逻辑错误:搞混了怪物表数据(Model)和怪物实体数据(Obj)或者叫怪物的运行时(runtime)数据。相信你有遇到过类似这样的问题——不同等级的长枪兵就是怪物表里2条数据;同一个等级的2个长枪兵,因为掉落甚至是AI不同就可以又是2条数据。但是我们仔细想一下,这样一来,怪物表的作用还对吗?我相信绝大多数人在最初设计怪物表的时候,他所想做的事情就是把“怪物分类”做一个表出来——所有的长枪兵都是同一条数据,如果还有个弓箭手,他会是第二条数据,但是不管是52级的弓箭手还是87级的弓箭手,他们应该都是一条数据,通过f(等级,怪物model)这个函数,我们可以得出这个怪物在任何等级时候不同的数据。
我们先把这个问题丢在一边,来看一些更严重的问题——目前我们游戏中有300种怪物,现在到了圣诞节了,我增加了圣诞长枪兵,圣诞弓箭手,他们不同于长枪兵和弓箭手,所以不管你的怪物表怎么用(即上面说的问题),他们都是新的数据。但是,需求是,我原本刷长枪兵的地方有几率刷(而不全部换成)圣诞长枪兵。这个需求听起来问题不大吧?非常合理!但是如果我们发现一个地图里面有120个点,其中大约30-50个点有刷长枪兵,这时候,第二个问题就暴露出来了——后期维护数据的时候,这是一个几乎不可能的任务,甚至因为上面说的怪物表作用模糊问题,还会导致因为增加掉落物品,而让追加刷怪变成日常行为。
因此这样一个刷怪的数据是错误的,不光之前我们脑洞的一些需求不太好加,就连本身维护都非常困难,所以我们要重新思考:
可以看出,经过仔细思考,我们刷怪需要的数据其实比我们之前随意想的要多出很多,这些数据及其主要作用:
即当前服务器(或者单机就是内存里)上实际在跑的这张地图的运行时数据,在这个举例中它包含的信息包括:
地图区域数据即当前这张地图中正在工作的地图区域的数据,是一个运行时的数据。这个名字其实起的不好,更确切的叫法应该是“怪物刷新组数据”,因为他描述的是地图中每一个刷怪区域,与实际理解的地图区域是有偏差的——不同的“怪物刷新组数据”中指向的“区域信息(locationModel)”可能是同一个。
locationModel则是静态的数据,是策划事先在地图上找好的坐标组,它的信息非常简单,通常只需要包括:
值得注意的是,因为它的功能就是把坐标管理起来,而没有任何其他逻辑作用,所以不要想反了——什么天气下激活这个区域跟这个区域本身毫无关系,那是地图的逻辑,这个逻辑是否存在取决于策划是否设计了。
“隶属于这个区域的怪物”这个索引信息也是非常有用的,因为刷怪的逻辑会非常依赖于这个,常见的用法是:限定这个区域的怪物总数,比如有Boss的时候这个区域最多只能有10个怪,没有Boss的时候可以有20个怪,类似这样的设计并不是不允许的;以及限定这个区域某些怪物的数量,比如这个区域虽然这个时候可能有15个怪物,但是要确保最多只有6个哥布林,就需要这个数据作为依据了。
“刷怪倒计时”之所以是一个array,是因为会有多个倒计时,每个倒计时结束的时候会刷一次怪,当然这个可以更精细的记录下挂掉的怪物的信息和刷怪剩余时间,如果策划需要的话,但其实这样做是有点违背这个区域信息的逻辑的。
“刷怪条件(spawnInfo)”即这个区域的刷怪筛选条件,也就是“原本的简单设计”中的“刷什么怪”的“复杂版本”,或者更确切的说是“精确版本”。每次刷怪具体刷什么怪,其实都是走这里来决定的,当然这里有一个更简单的做法,就是抛出脚本函数给策划:
characterObj SpawnMob(刷怪需要的数据)
把这个刷怪信息抛给策划,让策划返回给我们一个characterObj,其实这是我们真正需要的东西。
如果策划不想用代码解决问题(或者说代码用在更深的地方),那么我们就需要这个SpawnInfo,每一条SpawnInfo代表“这次刷怪的可能性之一”。它主要包含了:
最终这个List<SpawnInfo>也只是为了获得一个characterObj,即这个怪物,丢到地图上,以及“隶属于这个区域的怪物”数组里。
如果仔细看这个流程图,你会发现不管是玩家角 {MOD},还是怪物,在这里都是characterObj,的确没错,因为在这个逻辑世界里活动的,都是角 {MOD},至于这个角 {MOD}受到谁控制,是控制层的问题,与这个数据逻辑没有任何关系。
虽然characterObj的数据都是一样的,但是他们的来源未必非得一样,比如玩家的角 {MOD},可能数据来自于数据库保存的信息;而怪物的数据则是由怪物表(mobModel)的信息,配合一些其他信息而产生的,这些信息的组合,在这个脑图里就是mobSpawnInfo。
所以,首先我们分清楚characterObj和mobModel这两个东西,characterObj是一个runtime的数据,即随着游戏变化,这个数据总是在变化的;mobModel是来自数据表的,静态数据,这些数据无论游戏怎么进展都是不会变化的。所以有一些非常动态的数据,他根本就不该属于怪物表,比如:
这些都是非常典型的,应该属于运行时的数据,而这些数据的来源应该是根据游戏实际运行的状态去根据逻辑进行赋值的,他们绝对不该出现在怪物表(mobModel)里。
反过来验算一下经过上面的思路整理,我们差不多已经可以确定了很多数据的结构,接下来在动手写文档、建表、写代码之前,我们还需要做一件事情——带着我们之前的脑洞,回过头来看看这些功能能否实现,以及一些相关的玩法功能能不能实现,要尽可能的刁难自己,因为越是刁难,越是会出现边际情况,越是可以催促我们返回去进一步设计。
现在,我们把那些刚开始想过的脑洞拿出来看看
每一个问题,我们只需要代入性的思考1、2个情景就行了,因为具体的情况太多,如果卡死在验算的空想阶段就太糟糕了,不如等做完了我们实际遇到问题实际解决。
我们会最先想到的一个问题应该是刷怪和任务的关系了吧?如果刷怪是随机的,那么会不会影响任务呢?
回答是当然会。假如你设计的是杀长矛手多少个,那么刷出长矛手的数量就要通过这些数据控制好,不然玩家就会因为刷怪问题导致不同时候体验同一个任务的“难度”很不一样。当然我还是建议一点,也是最常见的游戏做法,把“杀死长矛手20个”变成“杀死山贼20个”,这样这个寨子里刷出来的人如果都带有山贼的tag,不管是长矛手还是弓箭手或者刀斧手,都算数量,这才是正常的玩家体验。
同样的,我们更深入的思考一个问题——这还让一种任务变成了可能,即“杀伤山贼的士气”,目标为把山贼的士气降低到0%,实际的做法是,玩家接受任务的时候获得一个buff,这个buff到达100层的时候,任务完成,屏幕上显示的是“山贼士气<100-buff层数>%。”,当你杀掉一个长矛手的时候叠加这个buff3层,弓箭手2层,刀斧手5层,是不是杀不一样的怪完成速度就不一样了?
想到这里就别多想了,该动手开始实现了,不过不论你从设计到实现的时候,一定不要忘记一个要点——猴叔的机制最大的不同是——设计这些做法为的是让人更容易发挥,所以从一开始就应该考虑的是如何更容易维护的开放式思维,而不是开始就想好有哪些约束,让别人只能在约束下设计,这样是有违设计精神的。