从0开始做个Brotato(2):避免开发坑点


01 项目开发中常见的坑点
在游戏开发过程中通常会存在一些刚开始产生游戏想法时候考虑不周的坑,不同的游戏会有各自不同的坑,其中一些甚至可能导致游戏开发不出来或者始终只是个demo(到后期简直无法维护)。
如果我们光是心血来潮有个想法就急于动手设计甚至开发游戏,那么难免会掉进这些坑里,因此在真正开始动手开发前,我们需要在设计层,做一些设计来避免这些坑。这并不是说因为有坑的存在,我们就必须牺牲掉好的设计,而是好的设计,通常会考虑周全坑的情况——结合开发团队的实力和游戏玩法,来找到一个最佳的平衡点,把游戏设计的更合适。
那么通常来说,粗略的想法会有一些什么样的坑,而通常又是怎么通过设计解决的呢?这些坑主要包括且不限于:
NO.1
可能存在的性能坑和后期优化坑
有一些玩法可能乍一想是非常爽快的,比如海量的怪物潮水般的涌来,然后被玩家使用大范围的AoE技能一波一波的消灭掉,这种“肉酱游戏”的快感相信玩游戏的人都不陌生,但是如果我们只考虑海量的怪物,而不进一步加以限制,那么就会在项目后期给游戏性能和优化带来天坑。经典的“万人国战”说的就是这个——加入一屏幕有一万个单位在各做各的事情,那么游戏也许会卡到无法运作,即使是比较高配的电脑,也可能会出现FPS低于24的情况,而“欺骗人眼”的FPS数是24——即当每秒渲染24帧或更高的时候,人眼会认为游戏是“流畅”的,否则就会有“卡顿感”。
当我们真的需要设计海量的角色、子弹等元素出现在同一个小区域内的时候,先不考虑后期我们可以通过隐藏一些不显眼单位(很多游戏使用这样的策略进行“优化”)比如不活跃的角色,不重要的特效等来实现优化。但是在设计层,其实也是有很多细节可以设计,来使得游戏保持“海量单位”的感受,比如约束怪物最大的数量,只有当怪物数量小于这个数量的时候才会刷新等。
除了刷新规则的约束以及渲染时候根据规则“裁剪”掉“不重要的单位”,通常还可以采用2D卡通风格的美术设定。当我们使用3D写实美术风格的时候,玩家眼里会很在意很多细节,衣服布料等稍有不真实就可能遭到嫌弃,但是如果我们采用2D卡通风格,人们不仅不会去看细节的问题,甚至会接受一些“简陋”的风格。也正是因此,所以2D卡通风格的美术资源,在项目后期更容易进行优化,来大幅度提高游戏性能的同时,不在美术上丢“细节分”。

(左是1997年FF7原版克劳德,尽管简陋但是因为采用了Q版风格很多问题就被包容了,右侧是2020年FF7Remake的克劳德,做到这样得费多大成本,不言而喻)
NO.2
过于放开的设计导致性质被淡化的坑
这是一个设计的坑,通常来说很多时候我们看一个玩法中的元素,可能产生出很多很多的脑洞,如果不对这些脑洞加以限制,就会开发出很多很多乍一看眼前一亮很有意思,但是经不起时间考验的“创意”,而当游戏中充满了这样的“创意”的时候,非但不会让游戏变得更有意思,还会因此导致游戏的一些性质惨遭淡化,以至于原本有策略的地方变得没有策略而损失乐趣了。

