这是一篇实现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的距离)。

实现移动的算法如下:

  1. 在主角要移动到的位置上创建一个副本(例如,要向右移动一个tile,那么先将主角的所有tile在右移1个tile的位置上创建一个副本)。
  2. 检查副本是否和背景中的某些部分或者其他角色产生了重叠。
  3. 如果有重叠,那么说明主角的移动被挡住了,此时应做出相应的反应(PS:如阻止移动或进行某些交互)。
  4. 如果副本没有和任何物体发生重叠,则表示主角可以移动,把主角移动到相应的位置,同时可以播放一些动画来让移动看上去更自然。

这种移动方式非常不适合实现传统的动作平台游戏中那种弧线形跳跃。所以在这种方案实现的游戏中,主角通常根本就不会跳(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:本人觉得这个算法应该还可以优化):

  1. 先将移动分解为X轴和Y轴,每次处理一个轴上的移动。如果后续还想实现斜坡运动,那么应先处理X轴,再处理Y轴。如果不考虑斜坡,那么处理顺序就不重要了。针对每个轴,实施下面的第2到6步。
  2. 获取角色碰撞体在该轴向上的“最前方”边界坐标:比如角色向左走,那么就取碰撞体最左侧的x坐标;如果向右走,就取碰撞体最右侧的x坐标;如果向上走,就取碰撞体最上方的y坐标,以此类推。
  3. 获取角色碰撞体在该轴向上的“最前方”的边界线(PS:即第2步获得边界坐标所在的线)所“压到”的背景tile的位置(PS:即在tilemap中的位置表示)——进而得到这些tile在角色移动方向正交轴上的取值范围。举例来说,假设角色要向左走(X轴移动),他的碰撞体的左侧边界线“压到”了背景上(10,32)(10,33)(10,34)三块tile(PS:为了好理解,假设角色在X方向上的第10列tile上),那么,可以在Y轴方向上得到一个取值范围,即从32到34(这些tile的实际像素位置的y值应该是y=32*TS,y=33*TS,y=34*TS,TS代表tile的大小)。
  4. 从第三步得到的角色“压到”的那组背景tile出发(PS:即(10,32)(10,33)(10,34)三块tile),沿着角色前进的方向,逐行查找距离角色最近的静态障碍物(PS:即先沿着第32行出发,找到32行上的最近障碍物,然后33行,34行)。之后扫描场景中的每个可移动障碍物,最后在这些静态和动态障碍物中,选择出在角色前进方向上离得最近的障碍物。
  5. 计算角色和最近障碍物之间的距离,然后把这个距离和角色本来想要移动的距离做比较,取两者的最小值作为角色在该轴向上的实际移动距离。
  6. 按照第5步得到的实际移动距离,移动角色到新位置。然后从这个新位置开始,再处理其他轴向(如y轴)上的移动,直到所有的轴向都处理完毕。

斜坡(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的角度来说是从左下向右上延伸的斜坡)。

