1 赞同
4 收藏

本文节选自《趣学Python游戏编程》。

我们将学习制作曾经风靡一时的移动游戏——Flappy Bird,游戏中玩家需要控制一只小鸟来飞越重重障碍。我们将使用之前学到的技能来完成这款游戏。同时为了让游戏的画面更加生动,我们将着重讨论如何滚动显示游戏的背景图像,以及播放小鸟角色的飞行动画。此外,我们还将对游戏的图形用户界面设计进行介绍。

主要涉及如下知识点:

  • 滚动背景图像
  • 模拟重力效果
  • 播放角色动画
  • 设计图形用户界面
  • 播放游戏音乐

1 创建游戏场景

1.1设置背景图像

相信经过之前的游戏制作学习,大家都十分清楚游戏设计的第一步就是创建游戏场景。我们首先设置常量WIDTH和HEIGHT的值,用来确定场景的大小。我们在程序的开头编写如下代码:

WIDTH = 138 * 4          
HEIGHT = 396

你或许会觉得奇怪,这里的WIDTH和HEIGHT值代表什么含义呢?为何不像之前游戏中那样,将场景的宽度和高度设为10的整数倍呢?那是因为,之前的游戏都只是用单一的白色作为游戏的背景,所以游戏场景的具体尺寸并不重要,只要能提供足够大小的区域将角色显示出来即可。然而在本章的游戏中,我们将使用图像作为游戏的背景,所使用的图片文件是“flappy_background.png”,它的尺寸为138×396。可以看到该图片的宽度较小,如果只用一幅图片作为背景,则游戏的显示区域将十分有限。因此我们可以将四幅这样的图片拼起来形成一幅完整的背景图像,于是场景总的宽度值就是138的4倍,而高度值就是396,效果如图1所示。

图1 游戏背景图像

由此可见,我们只需使用图片“flappy_background.png”分别创建4个角色,并将它们紧挨在一起显示出来,便可以形成一幅完成的背景图像。接下来定义一个列表backgrounds,用来保存所有的背景角色,然后循环生成各个背景角色,并保存在backgrounds中。我们在程序中加入如下代码:

backgrounds = []  
for i in range(5):
    backimage = Actor("flappybird_background", topleft=(i * 138, 0))
    backgrounds.append(backimage)

上面的代码是不是有点问题?我们明明只需要创建4个角色,可是程序循环了5次。的确,我们这儿总共创建了5个角色,其中4个用来组成背景图像,剩下的一个自有妙用,后面我们再具体解释。

需要注意,上述代码在创建角色时,为Actor构造方法中传入了一个topleft参数,用来指定角色左上角的横、纵坐标值。它的作用相当于如下语句:

    backimage = Actor("flappybird_background")
    backimage.top = 0
    backimage.left = i * 138

接下来,我们为draw()函数编写代码,以便将背景图像显示出来。代码如下所示:

def draw():
    for backimage in backgrounds:
        backimage.draw() 

运行游戏,可以看到游戏窗口中出现了图1所示的背景图像。

1.2 滚动背景图像

我们知道游戏的场景是有固定大小的,它实际上是由WIDTH和HEIGHT值所确定的一块显示区域。为了拓展场景的空间范围,让有限的场景区域看起来无限延伸,我们可以对背景图像实行滚动显示。所谓滚动显示,就是不断地移动背景图像,当图像从窗口的某个边界移出去之后,再让其从另一边重新移进来,从而形成连绵不绝的显示效果。

然而这样做有一个问题,就是当背景图像尚未完全移出窗口边界时,它此前所占据的区域会留下一段空白。为了实现滚动显示时的无缝连接,我们可以多准备一幅背景图像,将它放在窗口边界之外“待命”,只要当前背景图像稍微移出了窗口边界,那么后备的图像便立即从窗口另一侧移进来。如此便不难理解,之前我们为何要循环地创建5个角色来组成游戏背景。场景滚动的原理如图2所示,图中红色的矩形框表示游戏窗口。

图 2 背景图像滚动示意图

我们首先在程序开头定义一个全局常量SPEED,用来表示背景滚动的速度。

接着定义一个update_background()函数,用来滚动背景图像,代码如下所示:

SPEED = 3 
def update_background():
    for backimage in backgrounds:
        backimage.x -= SPEED
        if backimage.right <= 0:
            backimage.left = WIDTH

可以看到,update_background()函数对列表backgrounds进行遍历,对于其中的每个背景角色,首先减少它的x属性值以使其向左移动,然后判断它的right属性值是否小于等于0,若是则说明角色的右边缘超出了窗口左边界,于是将它的left属性设置为WIDTH的值,从而重新将角色置于窗口的右边界处。

最后为update()函数编写代码,在其中调用update_background()函数,代码如下所示:

def update():
    update_background()

现在运行游戏,你会惊喜地看到,游戏的背景图像一直不停地移动着。就如同坐在一辆高速行驶的列车上,游戏窗口就好似列车的车窗,而滚动的场景就像是车窗外的风景依次地从眼前掠过。是不是感觉很酷呢?

2 添加障碍物

现在我们已经创建了游戏场景,而且实现了背景图像的滚动显示。然而,背景图像的作用主要是美化游戏场景,它并不会与游戏角色进行交互。为此,我们还要继续在场景中添加用来交互的角色。根据Flappy Bird游戏规则,场景中会出现两种障碍物——地面和水管,它们用来阻碍小鸟的飞行。下面我们来设置障碍物角色。

2.1 设置地面

目前游戏背景描绘的是天空中的景象,我们可以在背景图像的底部加入表示地面的角色,一方面可以让场景显得更完整,另一方面又能与小鸟进行交互,从而阻止小鸟向下飞行。

我们准备了一个图片文件“flappybird_ground.png”用来创建地面角色,然后在程序中加入下面一行代码:

	ground = Actor("flappybird_ground", bottomleft=(0, HEIGHT))

这里定义了一个变量ground用来保存地面角色。通过在Actor构造方法中设置bottomleft参数,我们将地面放置在窗口的底端。

接着修改draw()函数,加入显示地面的代码,如下所示:

	ground.draw()

现在运行游戏,你是否觉得呈现在眼前的画面有点奇怪?那是因为背景图像在不停地滚动,而地面的图像却是静止不动。为此,我们要让地面与背景同步地滚动起来。由于地面图像的宽度很大,它超过了窗口的宽度,因此这里直接使用一幅地面的图像来实现滚动效果,原理如图3所示。其中左图表示地面的初始位置,当它不断向左移动到达右图所示的位置时,则重新将它设为左图的初始位置。如此往复,便形成了地面循环滚动的效果。

图3 地面滚动示意图

我们定义一个update_ground()函数,用来移动地面角色,然后在update()函数中调用它来执行。update_ground()函数的代码如下所示:

	def update_ground():
            ground.x -= SPEED
    	    if ground.right < WIDTH:
       	        ground.left = 0

上述代码先将地面角色的x属性值减去SPEED的值,以使其保持与背景相同的速度向左移动。然后判断它的right属性值是否小于WIDTH的值,若是则说明地面到达了图3中右图所示的位置,于是将它的left属性值设为0,从而让地面重新回到初始位置。

2.2 设置水管

游戏中还有另外一类障碍物,那便是水管,它们用来妨碍小鸟在水平方向的飞行。水管是成对出现的,一根位于窗口上方,另一根位于下方。它们彼此相对,并且间隔一段距离,以便留出空隙让小鸟穿越。

我们准备了图片“flappybird_top_pipe.png”和“flappybird_bottom_pipe.png”,分别用来创建上方水管角色及下方水管角色。接着在程序中加入如下代码:

	pipe_top = Actor("flappybird_top_pipe")
        pipe_bottom = Actor("flappybird_bottom_pipe")

上述代码定义了变量pipe_top和pipe_bottom,分别保存上方水管角色和下方水管角色。可以看到,目前我们并没有为这两个角色指定初始位置。

根据游戏规则,水管要从窗口的右侧出现,并且每次出现时水管的高度要发生改变。然而我们所准备的水管图片高度是确定的,怎样改变水管的高度呢?实际上,我们可以随机地改变水管在垂直方向上的位置,使得水管每次出现在窗口中的部分都不相同,从而看上去就像是水管的高度发生了改变。此外,由于上、下水管的间距是保持不变的,因此我们只需要随机生成上方水管的高度,便可进一步求得下方水管的高度,原理如图4所示。

图4 随机生成水管的高度

我们首先在程序开头定义一个全局常量GAP,用来表示上、下水管的间距。然后定义一个reset_pipes()函数,用来设置水管出现的位置。接着在程序中调用reset_pipes()函数来生成一对水管。新添加的代码如下所示:

GAP = 150
def reset_pipes():
    pipe_top.bottom = random.randint(50, 150)
    pipe_bottom.top = pipe_top.bottom + GAP
    pipe_top.left = WIDTH
    pipe_bottom.left = WIDTH
reset_pipes()

可以看到,reset_pipes()函数首先随机生成一个指定范围内的整数值,并将其赋给上方水管的bottom属性。然后将该值与GAP的值求和,并赋给下方水管的top属性,以此来确定下方水管的位置。最后将上、下水管的left属性都设为WIDTH的值,以便让水管从窗口的右边界开始出现。

此外根据游戏规则,水管也要跟随背景一起向左移动,当其移出窗口左边界之后要重新出现在窗口右侧。于是我们再定义一个update_pipes()函数,用来移动上、下水管,同时在update()函数中调用该函数来执行。update_pipes()函数的代码如下所示:

def update_pipes():
    pipe_top.x -= SPEED
    pipe_bottom.x -= SPEED
    if pipe_top.right < 0:
        reset_pipes()