加入新的机制和玩法,本身并不是坏事,但是加的过分了反而影响游戏性,因此我们在设计游戏的初期,一定要定好“设计范式”,类似于编程中的Design Patterns——即在我们游戏中,哪些性质是允许的,哪些是不允许的。这些范式约束的是性质,而非确切的设计,例如我们可以约束“所有对法力条产生变化的技能都是不允许的”,但是我们不能说游戏中只有什么,让策划从中pick出来组合就行了。
NO.3
数据表无法维护的深坑
数据表是一个无法忽视的存在,之所以我们需要数据表,是因为有一些数据内容只有人类的思维可以填写,他们没有规律,无法用公式表达,也没有任何理性,完全是感性的,比如某个怪物用什么样的外观等,这些都是必须由策划去配表的。当设计数据表的时候,就会发生一些数据内容能让程序运行起来毫无问题,但是这些数据结构本身和数据量在一起是地球人无法填写的。因此我们只是程序开发层逃避了“无法实现”的问题是不够的,但是人填写不了的数据表一样是“无法实现”的,在过去的20年中,可以说有绝大多数在研发阶段胎死腹中的项目,都存在数据表没有策划能填写的问题。
所以在设计数据结构的时候,我们还必须优先考虑一些数据表的内容量。如果一些数据可以用算法去生成,比如升级所需经验值,只需要一个公式——f(目标等级)即可实现的,就不应该需要人类去填表。这当中并不存在“填表比公式更可控”的说法,首先,对于这个业务来说本身需要的就是一个函数返回一个数据,填表本身只是提供一个“写分段函数的环境”;其次,最差最差的分段函数(数学上的分段函数正是编程中if else的起源)总是能实现填表能实现的东西的。
02 Brotato中需要预设定来避免的坑
在brotato这个游戏中,也有很多乍一想想不到,但是真的到后期就可能成为深坑的因素,brotato的设计师也在设计和开发的时候注意到了他们,所以做出了很多细节设计,来规避这些坑点。接着就让我们详细看看到底有些什么坑,brotato的作者又是如何规避的,如果我们来做的话又需要如何设计去规避。
NO.1
同时存在的怪物数量坑

(brotato中会出现同时有海量怪物和子弹)
海量怪物、子弹和掉落物是如何导致性能问题的
导致海量怪物、子弹和掉落物产生性能问题的关键点是3个——碰撞运算、寻路和渲染。
在brotato中,子弹和单位之间是存在碰撞检测的,因此子弹才能击败玩家或者怪物,玩家角色与掉落物之间也存在碰撞检测,因此玩家才能拾取掉落物。当我们做碰撞检测的时候,就会要用到循环,每一帧我们都需要循环遍历角色和子弹的位置关系、玩家角色和掉落物之间的碰撞关系,当循环的列表变大的时候,自然开销就会变大——假如我们有1000个角色和1000个子弹存在,那么同一帧,我们至少要进行一个1000*1000=1000000次的循环来遍历碰撞关系。

最后就是渲染问题,尽管实际游戏中所有的怪物、子弹、掉落物都同屏的可能性非常小,但是作为游戏(或者说是软件)的开发者,我们从编程的角度就不能忽视问题存在的可能性,即便对于人类来说十分“极端”的情况,对于计算机科学来说,也是必须要注意的。当同屏出现海量单位,并且每个单位在播放自己的动画的情况下,会对FPS(渲染帧率,Frame per Second)带来巨大的杀伤力。
了解了这些可能存在的坑之后,我们就看brotato在这些坑上是如何作出解决策略的。当然在这里,brotato也做了一个很聪明的设计——就是约束了同时出现的怪物数量上限为100,当怪物数量达到100之后,部分符合规则的老的怪物会自己消亡,给新的怪物让道,以确保怪物最大的数量为100,这样一来也就限制了遍历次数,从上面举例的1000次变成了100次,大幅度降低了遍历次数是很好的解决方案,但是光这个还不够,还需要将便利执行的内容也进行一番优化。
圆形碰撞带来的优势
通常没有游戏开发经验、并且躺在引擎功能上的小伙伴们会觉得,碰撞并不是什么大问题,只要使用碰撞组件就行了。但是实际上碰撞组件也是人写出来的,很多碰撞组件为了符合“模型碰撞”的需求,实际上用的是多边形碰撞——将所有多边形根据旋转得到这一帧在世界坐标系里的逻辑多边形(旋转后的多边形)然后去和其他多边形碰撞。多边形碰撞的数学算法这里就不复述了,如果采用多边形碰撞也并不是没有优化的余地,比如我们可以首先用外接AABB(axis-aligned bounding box,3d中为外接长方体)是否有碰撞(AABB碰撞这个概念可以通过百度或者google搜索了解,大意是“世界中”所有的矩形的每条边都是平行或者垂直于横轴或者纵轴的,然后进行碰撞判断,这样算起来只需要比大小),但是这样的碰撞仅仅适合于“静态的世界”。

