这是一篇实现2D平台类游戏的技术指导文章,原文地址:http://higherorderfun.com/blog/2012/05/20/the-guide-to-implementing-2d-platformers/,作者是Rodrigo Monteiro。
本人最初阅读这篇文章时,参考的是cping1982的译文,地址为:https://blog.csdn.net/cping1982/article/details/7748338,但在阅读过程中觉得对其中的部分内容还是不太理解,于是又对照原文重新进行了翻译,并添加了一部分自己的理解(在译文中以PS:的形式标出),在此将该译文和大家分享,欢迎大家批评指点!
【版权声明】
原文作者未做权利声明,视为共享知识产权进入公共领域,自动获得授权。
以下为译文:
2D平台类游戏开发教程
鉴于目前关于2d平台类游戏开发的参考资料较少,本人试着列举出实现此类游戏的几种方案,并就这些方案的优缺点和实现中的细节进行探讨。
本文的目标是力图成为2d平台类游戏开发的简单详细的指导教程。如果您有任何反馈、更正、要求或需要补充的内容,敬请留言。
免责声明:本文列举的部分实现方法是从一些游戏所表现出的行为来进行的反向推测,而不是从游戏的真实代码或者开发者处得来的。很有可能这些游戏并不是像本文所述这样来实现的这些功能,而仅仅是最终的表现效果相同而已。另外要注意的是,这里的tile(瓦片)大小是为了实现游戏逻辑而设置的,美术素材的tile大小可能是不同的尺寸。
四种实现方案:
以下按照从简单到复杂的顺序,列举出四种实现平台类游戏的方案:
方案1:纯粹的tile(瓦片)渲染
角色被限制为按照组成地图的一个个tile进行整块移动,所以他绝不会站在两个相邻tile的中间(PS:从数字上讲,如果用坐标表示角色所在的tile位置,那么角色只能处在(0,0)(0,1)等整数位置上,而不会出现在(0,0.5)的位置上)。
在游戏逻辑上,角色总是要位于某个tile之上,但是在移动过程中,可以通过播放动画来实现平滑移动的效果(PS:即角色的坐标在逻辑上从(0,0)变为(0,1),但在视觉上看上去他是连续走过去的,而不是突然“瞬移”过去)。
这是实现平台类游戏最简单的方案,但却严重限制了玩家对角色的控制,使得它不适用于实现传统的动作平台类游戏(PS:如超级马里奥),但是却在某些解谜类或者“互动电影“类的平台游戏中很流行。
图1 Flashback(MD),已显示tile边框
采用此方案的游戏举例:
波斯王子(FC),TokiTori(不清楚作者指哪个游戏),淘金者(推测应是Lode Runner:The Legend Returns,在线网址https://www.myabandonware.com/game/lode-runner-the-legend-returns-2wy),Flashback(MD)
如何实现:
游戏场景由许多tile按照网格式的布局组成,每个网格通常会保存着一些信息,比如:此网格是否是障碍物,使用什么图片来渲染,角色在这个网格上移动时播放什么声音等。游戏中的角色是以组合在一起的tile集合的形式体现的,并且这些tile在角色移动时会一起移动。比如,在淘金者中,主角只用了一个tile来表示。而在TokiTori中,主角是由2*2的tile集合组成的(PS:即4个tile组成的正方形)。在Flashback中,由于该游戏的tile比其他游戏设计的要小,因此主角在站立时使用了两个tile宽,5个tile高(见图1),而蹲下时只用了3个tile高。
在这类游戏中,角色通常不会进行斜向移动,但如果确有需要,可以将斜向移动分解为横纵两步来执行。同样的道理,角色通常每次只移动一个tile的距离,但如果确有需要,可以将跨多个tile的移动拆分为对一个tile移动的多次重复(如在Flashback中,角色每次移动2个tile的距离)。
实现移动的算法如下:
这种移动方式非常不适合实现传统的动作平台游戏中那种弧线形跳跃。所以在这种方案实现的游戏中,主角通常根本就不会跳(TokiTori,LodeRunner),或者只能进行垂直或水平的跳跃(波斯王子,FlashBack),这些跳跃只不过是直线移动的一种特例而已(PS:如在Flashback中,主角向前跳跃时会播放一段跳跃的动画,然后就直接跳到相应的tile上去了)。
这个方案的优点是简单而精确,它使得游戏逻辑以一种确定的状态运行(PS:每次移动都是整数),减少了小bug的出现,也能让玩家体验到更强的控制感(PS:至少不会发生超级马里奥中那种穿墙或者跳石缝),开发者也不用费大力气去微调那些依赖于环境的参数(PS:如重力加速度)。这种方案和其他方案相比,能够轻而易举的实现某些特殊的结构(如可攀爬的平台,或者可单向穿过的平台),只需要检查主角的tile集合是否以某种方式和地图上代表特殊结构的tile集合对齐(PS:比如主角站在可攀爬平台边缘的左下方tile上),然后让主角采取相应的行动即可。
原则上讲,这种方案不允许角色进行小于1个tile的移动,但我们可以采取一些取巧的方法来绕过这些限制。比如,可以让主角由多个tile组成,这样地图上的tile就比主角整体要小了,按照tile移动时就会感觉更精细(比如主角使用2*6的tile集合,而一次移动1个tile),或者可以允许主角在tile上只进行视觉上的移动,而不影响逻辑(我认为Lode_runner-the legend Returns就是采取了这种解决方案)(PS:也就是说,从游戏逻辑上,主角并没有移动,只是渲染时偏移了一些)。
方案2 平滑的瓦片渲染
此方案和方案1一样,也通过tilemap来检测主角和环境之间的碰撞(PS:tilemap即方案1中所述的tile的网格布局,通常在游戏引擎中使用此术语),但是角色可以在场景中自由移动(通常是进行像素级的移动,即最小的移动距离为1像素,如果在游戏逻辑中使用了小数来表示角色的位置,那么可能会发生移动距离不到1像素的情况,此时可以对移动距离取整,详情可见后文的“更平滑的移动”一节中的处理方法)。这是8位机(FC)和16位机(MD、SFC)上最普遍的实现平台类游戏的方案,即使放到今天也是最流行的,比起其他更复杂的技术方案,它很容易实现,并且制作游戏关卡也更容易(PS:可以用Tiled等工具制作游戏关卡地图),同时还能实现斜坡运动和弧形跳跃。
如果你只是想做一款动作游戏,而不确定应该使用本文所述的那种方案来实现,那我建议你就选择此方案,因为它很灵活,并且实现起来相对简单,在四种方案中,能够提供最强的控制感。因此,大部分优秀的平台动作类游戏都使用这种方案实现也就不足为奇了。
图2 洛克人X(SFC),显示tile边缘和主角的碰撞体(红色部分)
采用此方案的游戏举例:
超级马里奥世界(SFC),刺猬索尼克(MD),洛克人(SFC),银河战士(SFC),魂斗罗(FC),合金弹头(ARC)等,基本上16位时代的所有平台类游戏都是采用的本方案
如何实现:
地图信息的存储方式和方案1相同(PS:即在每个tile上存储相关信息),区别仅是角色如何与背景之间进行交互。角色的碰撞体模型被称为轴对称边界盒(AABB盒,一个不能旋转的矩形)。通常AABB盒的大小是tile大小的整数倍,可以是一个tile宽,一个tile高(小马里奥,球形状态的Samus),也可以是2个tile高(大马里奥,洛克人,下蹲状态的Samus),或者3个tile高(站立状态的Samus)。一般来说,角色的精灵贴图应该比碰撞体大一些,这样可以得到更好的视觉效果和游戏体验(让玩家觉得“看上去被碰了,而实际没有被碰到”的体验要优于“看上去不应该被碰到,而实际却被碰到了”的体验要好多了)。比如在图2中,洛克人X的精灵贴图基本上是个正方形(实际贴图大小是2个tile宽),而他的碰撞体(PS:红色部分)是个长方形(只有1个tile宽)。
假设没有斜坡和单向平台,那么移动算法的实现很简单(PS:本人觉得这个算法应该还可以优化):
斜坡(PS:斜坡处理这段不是特别明白,按照个人理解翻译,欢迎探讨)
图3 洛克人X(SFC),对斜坡tile进行了标注
斜坡(图3绿色箭头所指的那些tile)处理起来十分棘手,因为这些tile既算做障碍物,却又允许角色移动到tile内部。角色在斜坡上进行X轴方向上的移动时,需要同时调整他在Y轴上的坐标。
一种解决方案是在每个tile的信息中保存下他们左右两侧的“地板y坐标”(PS:应该是以一个集合范围的形式来保存,比如左侧地板y值为0,右侧地板值y值为3,那么在tile中保存一个{0,3}的集合)。以图3为例,假设以第一块斜坡tile的左上角为坐标系的原点(0,0),那么位于洛克人X(PS:这个X应该指的是洛克人的名字= =!)左侧的那个斜坡tile(即绿色箭头所指的第一个斜坡tile)应该保存的“地板y坐标”的集合为{0,3}(即该tile左右两端“地板y坐标”组成的集合),然后洛克人X所站的那个斜坡tile的“地板y坐标”的集合为{4,7},然后是{8,11},{12,15}。这几块tile就组成了一个完整的斜坡,后面的斜坡只是他的重复而已,即再从左侧“地板y坐标”为0开始,直到右侧“地板y坐标”为15结束。从图3上可以看到,在两个稍缓的斜坡后面是一个陡峭一些的斜坡,这个斜坡由两块tile组成,即两个集合{0,7},{8,15},之后的陡峭斜坡也只是他的重复而已。
图4 两侧“地板Y坐标”表示为集合{4,7}的斜坡tile细节图,左侧地板距离顶部4像素,右侧地板距离顶部7像素
接下来要描述的算法允许任意数量的tile组成的斜坡,但是从视觉效果上考虑,图3中的这两种斜坡是最常见的。也就是说,通常只需要12个tile就能组成常用的斜坡了(其中6个tile是前面描述过的,即4个tile组成的缓坡,2个tile组成的陡坡,另外6个tile是他们的镜像,即从图3的角度来说是从左下向右上延伸的斜坡)。
接下来的斜坡算法,是在上面的普通移动算法的基础上完善而来的。首先,水平方向上的移动和碰撞处理算法应完善以下内容:
然后,垂直方向上的移动算法应完善以下内容:
单向平台
图5 超级马里奥世界,此图展示了马里奥从单向平台上下落和站在平台上的两种状态
单向平台是这样一种平台,角色可以踩平台上,也可从下方跳跃时穿过平台。换言之,这些平台只有在角色处于平台上方时才算作障碍物,其他情况下,它们是可以穿透的。理解单向平台的行为特点是实现它的关键。角色在通过单向平台时的移动算法应调整为:
可能有人想让单向平台在角色的y方向速度为正时(即角色正在下落)起到障碍物的作用,但这样的做法是错误的,原因是:也许角色在从平台下方起跳后,在达到跳跃最高点时和平台发生了重叠,但在下落的时候,角色的“脚部”并没有超过平台的上表面,在这种情况下,角色应该仍然会下落,而不是被平台挡住。
有些游戏允许角色从单向平台上“跳下”(PS:即直接从平台中部开始下落,而不是走到平台两边,通常使用下方向键配合某个操作键来实现)。有很多简单的办法来实现这种功能:比如,你可以在某一帧内禁用单向平台,并且给角色添加一个最少为1的y方向速度(此时角色在下一帧就不会在处于能和单向平台发生碰撞的状态了),还有一种办法是可以检测角色当前是否正站在一个单向平台上,如果是,手动把角色向下移动1像素(PS:移动后的角色不再满足和平台发生碰撞的条件,因此开始下落)。
梯子
图6 洛克人7(SFC),已显示tile边缘,绿色高亮处为表示梯子的tile,红色方框为主角在梯子上时的碰撞体
梯子看上去实现起来很复杂,但实际上只要简单的对角色进行状态转换即可——当角色在梯子上时,忽略角色的其他碰撞法则,而采用一些在梯子上特有的碰撞法则。大多数游戏中的梯子被设计为1个tile宽。
一般通过两种方法让角色来“进入梯子”:
进行上面两种操作后,立刻将角色的x坐标和梯子tile的坐标对齐。如果是从梯子上方向下爬时,需要向下移动一点角色的y坐标值,这样角色就处于梯子内部了。在这之后,有的游戏会用一个特殊的碰撞体来确定角色是否还处于梯子上,比如在洛克人中,应该就是使用了一个单独的tile来判断角色是否在梯子上(即只用了原来角色碰撞体的上半部分的1个tile,如图6中的红色方框)。
“离开梯子”也有几种方法:
一般来讲,当角色处于梯子上时,他的行为会受到限制,通常只能上下移动,有些游戏中也能够进行攻击。
楼梯
图7 恶魔城X(SFC),已显示tile边框
楼梯是梯子一种变体,但是在游戏中相对少见,其中最有代表性的就是恶魔城系列中的楼梯了。楼梯实现起来和梯子类似,但也有一些不同点:
有些游戏中的楼梯表现得像是斜坡,这些楼梯应该只是在视觉上做了一些效果,内部其实是斜坡的原理。
可移动平台
图8 超级马里奥世界(SFC)
可移动平台看上去很难实现,但其实很简单。和普通平台不同,可移动平台无法用固定的tile来表示,而是通过一个AABB盒来表示,也就是一个不能旋转的长方体。可移动平台在碰撞时作为普通碰撞体来处理,但如果不进行一些特殊处理,那你只会得到一个看起来“非常滑”的移动平台(意思是说平台只是自己在移动,而没有带动上面的游戏角色,所以角色像是在平台上打滑)
有很多种方法来实现可移动平台,其中一种如下:
其他特性
图9 索尼克2(MD)
有的游戏有一些更复杂且独有的特性,比如索尼克系列。这些内容超出了本文的讨论范围(也超出了我的知识范围!),也许会在今后的某篇文章中讨论。
(PS:关于索尼克的物理实现,可以参考这个网站:http://info.sonicretro.org/Sonic_Physics_Guide)
方案3 像素位掩码运算
这个方案和方案2类似,但不是用tile来检测碰撞,而是直接使用一张背景图,然后用图中的每个像素来进行碰撞检测。使用这种方案可以让游戏场景更加精细化,获得更好的细节处理,但实现起来的复杂度和内存使用率都会显著增加,而且需要用单独的图形编辑器来创建关卡。这就意味着,在创建场景时没法使用tile了,而是需要大量的美术工作来单独实现各个关卡。因为这些原因,这种方案并不常用,但却能获得比基于tile的处理方案更好的游戏质量。本方案比较适用于需要动态变化的游戏场景,比如《百战天虫》中的可以被炮弹破坏的场景——实现方法是可以在每一个像素上添加位掩码来改变场景。(PS:比如对每个像素使用标志位来决定是否能显示,如果炮弹炸出了一个圆,那就把这个圆范围内的所有像素隐藏,让他们看上去“消失”了)
图10 百战天虫,图中的所有 地形都是可破坏的
采用此方案的游戏举例:
百战天虫(PC), Talbot’s Odyssey(https://www.indiedb.com/games/talbots-odyssey-part-i)
如何实现:
基本思想和方案2是一样的,可以简单的把每个像素想象成一个tile,用相同的算法来实现所有的功能,但有一个功能例外,那就是斜坡。现在斜坡只由相互靠近的一个个像素点组成了,而不是像以前一样能用tile区分出来,所以之前的处理方式行不通了,需要更加复杂的算法。另外,梯子的实现现在也成了个棘手的问题。
斜坡:
图11 Talbot’s Odyssey,图中的红色区域是覆盖在场景上层的碰撞体掩码
这种方案之所以实现起来难度很大,主要就是因为斜坡不好处理。可是,使用这种方案来设计游戏的一大目的就是创造出精细而多变的地形,包括斜坡在内。如果因为实现起来困难而不在此方案中实现斜坡,那反而失去了使用这种方案的意义了。
以下是Talbot’s Odyssey针对斜坡所使用的算法的大致描述:
这个方案对引起角色向下运动的原始是下坡还是下落无法区分,所以需要构建一个系统来限制角色最后一次接触地面后要经过多少帧,才能跳跃并播放动画。在Talbot’s Odyssey中,这个值是10帧。
另一个处理移动的窍门是在碰撞发生之前就计算出来可以移动的像素值,然后再进行移动。还有一些更加复杂的要素,比如单向平台(可以采用和方案2相同的实现方式)和滑下陡坡(这些要素的实现超出了本文的范围)。通常这个方案需要进行许多调优工作,并且它从原理上就不如第二种方案运行得稳定。建议大家仅在需要创建复杂的地形时使用此方案。
方案 4 矢量地图
这种技术使用矢量数据(线条或多边形)来确定碰撞体的边界。它实现起来很难,但由于Box2D等物理引擎在游戏开发中的广泛应用,这种方案也逐渐流行起来。它不仅能提供类似方案3的精细化效果,同时消耗的内存更少,也提供了一种全新的方法来创建游戏关卡。
图12-13 Braid的关卡编辑器,上图为可视层,下图为碰撞体多边形层
采用此方案的游戏举例:
Braid(PC),Limbo(PC)
如何实现:
有两种实现方法:
显然第二种方法更流行(但我猜测Braid使用了第一种方法),因为它即简单,又能让你在游戏中实现一些其他的物理效果。但是在我看来,使用物理引擎时需要非常的谨慎,要避免让你的游戏和其他使用相同引擎的游戏看起来感觉差不多,那样的话就太无趣了。
复合物体:
这个方案有一些特有的问题,比如游戏可能会突然无法判断角色是否正站在地面上(大多出于对位置取整而造成的误差),或者无法判断角色是否撞到墙了,是否正在从斜面向下滑动等等。如果使用了物理引擎,摩擦力的设置也是个问题,可能需要在在角色的脚下设置较大的摩擦力,而在身体两侧设置较小的摩擦力。
处理这些问题的办法很多,一个流行的解决方案是让角色由代表不同身体部位的多边形组合起来。比如,可以创建一个主体躯干,然后加上一个细长的矩形代表角色的脚,再加上两个细长的矩形来代表角色的身体两侧,最后再加上头部或者其他部位。有时还可以使用一些尖状的碰撞体的来避免角色陷进某些障碍物中。组成角色的多边形碰撞体可以有不同的物理属性,当它们发生碰撞时,可以通过回调的方式来改变角色的状态。为了获取更多信息,可以使用传感器(只检测碰撞,不处理),通常它被用于确定角色是否离地面足够近,是否能够跳跃,或者是否在撞墙等状态。
全局考量
不管你选用哪种方案进行2D平台类游戏开发(第一种方案除外),都必须考虑一些全局的要素。
加速度
图14 超级马里奥世界(低加速度),超级银河战士(中等加速度),洛克人7(高加速度)
游戏角色的加速度是影响一个平台类游戏的最重要因素之一。加速度是速度的变化率。当加速度较小时,角色在起动时需要较长时间来达到最大速度,同时角色在玩家停止操作后需要过一段时间才能停下。较小的加速度会让玩家觉得角色“脚底打滑”,难于控制。这样的加速度设置在马里奥系列里很常见。当加速度很大时,角色只需很短的时间就能达到最大速度,或者干脆直接就从0跳转到最大速度,在玩家松开方向键的时候,角色就直接停下,由于角色的反应速度非常快,控制起来就像是“一触即发”,这样的加速度设置常见于洛克人系列(我认为洛克人实际上使用的是无限大加速度,即角色只能停止或者全速前进)。
即使游戏在水平移动时不设置加速度,至少在跳跃时需要考虑加速度,否则跳跃时的轨迹就不是弧线而是三角形的折线了。
如何实现:
实现加速度其实很简单,但要注意一些细节:
两种实现加速度的方法:
另外很重要的一点,一定要在角色移动前给把就把加速度的效果加上去,否则角色控制上就会有1帧的延迟现象。(PS:也就是说,当前帧的输入效果要在下一帧才体现出来)
当角色在移动过程中碰到障碍物时,应该把移动方向上的速度设置为0。
跳跃控制
图15 超级银河战士,Samus正在进行“太空跳跃”(另外还有“旋转攻击”的效果)
平台类游戏中最简单的处理跳跃方法就是:先检测角色是否站在地面上(或者检测他在前几帧是否站在地面上),如果在地面上,给角色添加一个y轴负方向上的初始速度(物理上称为冲量),然后剩下的工作交给重力完成就好。
有四种方法能够让玩家对跳跃附加一些控制:
动画和引导
图16 黑色荆棘(暴雪出品),角色在反向射击前会有一段很长的动画(按下Y键时)
在许多游戏中,角色在实际执行操作命令前,都会先播放一段动画。然而,在对操作反应要求非常高的游戏中,这么做会使玩家感到非常挫败,所以千万不要这么做。如果游戏非常依赖于操作和反应的速度,那就应该让玩家的每个操作立刻就能反映到角色身上。可以在跳跃或跑步时保留那些引导动画,但他们只是对动作做些装饰,实际的游戏逻辑应该立刻改变。
更平滑的移动
使用整数来表示角色的位置是很明智的,这可以让游戏更稳定,运行速度更快。但是,如果把所有的数值都取整了,那角色的运动可能会发生不稳定的情况(PS:由于取整时会丢掉小数部分,可能会造成角色移动的距离时大时小,从而看上去有点“抽动”)。有一些方法可以解决这个问题: