前几天比较无聊,在知乎上看到有人问怎么写Flappy Bird,于是乎想自己造一个。一是为了打发时间,二是想随便找个游戏引擎,看看自己能不能快速写一个出来。游戏其实很简单,主要就是绘制鸟与随机长度的水管,然后加一个碰撞检测就行了。说起来挺简单的,但是做起来还是有一些小细节需要处理。对C++不熟悉,于是找了个Java写的游戏引擎libgdx。有了引擎,绘图什么的都能方便点。至于碰撞检测的话,发现该引擎提供了一个物理引擎,box2d,据说用它就可以进行碰撞检测了。好了,下面就讲讲怎么造一个Flappy Bird吧。

1. 绘制主要元素

libgdx提供了非常简便的绘图API,我采用了Scene2d来绘图。整个界面相当于一个stage,里面有很多的actor,比如鸟啊,水管啊,地面啊什么的。给stage设置一个视图(viewpoint),就可以适配不同分辨率的屏幕了。stage会根据不同屏幕适配actor的大小。绘图时,只需要根据viewpoint的大小来设置actor的位置与大小。默认情况下,采用不间断地绘图机制,因此,不需要考虑手动更新界面。只需要写好绘图函数就行了。

整个游戏中主要的actor就是小鸟,地面,水管了。游戏过程中,小鸟在水平方向上其实并没有移动,只是通过地面以及水管的移动来展示一种小鸟在“向前飞”的效果。地面与水管以相同的速率匀速向左移动。对于小鸟,游戏没开始时,小鸟上下浮动;游戏开始后,在竖直方向上,自由落体。要显示三者的“运动”效果,只需要根据时间来改变绘制位置即可。libgdx可以获取到每次屏幕绘制的时间间隔,根据预设的移动速率或者移动规律,就可以在重绘时改变位置了。由于重绘间隔很短,也就显示出连贯的“运动”效果了。

1.1 绘制移动的地面

游戏场景中有一个不断向左移动的地面,以此来衬托出小鸟的移动。以屏幕左侧为线,向左移动整个地面,把地面不可见的部分补到可见部分的右端,这样就可以显示出连贯的移动画面了。可见与不可见部分,分别在原图上切割即可。

1.2 绘制移动的水管

观察原游戏可以得知,整个屏幕最多同时显示上下三组水管。因此,只需要维护三组水管即可。每组水管之间的距离一定,每组水管中,上下水管的长度随机生成,但是也不能太长或太短,上下水管之间的空隙取固定值。当一组水管向左移出屏幕后,就将其放置到最右侧水管的后面,同时更改随机生成的水管高度,此时该水管还不可见。

1.3 绘制小鸟

小鸟的运动相对于前两者而言就有些复杂了。扇动翅膀,上下浮动,旋转,自由落体等。

1.3.1 小鸟扇动翅膀

所谓“扇动翅膀”,只不过是循环地切换三幅图像而已,一幅翅膀向上,一幅水平,一幅向下。使用Animation每隔一段时间换一副图像。

1.3.2 小鸟上下浮动

这个也比较简单,设置上下两个阈值,然后不断地均匀改变小鸟的纵坐标。到达顶部则向下,到达底部则向上。

1.3.2 小鸟跃起与下落

当用户点击屏幕时,小鸟有一个向上跃起的动作。初始情况下,小鸟水平方向上没有速度,竖直方向上做自由落体运动。向上跃起时,水平方向依然没有速度,只不过是突然获得了一个向上的速度,使其向上移动了一段距离,由于重力的作用,向上的速率会逐渐减至零,然后又开始向下运动。上下运动的同时,鸟会调整其“抬头”的角度,即旋转角度。跃起与回落过程可以总结为以下几点:

  1. 突然获得向上的速度,使得小鸟向上移动,同时逐渐向逆时针方向旋转直到某个最大角度,如20°;
  2. 在重力作用下,小鸟速率降为零后,再次向下自由落体;
  3. 当向下的速度达到某个值时,小鸟开始“低头”,进行顺时针旋转,直到-90°