在brotato中,或者说即便不是brotato,只要有快速移动的物体(通常是子弹,因为子弹的飞行速度很快)需要做碰撞的游戏中,我们就无法采用这种每一帧所有物体之间的坐标直接进行碰撞检测的算法,因为这会导致非常快的物体发生穿透现象:

因此我们需要使用“射线检测”来做碰撞判断,这时候如果全都采用圆形的碰撞体,就会带来巨大的优势:

而我们可以利用数学性质,将子弹的半径,加到世界上所有的角色(或者说子弹可以碰撞的物体)圆形半径上,就把问题简化为了线段和圆的碰撞问题了:

这样一来算法就更加简单了(这个算法的代码在本文中不进行讲解,在今后的代码篇中会有详细解说,当然这仅仅只是一个初中到高中的数学问题,相信不用讲解各位也能知道具体怎么搞)。
2D卡通的优势
2D卡通美术可以逃避很多精细动画问题,甚至只用scale等transform的内容就可以做到很多表现,即使表现的不够好,也可以被认为是风格。因为需要更少的美术资源去做动画等,所以batch也更好分,并且美术工作量也被海量的降低了,同时程序性能优化坑也被解决,还完全不影响游戏性。

(brotato的几种怪物)
在brotato中,美术就采用了2D卡通的风格,因此角色动画只需要scale和改变着色进行,因此大多怪物都只需要一帧的美术资源,这样怪物就可以尽可能多的在一张图集上——“图集”并不是一个新的概念,只是一个新的名称,传统称呼为Batchnode,在2D渲染中,每一帧每切换一次图集会产生一次drawcall,所以将需要绘制在屏幕上的元素尽可能的放在一个图集上,并且在一帧的渲染中尽可能避免图集之间的切换,就会大幅度降低drawcall,提高性能。
移动与挤开的坑
在brotato中角色之间是存在“碰撞”的,我们上面提到了圆形的碰撞的优势,可以很大程度的降低角色之间“挤开”效果的开销。我们只需要将其他角色的所在圆形当做场地上不可移动的范围,也就是寻路中常说的“阻挡”,就可以吃到圆形碰撞的优势。

这样的设计虽然会让怪物看起来十分弱智,但是由于是2D卡通的美术风格,以及“肉酱”式海量击杀怪物的乐趣作为游戏的特性,反而使得这样“弱智”的怪物变得好接受了——正是这样互相呼应一气呵成的设计,不仅没有辜负游戏性,还解决了程序的坑点,才可以被称为好的设计。
NO.2
“效果”之间build的坑
brotato的武器和道具有着多种多样的效果,从实现上来说,从子弹到道具,都可以设计出五花八门的玩法来。但是越有设计空间,就越是难设计,如果一旦效果设计过度,以至于脱离了brotato的框架和调性,非但不会增加游戏性,反而会使得游戏变得不好玩——因为当使用的性质过多的时候,玩家反而不容易找到自己想玩的build,在很多DBG卡牌游戏中就会犯类似的错误,过多的卡牌、过多的效果导致卡牌之间反而更不容易组合出有趣的build。
在brotato,或者说几乎所有的允许开放设计效果的游戏中(比如moba的英雄设计),我们要如何去做好效果之间的设计,以尽可能增加具体效果的同时不丢失build的可能性呢?
归类性质而非归类效果
我们要归类的是性质,而不是具体的效果,这通常是很多团队约束策划设计的一个错误方式——典型的是技能设计中,很多团队通常会由主策甚至是主程约定好一些效果,比如“造成持续性伤害”,“产生爆炸效果”等,然后做设计的策划只能从这些主策或主程挑选好的效果之中挑选并且组合,比如设计出“产生爆炸效果,爆炸命中的目标受到伤害并且收到一个持续性伤害效果,在5秒内每秒损失20点生命值”,但是如果你想做“受到伤害的目标在之后的5秒内每秒产生一次爆炸,对范围内的友军造成20点伤害”就不允许,因为主策或者主程没有选择这个效果,你就无法组合。