上述代码首先将上、下水管的x属性都减去SPEED的值,以便让它们同时向左移动,并与背景的滚动速度保持一致。接着检查上方水管的right属性值是否小于0,若是则说明水管超出了窗口的左边界,于是调用reset_pipes()函数来重新生成水管的位置。

最后修改draw()函数,在其中加入显示水管的代码,如下所示:

    pipe_top.draw()
    pipe_bottom.draw()

运行游戏可以看到,游戏的背景及障碍物在以相同的速度循环滚动。游戏画面如图5所示。

图5 添加障碍物后的游戏画面

3 添加小鸟

3.1 创建小鸟角色

现在是时候让游戏的主角登场了,下面我们来添加小鸟角色。我们准备了一个图片文件“flappybird1.png”,用来创建小鸟角色。然后在程序中加入如下代码:

	bird = Actor("flappybird1", (WIDTH // 2, HEIGHT // 2))
        bird.vy = 0

上述代码创建了一个小鸟角色,并将它保存在变量bird中,同时将它的初始位置设定为窗口的正中央。此外还为小鸟角色定义了一个vy属性,用来表示垂直方向的飞行速度,初值设为0。

接下来修改draw()函数,在其中加入显示小鸟的一行代码,如下所示:

	bird.draw()

现在运行游戏,可以看到小鸟竟然飞起来了,而且一直朝右水平飞行!这是怎么回事呢?我们明明没有编写小鸟移动的代码啊!其实这不过是你的错觉罢了,小鸟根本没有移动,因为它的坐标值没有发生任何改变。是不是感觉又领略了游戏设计的一大奥秘呢?

说明:

小鸟之所以看上去向右移动,是因为背景及障碍物都在向左移动,从而反衬出小鸟的向右移动。事实上,很多滚屏游戏都是采用类似的技巧,即固定角色的位置不动,而让场景进行滚动,从而看起来就像是角色在移动。

3.2 模拟重力下的飞行

现在小鸟看上去是在飞行,但也只是在水平方向上飞行,垂直方向好像并没有什么变化。为了让游戏的效果更加逼真,我们可以模拟重力的作用,即在垂直方向对小鸟施加重力的影响,从而实现小鸟在重力加速度下的飞行。

在我们之前制作的游戏中,角色的运动形式都是匀速运动,即角色的速度保持不变,而位置则均匀地改变。然而在加入重力作用后,小鸟的速度不再保持恒定,它会在重力影响下产生一个加速度,使得小鸟的位置不再均匀地改变。就像是从高空中坠落的物体,速度会变得越来越快,位置的变化也会愈来愈大。

另一方面,为了防止小鸟持续地向下坠落,我们需要对小鸟施加控制,让它能够向上飞起来。这里不妨使用鼠标来控制,每当玩家点击鼠标时,小鸟便会飞扬起来。而当它向上飞行达到最高点后,随即又会在重力影响下快速地坠落。图6显示了小鸟在垂直方向的速度变化。

图6 小鸟垂直速度变化示意图

为了达到上述效果,我们需要定义一个全局常量GRAVITY,用来表示重力加速度的值;同时还要定义一个全局常量FLAP_VELOCITY,用来表示小鸟飞扬时的初始速度。我们在程序的开头加入如下代码:

	GRAVITY = 0.2
	FLAP_VELOCITY = -5

可以看到,我们为初速度设置了一个负的整数值,表示小鸟飞起时的方向是朝上的。而重力加速度的值虽然看起来很小,但它对速度的影响却是相当大的,如果这个值设得比较大,小鸟可能会迅速地掉落到地面上。

接着定义一个fly()函数用来实现小鸟的飞扬,然后在update()函数中调用该函数来执行。fly ()函数的代码如下所示:

    def fly ():        
    	bird.vy += GRAVITY
    	bird.y += bird.vy  
    	if bird.top < 0:
        	bird.top = 0

上述代码首先将小鸟的vy属性累加了GRAVITY的值,表示垂直速度受到重力的影响而改变。然后将vy属性值累加到给小鸟的y属性,以此来改变小鸟的纵坐标。此外,为了防止小鸟飞出窗口的上边界,程序还对小鸟的top属性值进行了检查,若该值小于0,说明小鸟超出了上边界,则将它的top属性值设为0,以将其限制在窗口范围之内。

最后我们为on_mouse_down()函数编写代码,用来处理鼠标的点击事件。代码如下所示:

	def on_mouse_down(pos):
    	    bird.vy = FLAP_VELOCITY     
    	    sounds.flap.play()

可以看到,当程序检测到鼠标点击的动作后,首先将小鸟的vy属性设置为FLAP_VELOCITY的值,相当于给小鸟施加一个向上的爆发力,让它能够瞬间飞扬起来。同时播放一个音效文件“flap.wav”,用来增强交互的效果。

现在运行游戏,试着点击鼠标来操纵小鸟,看看它是否能够自由翱翔了呢?

3.3 播放飞行动画

你也许会觉得小鸟飞行时看上去有点不自然,它的翅膀总是向上扬起,而没有随着飞行上下摆动。的确是这样,那是因为目前我们仅仅使用了一个图片文件“flappybird1.png”来表示小鸟角色,其图像描绘的正是小鸟翅膀上扬的形态。为了让小鸟的飞行效果显得更加生动,我们希望能以动画的形式来展示小鸟飞行的姿态,让小鸟看上去是在不断地拍打着翅膀。那么如何播放角色的动画呢?

说明:

其实所谓的动画,不过是由一幅幅静止的图像而组成,只不过当图像快速切换时,人眼会所产生错觉,误认为图像是活动的。相关研究表明,当以不低于每秒24幅的速度切换图像时,就可以产生动画的效果,当然切换速度越快效果会越好。

由此可见,若要播放小鸟飞行的动画,仅使用一张图片是不够的,我们还得另外为小鸟角色准备两张图片“flappybird2.png”和“flappybird3.png”,分别呈现小鸟翅膀放平以及翅膀下摆的姿态,如图7所示。这样一来,我们就可以利用这3张图片来形成小鸟的飞行动画了。

图7小鸟飞行的图片

然而,如果每一次游戏循环就切换一幅图像的话,那动画的速度未免太快了,这样会让小鸟飞行的效果看上去不太自然。为了控制动画播放的速度,减缓图像切换的频率,我们需要再次借助延迟变量来实现。

首先在程序开头定义一个全局变量anim_counter 作为延迟变量,并将其初值设为0,代码如下所示:

anim_counter = 0

然后定义一个animation()函数用来播放小鸟的飞行动画,并在update()函数中调用该函数来执行。animation()函数的代码如下所示:

def animation():
    global anim_counter
    anim_counter += 1
    if anim_counter == 2:
        bird.image = "flappybird1"
    elif anim_counter == 4:
        bird.image = "flappybird2"
    elif anim_counter == 6:
        bird.image = "flappybird3"
    elif anim_counter == 8:
        bird.image = "flappybird2"
        anim_counter = 0

上述代码首先增加 anim_counter变量的值,然后根据它的值来修改小鸟的image属性,以便为它设置不同的图像。具体来说,当 anim_counter的值增加到2时,将小鸟图像设置为“flappybird1.png”,即图7左边的那幅图像,此时小鸟的翅膀向上扬起;当 anim_counter的值增加到4时,将小鸟图像设置为“flappybird2.png”,即图7中间的那幅图像,此时小鸟的翅膀水平不动;当 anim_counter的值增加到6时,将小鸟图像设置为“flappybird3.png”,即图7右边的那幅图像,此时小鸟的翅膀向下摆动;当 anim_counter的值增加到8时,又将小鸟图像设置为“flappybird2.png”,此时小鸟的翅膀重新放平。自此小鸟的飞行动画便播放完一轮,这时需要将 anim_counter的值重新设为0,从而开始下一轮的动画播放。

再次运行游戏,看看现在小鸟飞行时是否更加真实自然了呢?

4 小鸟与障碍物的交互

现在小鸟虽然可以飞行了,但看上去似乎畅通无阻,无论是水管还是地面都不能阻止它的前进。那是因为我们还没有实现小鸟与障碍物之间的交互行为,接下来便要分别对小鸟与地面,以及小鸟与水管实施碰撞检测。

4.1 小鸟与地面碰撞

根据游戏规则,若小鸟下落时碰撞到地面,则游戏结束。因此我们需要检测小鸟是否和地面发生了碰撞,并且在检测到碰撞后让游戏停止运行。为此,可以给小鸟添加一个布尔类型的属性dead,用来表示小鸟是否存活,当小鸟碰撞到地面后将该值设为True。我们在创建小鸟的语句后面加入这一行代码:

bird.dead = False

然后定义一个check_collision()函数来执行碰撞检测,代码如下:

def check_collision():
    if bird.colliderect(ground):
        sounds.fall.play()
        bird.dead = True  

可以看到,代码调用了小鸟角色bird的colliderect()方法,并将地面角色ground作为参数传递给该方法,从而对小鸟与地面实施碰撞检测。若该方法的返回值为True,说明两者发生了碰撞,这时播放碰撞的音效文件“fall.wav”,同时将小鸟的dead属性设为True。

最后对update()函数进行修改,以便在小鸟撞上地面后停止游戏的运行。修改后的update()函数如下所示:

def update():
    if bird.dead:
        return
    update_background()
    update_ground()
    update_pipes()
    fly()
    animation()
    check_collision()

update()函数首先对小鸟的dead属性值进行判断,如果该值为True,说明小鸟已经撞上了地面,于是调用return语句直接返回。此时后面的代码将不会执行,从而阻止了游戏的继续运行。

运行游戏测试一下,看看小鸟撞上地面后游戏是否会停下来。

4.2 小鸟与水管碰撞

接下来继续处理小鸟与水管的碰撞。其实与处理小鸟与地面的碰撞类似,我们只需要对小鸟与水管实施碰撞检测即可。只不过水管分为上、下两根,所以我们需要分别对上方水管和下方水管实施碰撞检测。为此我们在check_collision()函数中加入如下代码:

    if bird.colliderect(pipe_top) or bird.colliderect(pipe_bottom):
        sounds.collide.play()
        bird.dead = True

上述代码两次调用了小鸟的colliderect()方法,并分别将上方水管pipe_top及下方水管pipe_bottom作为参数传递给该方法,从而对小鸟与上、下水管分别实施碰撞检测。若是小鸟与上方水管或者下方水管其中之一发生了碰撞,则播放碰撞的音效文件“collide.wav”,同时将小鸟的dead属性设为True。

再次运行游戏测试一下,看看小鸟撞上水管后游戏是否会停下来。

4.3 小鸟飞越水管

目前我们已经实现了游戏的惩罚机制,即当小鸟撞上地面或水管时停止游戏。那么相应地我们还应该为游戏设计奖励机制,以便激励玩家在游戏中坚持下去。类此之前游戏设计中采取的做法,我们可以设置游戏积分,当玩家操纵小鸟从上、下水管之间穿越时,便增加分数值。

那么该如何判定小鸟飞越了水管呢?是否需要利用小鸟和水管的坐标来判断呢?的确如此。我们可以将小鸟与水管的横坐标进行比较,若发现前者大于后者,则判定小鸟飞越了水管,于是增加游戏积分的值。但这样存在一个问题,就是一旦小鸟飞越了水管,那么此后它的横坐标将始终大于水管的横坐标,这就意味着分数值会一直不断地增加,而事实上我们只希望分数增加一次。为此,我们需要借助布尔变量的“开关”作用。

说明:

布尔变量在游戏设计中使用得非常广泛,它除了用来表示游戏或角色的状态,还能对游戏中的各种操作进行控制,以防止连续执行操作。比如当布尔变量的值为True时,我们执行了某个操作,随即将它的值设为False,相当于“关闭”了开关,这就阻止了再一次执行该操作。直到某种情况下布尔变量的值重新被设为True,相当于“打开”了开关,这时才能重新执行该操作。

我们可以在程序中定义两个全局变量score和score_flag,前者是整型变量,用来表示游戏的分数值;后者是布尔变量,用来控制游戏积分的操作,只有它的值为True时才能增加游戏分数。代码如下所示:

score = 0               
score_flag = False

初始时我们将score的值设为0,同时将score_flag的值设为False。然后修改reset_pipes()函数,在其中对score_flag的值进行设置。修改后的reset_pipes()函数如下所示(粗体部分表示新添加的代码):

def reset_pipes():
    global score_flag 
    score_flag = True
    pipe_top.bottom = random.randint(50, 150)
    pipe_bottom.top = pipe_top.bottom + GAP
    pipe_top.left = WIDTH
    pipe_bottom.left = WIDTH

如此一来,每当重新设置水管时,便会将score_flag的值设为True,相当于“打开”了积分操作的开关,此时便具备了增加分数的必要条件。

接着修改fly()函数,在其中添加代码来实现游戏积分的功能。修改后的fly()函数如下所示(粗体部分表示新添加的代码):

def fly(): 
    global score, score_flag
    	if score_flag and bird.x > pipe_top.right:
       	    score += 1
       	    score_flag = False    
    	    bird.vy += GRAVITY
   	    bird.y += bird.vy
    	if bird.top < 0:
       	    bird.top = 0

可以看到,需要同时满足两个条件才会增加游戏分数,一个条件是score_flag的值为True,另一个条件是小鸟的x属性值大于水管的right属性值,即小鸟越过了水管的右边缘。若这两个条件都满足,则将score的值加1,同时将score_flag的值设为False,相当于“关闭”了积分操作的开关,从而阻止了分数值持续地增加。

最后修改draw()函数,在其中添加如下一行代码来显示游戏积分:

	screen.draw.text(str(score), topleft=(30, 30), fontsize=30)

现在运行游戏,可以看到如图8所示的游戏画面。试着玩一下,看看你最多能玩到多少分。

图8添加积分后的游戏画面

5 设计图形用户界面

至此游戏的基本功能已经实现了,但我们还可以更进一步,把游戏的功能设计得更加完善。那什么地方还能继续完善呢?如果对比市面上发行的Flappy Bird游戏,不难发现我们目前的游戏缺少一个初始界面,其中包含了游戏标题、开始按钮、游戏提示等基本元素。这个初始界面就是通常所说的图形用户界面(简称GUI),它往往由一些文字或图像的元素组成,用来显示游戏的基本信息,或者提供一些游戏设置的选项。图形用户界面建立了玩家与游戏之间的沟通渠道,使得玩家能够快速方便地了解及操作游戏。

下面我们来为游戏设计图形用户界面。

5.1 显示GUI图像

我们事先准备了一些图片文件用来表示GUI中的图像元素,它们分别是:“flappybird_title.png”,用来显示游戏的标题;“flappybird_get_ready.png”,用来显示游戏的操作提示;“flappybird_start_button.png”,用来作为游戏的开始按钮;“flappybird_game_over.png”,用来显示游戏的结束信息。然后可以使用上述图片来创建GUI角色,并将它们显示在游戏窗口中。我们在程序中加入如下代码:

gui_title = Actor("flappybird_title", (WIDTH // 2, 72))
gui_ready = Actor("flappybird_get_ready", (WIDTH // 2, 204 ))
gui_start = Actor("flappybird_start_button", (WIDTH // 2, 345))
gui_over = Actor("flappybird_game_over",(WIDTH // 2, HEIGHT // 2))

可以看到,上述代码在创建各个GUI角色时为其指定了初始位置,事实上参数中的坐标值并没有什么特殊的含义,可以根据实际的显示效果来进行调整。

由于图形用户界面需要在游戏开始运行之前来显示,因此我们还要对游戏的状态进行标记,以便确定何时显示GUI角色。我们在程序开头定义一个全局布尔变量started,用来表示游戏是否开始,代码如下所示:

started = False

代码将started的初始值设为False,表示游戏尚未开始的状态。

接着修改draw()函数,在其中加入显示GUI角色的代码。修改后的draw()函数如下所示(粗体部分表示新添加的代码):

def draw():
    for backimage in backgrounds:
        backimage.draw()
    if not started:
        gui_title.draw()
        gui_ready.draw()
        gui_start.draw()
        return
    pipe_top.draw()
    pipe_bottom.draw()
    ground.draw()
    bird.draw()
    screen.draw.text(str(score), topleft=(30, 30), fontsize=30)
    if bird.dead:
        gui_over.draw()

可以看到,我们在draw()函数中插入了两段代码,一段放在显示背景图像的语句之后,它检查全局变量started的值,若为False说明游戏尚未开始,于是分别显示游戏标题、操作提示及开始按钮的GUI图像;另一段代码则放在了最后,它检查小鸟的dead属性值,若为True说明小鸟撞上了地面或水管,于是显示游戏结束的GUI图像。

现在运行游戏,可以看到窗口中显示了图形用户界面,如图9所示。

图9 游戏的图形用户界面

提示:

由于游戏中角色数量众多,所以要小心它们的显示顺序。倘若显示语句的执行顺序不当,则可能造成错误的遮挡关系。试着改变draw()函数中各语句的执行顺序,看看游戏的画面会发生什么改变。

5.2 点击开始按钮

观察一下刚刚添加的图形用户界面,可以看到其中有一个开始按钮的图像,直觉告诉你这个按钮是有用的。的确如此,我们需要用它来启动游戏。具体来说,当玩家用鼠标点击该按钮时,游戏便开始运行。

然而这个开始按钮只是看起来像一个按钮,它本质上不过是一幅图像罢了。那又如何让它响应鼠标的点击操作呢?这个问题不难解决。由于我们能够获取鼠标点击处的坐标,因此可以判断鼠标点的坐标是否位于按钮图像的范围之内,若是则说明鼠标点击了开始按钮。

接下来修改on_mouse_down()函数,在其中加入点击开始按钮的功能。修改后的on_mouse_down()函数如下所示:

def on_mouse_down(pos):
    global started
    if bird.dead:
        return
    if started:
        bird.vy = FLAP_VELOCITY     
        sounds.flap.play()        
        return   
    if gui_start.collidepoint(pos):
        started = True
        reset_pipes()              

上述代码有三个if语句,第一个if语句检查小鸟的dead属性,若为True说

明小鸟撞上地面或水管,于是调用return语句返回;若小鸟的dead属性为False,则执行第二个if语句,检查全局变量started的值,若为True说明游戏已经开始,于是执行小鸟飞扬的操作;若started的值为False,则执行第三个if语句,并通过按钮的collidepoint()方法来检查它是否被点击,若为True说明鼠标点击了按钮,于是将started的值设为True,并调用reset_pipes()函数生成水管的位置。

5.3 播放背景音乐

现在游戏已经相当完美了,最后再让我们锦上添花,为游戏添加一段背景音乐。在此前的游戏设计中,我们仅仅播放了动作音效,目的是让游戏及时地对玩家的操作进行反馈,以此增强游戏的交互效果。然而背景音乐是一段比较长的乐曲,它具有特定的旋律,主要用来烘托游戏场景的气氛。那么如何播放背景音乐呢?

事实上利用Pgzero库来播放音乐十分简单,因为它提供了一个music对象,能够方便地实现音乐的播放及控制功能。我们这里只需使用music对象的两个方法,一个是play()方法,用来播放音乐文件;另一个是stop()方法,用来停止播放音乐。

我们事先准备了一个音乐文件“flappybird.mp3”作为游戏的背景音乐,然后将其放置在“music”文件夹之下。接着修改on_mouse_down()函数,在鼠标点击开始按钮后的操作中加入如下一行代码:

music.play("flappybird")

最后修改check_collision()函数,在小鸟碰撞地面及水管后的处理中分别加入下面这行代码:

music.stop()

现在运行游戏可以发现,当鼠标点击开始按钮的同时,紧张刺激的背景音乐便随之响起了,玩家将在音乐的激励下斗志昂扬地“投入战斗”。而当小鸟碰到地面或水管的那一刻,背景音乐便会戛然而止,整个游戏世界将重新归于寂静。

6 回顾与总结

我们学习制作了Flappy Bird游戏。我们首先讨论了如何滚动游戏的背景图像,并设法让地面及水管障碍物跟随背景一起移动,同时还介绍了如何随机生成一对水管的高度。然后我们重点讨论了小鸟角色的操作,一方面通过模拟重力效果实现了它的垂直飞行,另一方面为其播放了飞行时的动画。接着对小鸟与地面及水管的碰撞进行了处理,并实现了游戏积分的功能。最后我们为游戏添加了图形用户界面,并实现了背景音乐的播放。

下面给出Flappy Bird游戏的完整源程序代码。

# Flappy Bird游戏源代码flappybird.py
import random
WIDTH = 138 * 4             # 窗口宽度(由四张背景图片组成)
HEIGHT = 396                # 窗口高度
GAP = 150                   # 上下水管间的缺口大小
SPEED = 3                   # 场景滚动速度
GRAVITY = 0.2               # 重力加速度
FLAP_VELOCITY = -5         # 飞扬时的初始速度
anim_counter = 0              # 小鸟动画计数器
score = 0                     # 游戏积分
score_flag = False              # 得分标记
started = False                 # 游戏开始标记
backgrounds = []               # 背景图像列表
# 创建五张背景图像角色,用于循环滚动游戏场景
for i in range(5):
    backimage = Actor("flappybird_background", topleft=(i * 138, 0))
    backgrounds.append(backimage)    
# 创建地面角色
ground = Actor("flappybird_ground", bottomleft=(0, HEIGHT))
# 创建上下水管角色
pipe_top = Actor("flappybird_top_pipe")
pipe_bottom = Actor("flappybird_bottom_pipe")
# 创建小鸟角色
bird = Actor("flappybird1", (WIDTH // 2, HEIGHT // 2))
bird.dead = False        # 标记小鸟是否存活
bird.vy = 0             # 设置小鸟垂直速度
# 创建GUI角色
gui_title = Actor("flappybird_title", (WIDTH // 2, 72))
gui_ready = Actor("flappybird_get_ready", (WIDTH // 2, 204 ))
gui_start = Actor("flappybird_start_button", (WIDTH // 2, 345))
gui_over = Actor("flappybird_game_over",(WIDTH // 2, HEIGHT // 2))

# 游戏逻辑更新
def update():
    if not started or bird.dead:
        return
    update_background()
    update_ground()
    update_pipes()
    fly()    
    animation()   
    check_collision()

# 绘制游戏角色
def draw():
    screen.fill((255, 255, 255))
    for backimage in backgrounds:
        backimage.draw()         
    if not started:
        gui_title.draw()
        gui_ready.draw()
        gui_start.draw()
        return 
    pipe_top.draw()
    pipe_bottom.draw()
    ground.draw()
    bird.draw()
    screen.draw.text(str(score), topleft=(30, 30), fontsize=30)
    if bird.dead:
        gui_over.draw()

# 处理鼠标点击事件
def on_mouse_down(pos):
    global started
    if bird.dead:
        return
    if started:
        bird.vy = FLAP_VELOCITY     
        sounds.flap.play()             
        return
    # 若点击开始按钮,初始化游戏    
    if gui_start.collidepoint(pos):
        started = True
        reset_pipes()                  
        music.play("flappybird")        

# 更新游戏场景,循环滚动背景图像       
def update_background():       
    for backimage in backgrounds:
        backimage.x -= SPEED
        if backimage.right <= 0:
            backimage.left = WIDTH 

# 更新地面角色   
def update_ground():
    ground.x -= SPEED
    if ground.right < WIDTH:
        ground.left = 0

# 更新水管角色
def update_pipes():
    pipe_top.x -= SPEED
    pipe_bottom.x -= SPEED
    if pipe_top.right < 0:
        reset_pipes()

# 重新设置上下水管出现的位置    
def reset_pipes():
    global score_flag 
    score_flag = True
    # 随机生成上方水管的垂直位置
    pipe_top.bottom = random.randint(50, 150)
    # 根据上方水管的垂直位置来设置下方水管的垂直位置
    pipe_bottom.top = pipe_top.bottom + GAP
    # 设置上下水管的水平位置
    pipe_top.left = WIDTH
    pipe_bottom.left = WIDTH
 
# 控制小鸟飞行
def fly(): 
    global score, score_flag
    # 当小鸟越过水管,则分数加一
    if score_flag and bird.x > pipe_top.right:
        score += 1
        score_flag = False        
    # 更新小鸟坐标    
    bird.vy += GRAVITY
    bird.y += bird.vy
    # 防止小鸟飞出窗口上边界
    if bird.top < 0:
        bird.top = 0

# 播放小鸟飞行的动画
def animation():
    global anim_counter
    anim_counter += 1
    if anim_counter == 2:
        bird.image = "flappybird1"
    elif anim_counter == 4:
        bird.image = "flappybird2"
    elif anim_counter == 6:
        bird.image = "flappybird3"
    elif anim_counter == 8:
        bird.image = "flappybird2"
        anim_counter = 0

# 小鸟与水管和地面的碰撞检测
def check_collision():
    if bird.colliderect(pipe_top) or bird.colliderect(pipe_bottom):
        sounds.collide.play()
        music.stop()
        bird.dead = True
    elif bird.colliderect(ground):
        sounds.fall.play()
        music.stop()
        bird.dead = True  

PS:若要全面系统的学习可以参考《趣学Python游戏编程》,该书通过十个经典游戏案例,深入浅出地介绍了游戏编程的基本原理,以及使用Python编写游戏的具体方法。相信学完这本书后你也能开发出如此精彩的小游戏。

编辑于 2022-07-02 · 著作权归作者所有

玻璃钢生产厂家小品玻璃钢动物雕塑厂家现货户外玻璃钢雕塑销售价格佛像玻璃钢雕塑怎么制作安阳玻璃钢浮雕雕塑价格报价雕塑材料玻璃钢哪家好河北玻璃钢花盆供货商定做形象卡通游乐园玻璃钢雕塑广东大型主题商场美陈市场报价深圳玻璃钢透光雕塑厂家宿州抽象玻璃钢雕塑批发济宁梅州玻璃钢动物雕塑杭州精致玻璃钢面包雕塑深圳市深美玻璃钢雕塑公司内蒙古公园玻璃钢雕塑公司安徽大型玻璃钢雕塑厂家玻璃钢鹤雕塑图片烟台学校玻璃钢雕塑定做甘肃玻璃钢长颈鹿动物景观雕塑南平玻璃钢仿真水果雕塑甘肃动物玻璃钢雕塑烟台玻璃钢电影动漫雕塑广东加工玻璃钢卡通雕塑费用商场美陈方案怎么写郑州玻璃钢浮雕景观雕塑厂六安公园玻璃钢雕塑市场安宁玻璃钢雕塑造型厂家哪里有玻璃钢仙鹤雕塑图片上海玻璃钢花盆费用广州动物玻璃钢雕塑有哪些东城区商场美陈香港通过《维护国家安全条例》两大学生合买彩票中奖一人不认账让美丽中国“从细节出发”19岁小伙救下5人后溺亡 多方发声单亲妈妈陷入热恋 14岁儿子报警汪小菲曝离婚始末遭遇山火的松茸之乡雅江山火三名扑火人员牺牲系谣言何赛飞追着代拍打萧美琴窜访捷克 外交部回应卫健委通报少年有偿捐血浆16次猝死手机成瘾是影响睡眠质量重要因素高校汽车撞人致3死16伤 司机系学生315晚会后胖东来又人满为患了小米汽车超级工厂正式揭幕中国拥有亿元资产的家庭达13.3万户周杰伦一审败诉网易男孩8年未见母亲被告知被遗忘许家印被限制高消费饲养员用铁锨驱打大熊猫被辞退男子被猫抓伤后确诊“猫抓病”特朗普无法缴纳4.54亿美元罚金倪萍分享减重40斤方法联合利华开始重组张家界的山上“长”满了韩国人?张立群任西安交通大学校长杨倩无缘巴黎奥运“重生之我在北大当嫡校长”黑马情侣提车了专访95后高颜值猪保姆考生莫言也上北大硕士复试名单了网友洛杉矶偶遇贾玲专家建议不必谈骨泥色变沉迷短剧的人就像掉进了杀猪盘奥巴马现身唐宁街 黑色着装引猜测七年后宇文玥被薅头发捞上岸事业单位女子向同事水杯投不明物质凯特王妃现身!外出购物视频曝光河南驻马店通报西平中学跳楼事件王树国卸任西安交大校长 师生送别恒大被罚41.75亿到底怎么缴男子被流浪猫绊倒 投喂者赔24万房客欠租失踪 房东直发愁西双版纳热带植物园回应蜉蝣大爆发钱人豪晒法院裁定实锤抄袭外国人感慨凌晨的中国很安全胖东来员工每周单休无小长假白宫:哈马斯三号人物被杀测试车高速逃费 小米:已补缴老人退休金被冒领16年 金额超20万

玻璃钢生产厂家 XML地图 TXT地图 虚拟主机 SEO 网站制作 网站优化