接下来的斜坡算法,是在上面的普通移动算法的基础上完善而来的。首先,水平方向上的移动和碰撞处理算法应完善以下内容:

  1. 确保先处理X轴方向上的移动,后处理Y轴方向上的移动。
  2. 在上面描述的普通移动算法的第4步(检测障碍物)中,如果检测到的斜坡tile离主角最近的边是它的看上去高一些的那个边时(也就是“地板y坐标”小一些的那个边,比如集合{0,3}中0所在的那个边,或者集合{4,7}中4所在的那个边),就把这个斜坡tile算作一个普通障碍物,和主角发生碰撞并阻挡住主角前进,这样可以防止角色从斜坡的背面穿过来。
  3. 可以通过禁止斜坡(PS:即把斜坡tile当做一般障碍物来处理,阻挡住主角前进)的方式,来阻止角色直接走上“半个斜坡”(PS:“半个斜坡”应该是指由上述的斜坡tile组合中的一半构成的斜坡,即在4个tile的斜坡组合中,只使用左侧2个tile组成一个斜坡,从图上看就是没有{8,11}{12,15}这两个tile,而直接用{0,3}{4,7}来组成一个斜坡,假如角色从右侧走向这样的半个斜坡,他应该被挡住,而不应该直接走上去,如果想上去,需要跳跃)。这种处理方式被用于洛克人X和许多其他游戏上。如果不这样处理,那么当角色试图从“半个斜坡”的低端走上来时就需要进行更复杂的处理——一种方法是对整个关卡进行预处理,标记出所有这样的“非常规”斜坡tile(PS:即这些“半个斜坡”),然后,在碰撞检测时,就算角色的最低y坐标(PS:角色脚底y坐标)要大于这“半个斜坡”较低一侧的“地板y坐标”的世界坐标值(世界坐标值的计算的方法为“半个斜坡的tile所在的地图位置编号”*“tile大小”+“低侧地板y坐标”),也要把这个“半个斜坡”的tile算作普通障碍物,和角色发生碰撞并阻挡住角色移动。
  4. 如果角色所在的斜坡连接着一个普通的障碍物tile,那么这个障碍物tile在角色移动时不应该参与碰撞检测。也就是说,如果角色站在(这里指角色底部的中央像素所处的位置)在一个由{0,*}(PS:比如角色站在{0,3}的tile上)的集合组成的斜坡上,那么连接着这个斜坡左侧的那个障碍物tile就不应该参与碰撞,如果在橘色站在一个由{*,0}(PS:也就是之前那个斜坡的镜像)的集合组成的斜坡上,那么连接着这个斜坡右侧的那个障碍物tile就不应该参与碰撞。如果你的角色比两个tile还宽,那可能就需要忽略掉更多的普通障碍物tile——如果角色正在向斜坡上方移动时,你甚至可以简单的直接忽略和斜坡连接的整行的tile的碰撞检测。这么做的原因是为了防止角色爬坡时被这些障碍物tile卡住(即图3中黄色高亮的tile,这些高亮的tile和斜坡相连),造成角色卡住的原因是:如果这样的障碍物tile参与了碰撞检测,那么在角色从斜坡下面走到那个障碍物tile上方之前,角色脚下的坐标还是要比障碍物tile的表面坐标要低(PS:如果以屏幕左上角为坐标原点的话,从y坐标的值来讲应该是要大,因此会被判定为无法通过这个tile,但实际应该达到的效果是要走到这个tile上面去)。