需要注意的是:

  • 小鸟下落一段距离后才开始顺时针旋转(“低头”),所以有一段时间小鸟一直高昂着头。
  • 小鸟抬头的速率比低头的速率大

2.构建box2d World,并与绘图匹配

由于要利用box2d进行碰撞检测,因此小鸟,地面,水管这三者都需要有与之对应的body。box2d通过各个bodyContact来反应是否碰撞。这样以来,就需要将body正确地“绘制”在box2d的世界中,使在屏幕上见到的图像与在box2d世界中模拟的对象位置、大小相对应,避免出现屏幕上已经相撞了,但是在box2d世界中并没有相撞的尴尬现象。

box2d通过body的位置以及形状来模拟一个对象。

2.1 形状

地面,水管的形状都是很规则的:地面为一个矩形,水管由上下两个宽度不同的矩形叠加。而小鸟则是一个不规则的多边形。三者都属于多边形,PolygonShape,只是构建方式不同:

  • 地面,直接给定位置,高度及宽度,然后setAsBox
  • 水管,则需要将其拆分为两个矩形,分别给出二者的四个顶点
  • 小鸟,与水管类似,也是先将小鸟近似地用多边形代替,然后给定顶点坐标。使用box2d-editor来确定顶点坐标。

2.2 位置

box2d中的坐标系与libgdx中的不同,在box2d中,坐标系原点在世界(屏幕)中间;libgdx的坐标原点在屏幕左下角。二者都是向上为Y轴正向,向右为X轴正向。

上述坐标系只能用来确定图像坐标原点的位置,绘制图像时,还要根据原点位置进行绘图。一般来说,绘图时图像的原点都在左下角。但是当直接给地面形状setAsBox时,其图像的坐标原点在图像中间

2.3 旋转

在旋转小鸟时,要考虑到旋转中心。libgdx通过使用setOrigin来设置旋转与缩放中心,但是不影响绘制。绘制时,还是以左下角代表图像的位置。而box2d以(0,0)为坐标原点与旋转中心,设置body的位置时,原点位置即代表body的位置,然后再根据顶点位置来勾勒出body的形状。顶点位置坐标都是相对于原点(0,0)而言的,因此坐标中可能会出现负数。

需要注意,libgdx中,旋转用角度表示;box2d用弧度表示。

2.4 坐标缩放

通过stageviewpoint来进行屏幕适配,在绘图时,指定的坐标都是图像原点在viewpoint中的坐标,stage会将该坐标缩放,然后绘制在屏幕上。而box2d的坐标又是由viewpoint中的坐标缩小一定比例而来(因为box2d中的坐标以“米”为单位,物体下落速度有限制,不能直接用像素坐标代替之,会导致下落时变成匀速运动,因而要将像素坐标进行缩放)。因此要将三者结合起来。

在确定小鸟与水管形状时,指定的顶点坐标都是在box2d中的坐标,是经过缩小的坐标。

3. 碰撞检测

成功地将box2d中的body与在屏幕上的图像匹配之后,box2d就能正确地检测出哪些物体接触了。通过setContactListener就能实时监听接触情况。当小鸟碰到水管时,地面与水管停止运动,但是小鸟还需要自由落体到地面;小鸟碰到地面时,box2d中的世界停止模拟,整个游戏停止。

同时还需注意,小鸟落地或者碰到水管时,box2d中模拟的body回对碰撞进行一些物理反应,改变位置或者角度什么的。因此,需要将小鸟当成传感器(isSensor),不需要其对碰撞做出反应。

4. 资源文件

资源文件可以从原版APK中提取,只不过该文件是已将所有图片合成为一张的atlas版本,由于原作是用AndEngine开发的,libgdx无法直接解析。参考这篇文章可以将其分割,然后利用libgdx的合成工具Texture-packer再次合成并在项目中解析。

(完)

源码见https://github.com/YieldNull/FlappyBird