(Brotato中即使效果非常独特的道具,也是利用了游戏的性质,比如萌萌猴,利用的是“拾取材料”这个触发点,配合“恢复生命”的这个性质在游戏中是被允许的,就有了这样的设计,并非是主策规定好了就有一个“拾取材料时概率回血”的效果)
真正合适的做法,是归纳出游戏中允许使用的事件触发点,比如在brotato中,就有子弹命中时(怪物碰撞到玩家也可以理解为子弹命中时),怪物死亡时这些触发点,在每一个触发点,我们都约束了允许使用的性质以及明令禁止的性质,比如“允许命中时附带持续性伤害”,“允许持续性伤害产生范围效果”,“允许范围效果产生持续性伤害”,“禁止命中时对武器等级进行修改”等规范,那么当设计师设计的时候,利用这些性质,就能组合出“命中时产生一个持续性伤害(火焰伤害),这个持续性伤害每次生效时会产生一个aoe,这个aoe寻找附近一名距离最近的队友,并给他添加一个一样的持续性伤害”,也就有了火焰的“传染”效果了,这并不是说主程或者主策约定了“允许做传染效果”,而是设计师基于性质作出了设计,但是如果设计师设计了“持续性伤害每次生效都会导致武器等级降低1级”这就是不允许的,因为触碰到了明令禁止的性质。
归类性质而非归类具体效果,然后将这些性质开放给设计师,或者约束禁止某些设计,这才是合理的设计范式,正如软件开发中编程也需要设计范式(design patterns)一样,在设计中,我们也要先做好设计范式(这也是身为主策的主要工作之一),然后再展开进一步设计,当我们在设计中发现新的问题或者细节的时候,也需要进一步对设计范式进行补充,比如我们发现有人的设计为brotato引入了子弹数量的概念,这个概念和游戏调性不符合——“肉酱游戏”本身讲究的是“清场”的爽快感,而弹药限制则是在约束“清场”的可能性,反其道行之,所以不符合,因此我们要明令禁止,于是在设计范式中就追加了“不允许设计子弹数量”的条目。
NO.3
刷怪算法坑
刷怪看起来是一件容易被忽略的事情,包括我们参考Brotato的时候,可能根本就不会去深入想这些怪是按照一个什么样的规则在每一关刷出来的,或者说不以为然,不觉得是一件事情。但是当问题被具体到“下一个怪刷什么刷在哪儿为什么”的问题的时候,这个坑就被提上了议事日程。
刷怪表可能无法填写
当我们粗看Brotato的刷怪的时候,会觉得他是完全随机刷怪的,但是仔细看,又会发现,在不同的难度的每一关,他的怪其实是有规则的,并且出现的顺序都是有规则的,无论是正常刷怪,还是堆到怪物+100%(这是一个属性属性),只要怪物数量+x%这个x相等,他刷怪都是必然相等的,除了大树和背着宝箱的奖励怪。
但是我们深入一想,这要怎么配置才能一样呢?
首先这个配置要解决的几个问题——在第几波,大约什么位置,刷一个什么样的怪,这个怪的出现条件是“怪物+x%”的x>=多少的时候。当有这些数据的时候,我们就能做到像Brotato一样精确地控制刷怪了。但是问题来了,之所以Brotato的设计中会有同场100个怪物上限的设定,是因为关卡中一定会出现刷怪超过100的情况,那么这样一来,每一关策划都可能要手工配置100条以上的刷怪数据,游戏有6个难度,每个难度20关,就是12000条数据,这看起来似乎是“策划辛苦点”就能完成的工作,但实际上即便完成了,要进行调整(比如某一条不满意的时候)也是超乎人类工作能力的事情,且不说让人填写还会出错。

于是,我们重新整理思路,该如何设计这个表呢?我们只需要设计一个20多行(怪物数量)的表,这个表的列数,则根据实际需要运算权重的常量来决定,大约是2-5列的样子,最多100多个单元格,就能做出原本12000的单元格的数据的效果来。尽管最终可能和Brotato原版的有些出入,也并没那么的“固定”。
03 宗旨
设计“我们需要的”而非“brotato是怎样的”
到这里,我们过了一遍Brotato这个游戏中设计可能存在的坑,也想法进行了避免,这是一个工程级游戏开发中必要的工作。而在这个工作中,我们也注意到了一个细节——我们并不是原班不动的去照抄一个Brotato,无论是设计意图还是做法上,我们只是参考了原来的Brotato,而未必是要做的一模一样。