然后,垂直方向上的移动算法应完善以下内容:

  1. 如果用重力来实现角色的下坡效果,那么要确保重力作用下的最小位移量要和角色的水平速度相匹配。比如:在由4块tile组成的斜坡上(比如上图的{4,7}这种斜坡),重力形成的位移至少要是水平速度的1/4(取整数)(PS:个人认为这部分描述的不太清楚,其实整体的理念应该是让水平速度和垂直速度的矢量和的方向与斜面方向一致,可以参考文章https://www.cnblogs.com/undefined000/p/platformer-slope-physics.html)。在由2块tile组成的斜坡上(如{0,7}这种斜坡),重力形成的位移至少要是水平速度的1/2。如果不这么做,那角色在下滑的过程中就可能会在水平方向上离开斜坡一会,然后在重力的作用下被拉回到斜坡上,使得角色看上去在斜坡上“一弹一弹”的下落,而不是光滑的沿斜面下滑。
  2. 如果不用重力,另一种方法是直接计算出角色移动前距离“地面”多少像素,移动后距离“地面”多少像素(用下方的公式),然后调整角色的位置使这两个值一致(PS:这里的地面位置应该根据斜坡的具体样式来确定,即让角色运动后直接向着地面方向“贴过去”)。
  3. 向下移动时,不应把斜坡tile的顶边作为碰撞边界,而是应该计算出斜坡在角色所处的x坐标上的“地面”像素所在的位置,并使用这个“地面”的坐标值作为碰撞边界。计算斜坡中的“地面”坐标的方法是,先找到角色当前x坐标在在所在斜坡tile上的归一化值,这个值应位于[0,1]之间(0代表左边界,1代表右边界),然后通过线性插值的方法计算出这个x坐标对应的“地面y坐标”的值。代码如下:
    float t = float(centerX - tileX) / tileSize;(PS:归一化x坐标) 
    float floorY = (1-t) * leftFloorY + t * rightFloorY;(PS:线性插值)
  4. 向下移动时,如果在相同的y坐标上有多个tile作为障碍物出现,并且角色当前的中心点x坐标下方的tile是斜坡(PS:也就是说角色还站在斜坡上),那么在碰撞检测时应该先处理和这个斜坡tile的碰撞,而忽略其他的障碍物tile,即便那些障碍物tile在距离上离角色更近。这样做能保证角色在斜坡底端移动时的正确行为,解决由于在斜坡上的距离调整而使得角色看上去“陷入”了一个一般障碍物tile的问题。

单向平台

图5  超级马里奥世界,此图展示了马里奥从单向平台上下落和站在平台上的两种状态

单向平台是这样一种平台,角色可以踩平台上,也可从下方跳跃时穿过平台。换言之,这些平台只有在角色处于平台上方时才算作障碍物,其他情况下,它们是可以穿透的。理解单向平台的行为特点是实现它的关键。角色在通过单向平台时的移动算法应调整为:

  1. 在x轴方向上,单向平台永远不是障碍物。
  2. 在y轴方向上,只有在角色移动只前完全处于平台上方时(即角色最底部的y像素值至少要比平台最顶部的y像素值高1个像素),才把单向平台算做障碍物。为了检测角色是否完全处于平台上方,你需要让角色在移动之前先记录下所在的初始位置。

可能有人想让单向平台在角色的y方向速度为正时(即角色正在下落)起到障碍物的作用,但这样的做法是错误的,原因是:也许角色在从平台下方起跳后,在达到跳跃最高点时和平台发生了重叠,但在下落的时候,角色的“脚部”并没有超过平台的上表面,在这种情况下,角色应该仍然会下落,而不是被平台挡住。

有些游戏允许角色从单向平台上“跳下”(PS:即直接从平台中部开始下落,而不是走到平台两边,通常使用下方向键配合某个操作键来实现)。有很多简单的办法来实现这种功能:比如,你可以在某一帧内禁用单向平台,并且给角色添加一个最少为1的y方向速度(此时角色在下一帧就不会在处于能和单向平台发生碰撞的状态了),还有一种办法是可以检测角色当前是否正站在一个单向平台上,如果是,手动把角色向下移动1像素(PS:移动后的角色不再满足和平台发生碰撞的条件,因此开始下落)。

梯子

图6  洛克人7(SFC),已显示tile边缘,绿色高亮处为表示梯子的tile,红色方框为主角在梯子上时的碰撞体

梯子看上去实现起来很复杂,但实际上只要简单的对角色进行状态转换即可——当角色在梯子上时,忽略角色的其他碰撞法则,而采用一些在梯子上特有的碰撞法则。大多数游戏中的梯子被设计为1个tile宽。

一般通过两种方法让角色来“进入梯子”:

  1. 让角色的碰撞体和梯子重叠,在地上或空中都可以,然后按上键(有的游戏中也可以按下键)。(PS:上梯子)
  2. 让角色站在一个代表“梯子顶部”的tile之上(通常这个tile被设定为类似单向平台,角色可以在上面移动,也可以从下方穿过这个tile),然后按下键。(PS:下梯子)

进行上面两种操作后,立刻将角色的x坐标和梯子tile的坐标对齐。如果是从梯子上方向下爬时,需要向下移动一点角色的y坐标值,这样角色就处于梯子内部了。在这之后,有的游戏会用一个特殊的碰撞体来确定角色是否还处于梯子上,比如在洛克人中,应该就是使用了一个单独的tile来判断角色是否在梯子上(即只用了原来角色碰撞体的上半部分的1个tile,如图6中的红色方框)。

“离开梯子”也有几种方法:

  1. 角色接触到梯子顶部。通常会播放一段动画并把角色向上移动几个像素,然后他就站在梯子的上面了。
  2. 接触到梯子底部。此时只要让角色直接下落就可以,不过有些游戏不允许角色这样离开梯子。
  3. 左右移动离开梯子。如果梯子两侧的tile不是障碍物,那么角色可以从两侧离开梯子。
  4. 通过跳跃离开梯子。有些游戏允许角色这样做。

一般来讲,当角色处于梯子上时,他的行为会受到限制,通常只能上下移动,有些游戏中也能够进行攻击。

楼梯

图7  恶魔城X(SFC),已显示tile边框

楼梯是梯子一种变体,但是在游戏中相对少见,其中最有代表性的就是恶魔城系列中的楼梯了。楼梯实现起来和梯子类似,但也有一些不同点:

  1. 角色可以每次移动一个或者半个tile(比如在恶魔城X中)。
  2. 每次移动时,会在x轴和y轴方向同时移动预定好的距离。
  3. 角色在上楼梯时,要在楼梯tile的前一个tile上就进行碰撞检测(PS:应该是临近第一级楼梯的那个tile,即图7中楼梯左侧那个tile),让角色进入“上楼梯”的状态,而不是碰到了楼梯tile时才检测。

有些游戏中的楼梯表现得像是斜坡,这些楼梯应该只是在视觉上做了一些效果,内部其实是斜坡的原理。

可移动平台

图8  超级马里奥世界(SFC)

可移动平台看上去很难实现,但其实很简单。和普通平台不同,可移动平台无法用固定的tile来表示,而是通过一个AABB盒来表示,也就是一个不能旋转的长方体。可移动平台在碰撞时作为普通碰撞体来处理,但如果不进行一些特殊处理,那你只会得到一个看起来“非常滑”的移动平台(意思是说平台只是自己在移动,而没有带动上面的游戏角色,所以角色像是在平台上打滑)

有很多种方法来实现可移动平台,其中一种如下:

  1. 在移动场景中的所有物体之前,先判断角色是否处于一个可移动平台上。例如,可以通过检测角色底部中心坐标是否在可移动平台上方1像素的位置来判断。如果角色处在可移动平台上,在主角的对象的内部保存这个平台处理接口和平台位置。(PS:应该是在角色类的内部保存一个平台类的对象,如果角色站在可移动平台上时,由这个对象来对角色进行位置调整)
  2. 先让所有的可移动平台移动,确保对可移动平台的位置调整在主角移动前完成。
  3. 可移动平台移动后,得到它在x轴和y轴方向上的移动距离,然后直接修改平台上的每个角色的位置,让他们在x轴和y轴上加上和可移动平台相同的移动距离。
  4. 修改完站在平台上的角色的位置后,再按普通的移动处理方式,处理他们自身的移动。(PS:这样处理过后,角色就会随着平台移动了,就像现实中一样)

其他特性

图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针对斜坡所使用的算法的大致描述:

  1. 通过加速度和速度,计算出角色要移动的距离(可以将向量分解到每个轴上分别求解)。
  2. 先处理移动距离大的那个轴的方向。
  3. 在水平移动时,每次启动时要附加向上提升角色的AABB盒3个像素,这样角色就可以爬上斜坡了。
  4. 扫描角色移动的前方,通过前方的障碍物和位掩码的值,来决定角色碰到障碍物前能移动多少像素,然后移动角色到这个新的位置。
  5. 如果是水平移动,将角色向上提升需要的像素数(Talbot’s Odyssey中使用的值为3)来满足爬坡的需要。
  6. 如果在移动结束后,角色的任何像素和障碍物产生了重叠,则取消这个轴上的移动。
  7. 不管之前那个方向的移动结果如何,在另一个方向上进行相同的移动处理。

这个方案对引起角色向下运动的原始是下坡还是下落无法区分,所以需要构建一个系统来限制角色最后一次接触地面后要经过多少帧,才能跳跃并播放动画。在Talbot’s Odyssey中,这个值是10帧。

另一个处理移动的窍门是在碰撞发生之前就计算出来可以移动的像素值,然后再进行移动。还有一些更加复杂的要素,比如单向平台(可以采用和方案2相同的实现方式)和滑下陡坡(这些要素的实现超出了本文的范围)。通常这个方案需要进行许多调优工作,并且它从原理上就不如第二种方案运行得稳定。建议大家仅在需要创建复杂的地形时使用此方案。

方案 4 矢量地图

这种技术使用矢量数据(线条或多边形)来确定碰撞体的边界。它实现起来很难,但由于Box2D等物理引擎在游戏开发中的广泛应用,这种方案也逐渐流行起来。它不仅能提供类似方案3的精细化效果,同时消耗的内存更少,也提供了一种全新的方法来创建游戏关卡。

图12-13 Braid的关卡编辑器,上图为可视层,下图为碰撞体多边形层

采用此方案的游戏举例:

Braid(PC),Limbo(PC)

如何实现:

有两种实现方法:

  1. 自己解决物体移动和碰撞检测,采取类似方案3的方法,使用多边形的角度来计算偏转和倾斜。(PS:这句没看懂)
  2. 使用物理引擎,比如Box2D。

显然第二种方法更流行(但我猜测Braid使用了第一种方法),因为它即简单,又能让你在游戏中实现一些其他的物理效果。但是在我看来,使用物理引擎时需要非常的谨慎,要避免让你的游戏和其他使用相同引擎的游戏看起来感觉差不多,那样的话就太无趣了。

复合物体:

这个方案有一些特有的问题,比如游戏可能会突然无法判断角色是否正站在地面上(大多出于对位置取整而造成的误差),或者无法判断角色是否撞到墙了,是否正在从斜面向下滑动等等。如果使用了物理引擎,摩擦力的设置也是个问题,可能需要在在角色的脚下设置较大的摩擦力,而在身体两侧设置较小的摩擦力。

处理这些问题的办法很多,一个流行的解决方案是让角色由代表不同身体部位的多边形组合起来。比如,可以创建一个主体躯干,然后加上一个细长的矩形代表角色的脚,再加上两个细长的矩形来代表角色的身体两侧,最后再加上头部或者其他部位。有时还可以使用一些尖状的碰撞体的来避免角色陷进某些障碍物中。组成角色的多边形碰撞体可以有不同的物理属性,当它们发生碰撞时,可以通过回调的方式来改变角色的状态。为了获取更多信息,可以使用传感器(只检测碰撞,不处理),通常它被用于确定角色是否离地面足够近,是否能够跳跃,或者是否在撞墙等状态。

全局考量

不管你选用哪种方案进行2D平台类游戏开发(第一种方案除外),都必须考虑一些全局的要素。

加速度

图14  超级马里奥世界(低加速度),超级银河战士(中等加速度),洛克人7(高加速度)

游戏角色的加速度是影响一个平台类游戏的最重要因素之一。加速度是速度的变化率。当加速度较小时,角色在起动时需要较长时间来达到最大速度,同时角色在玩家停止操作后需要过一段时间才能停下。较小的加速度会让玩家觉得角色“脚底打滑”,难于控制。这样的加速度设置在马里奥系列里很常见。当加速度很大时,角色只需很短的时间就能达到最大速度,或者干脆直接就从0跳转到最大速度,在玩家松开方向键的时候,角色就直接停下,由于角色的反应速度非常快,控制起来就像是“一触即发”,这样的加速度设置常见于洛克人系列(我认为洛克人实际上使用的是无限大加速度,即角色只能停止或者全速前进)。

即使游戏在水平移动时不设置加速度,至少在跳跃时需要考虑加速度,否则跳跃时的轨迹就不是弧线而是三角形的折线了。

如何实现:

实现加速度其实很简单,但要注意一些细节:

  1. 判断x方向上的要达到的目标速度。如果玩家不操作时应该是0,按左方向键时,目标速度应该是预设的角色最大速度的负值,按右方向键时,目标速度应该是最大速度的正值。
  2. 判断y方向上的目标速度。当角色站在平台上时应该是0,其他情况下角色在y方向上的能达到的最大速度应该是临界速度。(PS:可能是指下落时重力和阻力平衡状态时的速度)
  3. 在每个轴的方向上,使用加速度参数,向着目标速度来不断修改角色的当前速度,修改的方式有以下两种:加权平均或者直接在速度上增加加速度的值。

两种实现加速度的方法:

  1. 加权平均:假设加速度为a,取值范围从0(没有加速度)到1(加速度最大,瞬间达到最大速度)。对当前速度和目标速度,用a做线性插值计算,然后把结果重新赋给当前速度,代码如下:
    vector2f curSpeed = a * targetSpeed + (1-a) * curSpeed;
    if (fabs(curSpeed.x) < threshold) curSpeed.x = 0;
    if (fabs(curSpeed.y) < threshold) curSpeed.y = 0;
  2. 直接增加加速度的值:首先决定加速度的方向(使用sign函数获取加速度的符号(大于0时返回1,小于0时返回-1)),然后检测当前速度是否超过了最大速度。
    vector2f direction = vector2f(sign(targetSpeed.x - curSpeed.x),sign(argetSpeed.y - curSpeed.y));
    curSpeed += acceleration * direction;
    if (sign(targetSpeed.x - curSpeed.x) != direction.x) curSpeed.x = targetSpeed.x;
    if (sign(targetSpeed.y - curSpeed.y) != direction.y) curSpeed.y = targetSpeed.y;

另外很重要的一点,一定要在角色移动前给把就把加速度的效果加上去,否则角色控制上就会有1帧的延迟现象。(PS:也就是说,当前帧的输入效果要在下一帧才体现出来)

当角色在移动过程中碰到障碍物时,应该把移动方向上的速度设置为0。

跳跃控制

图15  超级银河战士,Samus正在进行“太空跳跃”(另外还有“旋转攻击”的效果)

平台类游戏中最简单的处理跳跃方法就是:先检测角色是否站在地面上(或者检测他在前几帧是否站在地面上),如果在地面上,给角色添加一个y轴负方向上的初始速度(物理上称为冲量),然后剩下的工作交给重力完成就好。

有四种方法能够让玩家对跳跃附加一些控制:

  1. 冲量:在超级马里奥世界和索尼克等游戏中可以看到,在角色跳跃前保存动能(在程序的实现上,就是添加一个初始速度)。在有些游戏中,这是唯一能影响跳跃曲线的方法,就像在现实中一样。角色会在物理作用下自然的进行跳跃,不需要再进行什么干预,除非你想打断这种跳跃状态。
  2. 空中加速:就是值角色处在半空中时,仍然让玩家能够控制他左右移动。虽然这在物理上似乎不合理,但却是一个非常流行的要素,可以使得角色的可控性更强。几乎所有的平台类游戏都使用了这个特性,除了像波斯王子那样的。通常来说,空中的加速度会迅速降低,所以,添加合适的冲量很重要,但有些游戏(如洛克人)会给玩家完全的空中控制角色的能力。一般的实现方式是当角色在空中的时候可以通过操作来修改加速度参数。
  3. 上升控制:这是另外一个在物理上不合理的动作,但是也非常流行,也能让角色的可控性更强。玩家按下跳跃键的时间越长,角色跳的越高。典型的实现方式是在跳跃按钮被按下时连续给角色添加冲量(同时这些冲量的值可以逐渐减小),或者在跳跃按钮被按住时禁用重力。重要的一点是要限制跳跃的时间,除非你希望角色能跳的无限高。
  4. 多段跳:有的游戏允许玩家在空中再次跳跃,可以是无限次的再跳跃(比如超级银河战士中的Space Jump或者Talbot’s Odyssey中的飞行),或者是角色落地前的有限次跳跃(二段跳使用的最普遍)。可以通过在角色内维持一个计数器来实现多段跳的功能,这个计数器没有超过限值时,角色就可以进行多段跳,每进行一次跳跃就增加计数器,落地时重置计数器(更新计数器的时候一定要谨慎,以防落地后计数器不能清零)。多段跳通常会有一些限值,比如第二次跳的比第一次低,或者要完成某些特定动作,比如Samus的Space Jump只能在刚完成旋转跳跃并要下落时候才能触发。

动画和引导

图16  黑色荆棘(暴雪出品),角色在反向射击前会有一段很长的动画(按下Y键时)

在许多游戏中,角色在实际执行操作命令前,都会先播放一段动画。然而,在对操作反应要求非常高的游戏中,这么做会使玩家感到非常挫败,所以千万不要这么做。如果游戏非常依赖于操作和反应的速度,那就应该让玩家的每个操作立刻就能反映到角色身上。可以在跳跃或跑步时保留那些引导动画,但他们只是对动作做些装饰,实际的游戏逻辑应该立刻改变。

更平滑的移动

使用整数来表示角色的位置是很明智的,这可以让游戏更稳定,运行速度更快。但是,如果把所有的数值都取整了,那角色的运动可能会发生不稳定的情况(PS:由于取整时会丢掉小数部分,可能会造成角色移动的距离时大时小,从而看上去有点“抽动”)。有一些方法可以解决这个问题:

  1. 使用浮点数来计算和保存位置,然后在渲染和碰撞检测时转换为整数。这种方法快速而简单,但是如果角色移动的离原点(0,0)太远,这种处理方法就会开始丢失精度。如果你的游戏区域没有那么大,就不用担心这个问题,只要留意一下就行了,即便发生了丢失精度的问题,也可以使用双精度值来代替浮点数,从而提高精度。
  2. 使用定点数来进行计算并保存位置,然后在渲染和碰撞检测时转换为整数。这样做要比使用浮点数精度低一些,并且有范围限制,但是可以让全局的数值精度保持一致,从而在某些硬件上运算得更快(尤其是在一些手机上,浮点数的计算很慢)。
  3. 角色的位置信息由整数部分和代表小数部分的“余数”共同组成,然后把整数部分存储为为整形变量,把余数部分存储为浮点型变量。当角色移动时,预计要移动的距离是一个小数,先把这个小数和角色位置信息中的“余数”相加,把相加后的结果的整数部分再加到角色位置信息的整数部分上,再把结果剩下的小数部分作为新的位置的“余数”保存起来,以供下一帧移动时使用。这种方法的优点是可以在除了移动以外的任何地方使用整数而不是小数,进而提升计算效率。同时,这种技术也适合用在一些需要把对象位置体现为整数的框架中,或者用在那些虽然用小数保存对象位置,但是渲染时会直接把小数值传递给渲染系统的框架中——在这样的框架中,你可以在系统提供的浮点型变量里只保存整数,进而把整数传递给渲染系统,来保证渲染时能够对齐到像素。

2D平台类游戏开发教程(翻译)相关推荐

  1. 【cocos 2d微信小游戏开发教程】基础使用笔记分享(三)

    富文本(RichText) 优点:自定义颜色,大小,描边,还能加图片.对于复杂的文本表现力更好. 缺点:cocos的富文本是由Label组件拼装实现的.低版本会打断合批.Label太多导致卡顿. 常用 ...

  2. Unity 2D游戏开发教程之使用脚本实现游戏逻辑

    Unity 2D游戏开发教程之使用脚本实现游戏逻辑 使用脚本实现游戏逻辑 通过上一节的操作,我们不仅创建了精灵的动画,还设置了动画的过渡条件,最终使得精灵得以按照我们的意愿,进入我们所指定的动画状态. ...

  3. Unity 2D游戏开发教程之摄像头追踪功能

    Unity 2D游戏开发教程之摄像头追踪功能 上一章,我们创建了一个简单的2D游戏.此游戏中的精灵有3个状态:idle.left和right.这看起来确实很酷!但是仅有的3个状态却限制了精灵的能力,以 ...

  4. 微信小游戏开发教程-2D游戏原理讲解

    微信小游戏开发教程-2D游戏原理讲解 原理 为了更加形象的描述,这里先上一张图: 背景 a. 首先,我们看到背景好像是一张无限长的图片在向下移动.实际则不然,这是一张顶部和底部刚好重叠的图片.这是一种 ...

  5. Unity 2D游戏开发教程之游戏中精灵的跳跃状态

    Unity 2D游戏开发教程之游戏中精灵的跳跃状态 精灵的跳跃状态 为了让游戏中的精灵有更大的活动范围,上一节为游戏场景添加了多个地面,于是精灵可以从高的地面移动到低的地面处,如图2-14所示.但是却 ...

  6. Unity 2D游戏开发教程之为游戏场景添加多个地面

    Unity 2D游戏开发教程之为游戏场景添加多个地面 为游戏场景添加多个地面 显然,只有一个地面的游戏场景太小了,根本不够精灵四处活动的.那么,本节就来介绍一种简单的方法,可以为游戏场景添加多个地面. ...

  7. Unity 2D游戏开发教程之精灵的死亡和重生

    Unity 2D游戏开发教程之精灵的死亡和重生 精灵的死亡和重生 目前为止,游戏项目里的精灵只有Idle和Walking这两种状态.也就是说,无论精灵在游戏里做什么,它都不会进入其它的状态,如死亡.于 ...

  8. 用Unity开发2D消除类游戏的素材资源精选

    本文精选了一些用Unity制作2D消除类游戏的UI素材.音频资源和完整项目. 常见的消除类游戏种类有:三消.六边形三消.点点消.连连消.泡泡龙类型消除.连连看.1024类型消除等.也有各种各样和其他元 ...

  9. unity 2d 游戏开发教程(2d战棋)

    unity 2d 游戏开发教程(2d战棋) 类似的游戏有:火焰纹章,梦幻模拟战 先上效果 源码领取方式:私信发送 2D战棋资料领取 这是 unity3d 战棋游戏开发 专题的内容拓展 这个专题完整的讲 ...

最新文章

  1. sybase数据库导出mysql_sybase导出数据库的表结构命令
  2. 【行业应用】一文讲通电力数字化转型
  3. .NET Core实战项目之CMS 第十六章 用户登录及验证码功能实现
  4. VC中设置头文件的搜索路径~~
  5. 用python写石头剪刀布_Python实现简单石头剪刀布游戏
  6. JavaScript基本数据类型讲解
  7. string 释放_由String,String Builder,String Buffer 引起的面试惨案
  8. 25 岁的 JavaScript 都经历了什么?
  9. 8-4 测试http服务器(上)
  10. [WebKit] JavaScriptCore解析--高级篇(一) SSA (static single assignment)
  11. android view state,Android状态系统(二)——View状态组合
  12. hg diff仅对当前目录下的文件有效
  13. Hibernate完全自学手册
  14. vim字符串全局替换
  15. git ssh密钥生成与配置
  16. 这条命令帮我在一分钟内修改了200台远程服务器密码!
  17. 纪念第一次2019河南省第十二届ACM大赛之旅
  18. windows 无法更新计算机启动配置,“windows 无法更新计算机的启动配置。安装无法继续”这样解决...
  19. 线段树辅助——扫描线法计算矩形面积并
  20. Django+Vue开发生鲜电商平台之2.开发环境搭建

热门文章

  1. 『Backup』不用JTAG修好你的Lumia死砖?绝对行!
  2. 光纤收发器怎么连接?光纤收发器连接方式解析
  3. SpringBoot实现zip文件下载
  4. P问题、NP问题、NPC问题的概念及实例证明
  5. 威联通建php邮件服务器_威联通TS-563虚拟机安装LEDE+单网口NUC+VLAN配置(网件GS105V2)...
  6. 数字图像处理吴娱课后答案_何东健数字图像处理课后答案
  7. LED与照明光学基础知识
  8. 公司财务发工资时,记录了当时发工资的资料Employee.txt 1.定义公司员工类Employee,属性有:工号,姓名,性别,工资(double类型),进行属性的隐藏和封装,重写toString.
  9. 【HJ42 学英语】C++
  10. PBX220评测报告