趣味Python游戏编程:第2章 用鼠标控制游戏:拼图

趣味Python游戏编程:第2章 用鼠标控制游戏:拼图

在第1章中,我们完成了一个弹跳小球的游戏。虽然实现了小球图像的移动和显示,但是准确来说,它还不能算作一个游戏。因为游戏是用来“玩”的,也就是说,玩家需要通过某种方式来操作游戏的角色,而在弹跳小球游戏中,小球只是自己在不停地移动,它的运动并没有受到我们的控制。若想编写一款真正的游戏,则要考虑为游戏添加控制操作。

在本章中,我们将一起编写一个拼图游戏,即将一些零散的图片块拼合成一幅完整的图像。我们希望通过鼠标来操作图片块,具体来说,当玩家用鼠标单击图片块时可以移动它的位置,当所有的图片块都移动到正确位置时游戏便完成了。让我们一起来编写这个游戏吧!

本章主要涉及如下知识点:

自动创建多个不同角色

使用随机函数

处理鼠标单击事件

模块化编程方法

播放声音文件

在窗口显示文字

2.1 添加图片块

2.1.1 准备图片资源

由于拼图游戏是将一系列小的图片块拼成一幅完整的大图像,因此首先需为游戏准备一些小图片,要保证每一张小图片都是完整图像的某个组成部分。在拼图游戏中,图片块越多,操作的难度就越大。为了简单起见,这里只使用了9张图片来完成拼图,如图2.1所示。

在这里插入图片描述

2.1.2 创建游戏场景

作为游戏程序编写的第一步,需要创建一个游戏角色活动的场景。首先确定场景的大小。我们的游戏使用9个图片块来完成拼图,最终图像的尺寸应该是3×3个图片块大小,即水平方向3个图片块,垂直方向也是3个图片块。每个图片块的宽度和高度都是96像素,因此最终图像的宽度和高度则都是96的3倍,以此作为游戏场景的宽度和高度值,代码如下所示:

SIZE = 96
WIDTH = SIZE * 3
HEIGHT = SIZE * 3

上述代码首先定义了一个常量SIZE,用来表示图片块的尺寸。然后将场景的宽度WIDTH和高度HEIGHT分别设置为SIZE的3倍。

接着显示场景,这就需要在draw()函数中进行处理。为程序加入如下代码:

def draw():
    screen.fill((255, 255, 255))

可以看到,这里仍然采用最简单的办法,即使用单一的颜色来填充整个游戏场景。这里使用白色作为背景颜色。

2.1.3 用列表管理图片块

游戏场景已经准备好了,接下来要考虑创建游戏角色。对于拼图游戏来说,游戏角色就是玩家需要操纵的图片块,准确来说,每一个图片块都对应着一个游戏角色。在弹跳小球游戏中,我们学习了如何创建游戏角色,以及如何对多个游戏角色统一管理,现在是时候将之前学到的知识运用一下了!

还记得吗?可以通过Actor()构造方法来创建一个角色,只需要将角色的图片文件名作为参数传递给Actor()方法即可。例如创建第一个图片块角色的代码可以这样写:

pic = Actor("puzzle_pic0" )

为了管理9个图片块,需要定义一个列表,将创建的图片块角色统统加入其中。代码如下所示:

pics = [ ]
pics.append(pic)

上述代码中首先定义了一个图片块列表pics,然后调用append()方法将刚才创建的图片块角色pic加入列表。

那么问题来了,总共有9张图片,难道要一张一张地创建角色并加入列表吗?当然不用亲自动手,重复的操作交给循环语句来处理就好了。由于知道循环的确切次数,可以使用计数循环来完成创建角色的操作,在这里循环的次数为8次。

慢着,是不是弄错了,一共9张图片,难道循环次数不是9次吗?没错,只有8次。根据游戏规则,需要移动图片块的位置,倘若将全部的9张图片都加入游戏场景,那么图片块就没移动的空间了,因此需要预留一个空白的区域用来移动图片块。

原来如此。那么还有一个问题就是,每个图片文件的名字都不一样,怎么在循环语句中通过不同的文件名来创建相应的图片块角色呢?这个问题不难解决。请仔细看看图2.1中展示的图片文件名,你会发现所有文件名的前面都是一串相同的字符“puzzle_pic”,只是后面接了个不同的数字(按照从0到8的顺序依次编号)。其实这样来命名就是为了便于循环操作的。

我们可以将图片文件的名字分为两部分,前面是字符串“puzzle_pic”,后面是数字,而数字的编号恰好可以和循环变量的值对应起来。于是可以编写如下代码来自动创建所有图片块角色:
for i in range(8):
pic = Actor(“puzzle_pic” + str(i))
pic.index = i
pics.append(pic)

需要注意的是,由于循环变量i的类型是整型,而图片文件名的类型是字符型,为了将i的值转化为图片文件名中的字符,首先需要调用Python内置的str()函数进行类型转换,然后通过字符串连接操作符“+”将文件名的前后两部分连接起来,从而形成完整的图片文件名。

接着将完整的图片文件名传入Actor()方法,便可创建相应的图片块角色。角色创建之后随即将其加入图片块列表。

此外,上述代码还为每个图片块角色定义了一个属性index,并将循环变量的值赋予该属性。那么这个属性有什么作用呢?它实际上记录了图片块的编号,具体作用后面再进行解释。

2.2 打乱图片块

下面要考虑将图片块显示在窗口中。若要将所有的图片块显示在窗口中,则需要依次从列表中读取各图片块并显示。需要注意的是,目前我们是按照图片文件名从puzzle_pic0到puzzle_pic7的次序将图片块加入列表中的,很显然,图片块在列表中的位置也是固定的。这就意味着每次运行游戏的时候,图片块会按照固定次序显示在窗口中。这显然不符合游戏的规则。

在拼图游戏中,玩家需要将分散的图片块拼合成完整的图像。所谓分散的图片块,就是说各个图片块在窗口中的位置不是事先确定的,而是随机变化的。因此在每次的游戏中,图片块的位置都是不相同的。这也是拼图游戏的乐趣所在,每次游戏都是新的挑战,玩家可以反复进行游戏。

那么怎样让图片块在窗口中分散地显示出来呢?上面提到,所有图片块都是按固定次序保存在列表中的,只要设法随机打乱图片块在列表中的次序,然后再将它们显示出来,则可以达到分散显示的效果。这需要借助Python提供的随机函数。

2.2.1 使用随机函数

在程序设计中经常会遇到使用随机数的情况,对于游戏程序更是如此。为了增添游戏的乐趣或挑战,游戏往往会通过随机发生的事件来制造惊喜或障碍,而这都需要借助随机数来实现。

说明:

为了让编程者方便地获取随机数,Python中提供了一个名叫random的库,其中包含了很多对随机数进行处理的函数。我们希望随机打乱图片块在列表中的次序,因此可以使用random库中的shuffle()函数。只需要将列表所为参数传入该函数,它可以随机地改变各个元素在列表中的位置。

需要注意的是,使用shuffle()函数前首先得导入random库,因此在程序的最前面加上这一行代码:

import random

接着在之前编写的程序后面加入如下语句:

random.shuffle(pics)

该语句表示调用random库的shuffle()函数,参数为图片块列表pics。执行这句之后,pics中各个图片块的次序便被随机打乱了。

2.2.2 将图片块显示出来

最后显示图片块。既然列表已经被随机打乱次序,只需要从列表中依次取出各个图片块并显示即可。然而还有一个问题。就是目前还没有为图片块设置坐标。在创建图片块的时候,在Actor()方法中仅仅传入了图片文件名,而没有传入坐标值。在这种情况下,Pgzero会自动地将每个图片块的坐标设置为(0,0),如果直接显示,可以看到所有图片块都会“挤”在窗口的左上角。

为了让图片能够显示在正确的位置,需要为每个图片块设置坐标。当然,你可以逐个设置8张图片,但既然循环语句可以自动操作,我们又何必浪费时间亲手去做呢?实际上,可以使用计数循环语句,从列表中依次取出每个图片块并为其设置坐标即可。

那么各个图片块的坐标又分别设为多少呢?由于我们的拼图是由9张图片块组成的3×3的网格,因此程序窗口也可以相应地划分为9个方格,一个方格中显示一个图片块。每个方格的位置可以用一对数值(i,j)来表示,其中i代表方格所在列的编号,而j表示方格所在行的编号。各图片块所在方格的位置编号如图2.2所示(注意行和列的编号都是从0开始的)。

这样一来,每个图片块的坐标便可以通过其所在的方格编号来确定,只需要将图片块的大小乘上相应的列号或行号即可。于是可以编写如下代码为所有图片块设置坐标:

for i in range(8):
    pics[i].left = i % 3 * SIZE
    pics[i].top = i // 3 * SIZE

在这里插入图片描述

在循环运行过程中,循环变量i的值从0增加到7,i的每个取值都对应着列表pics中的一个图片块pics[i],同时该图片块的坐标也可通过i求得。由于拼图中总共有3列图片块,因此可用i对3进行“取余”运算,得到pics[i]所在方格的列号,而用i对3进行“取整”运算来得到行号。接着分别将列号和行号乘上图片块的尺寸SIZE,便可算出pics[i]的左边界和上边界的位置,并分别赋值给pics[i]的left和top属性。

最后修改draw()函数,在其中加入遍历循环语句,依次调用每个图片块的draw()方法进行显示。修改后的draw()函数代码如下所示:

def draw():
    screen.fill((255, 255, 255))

    for pic in pics:
        pic.draw()

现在运行一下游戏,可以看到如图2.3所示的效果,其中各个图片块的位置是随机确定的。多运行几次,看看是否每一次显示的画面都不相同。

在这里插入图片描述

2.3 移动图片块

现在图片块已经准备好了,但是游戏暂时还不能玩,因为它还不具备交互性。对于一款游戏来说,其根本的特性在于交互性,即玩家和游戏可以通过某种渠道建立联系,一方面玩家可以向游戏传达操作命令,另一方面游戏也需要向玩家反馈操作结果。前者可借助计算机的输入设备来实现,例如通过鼠标或键盘来控制游戏操作;后者则要求游戏将操作的结果以图像或声音的形式即时表现出来。

下面为游戏添加交互手段。根据拼图游戏规则,玩家能够移动各个图片块的位置,使得所有图片块拼合成一幅完整的图像。那么如何让玩家来操作图片块呢?可以使用鼠标来控制游戏操作,例如当玩家用鼠标单击某个图片块时,能够移动它的位置,并将其重新显示在窗口中。

2.3.1 处理鼠标单击事件

可是程序如何知道玩家是否单击了鼠标呢?Pgzero早已帮我们解决了这个问题,它会自动检测鼠标的单击动作。

说明:

事实上,大多数高级语言都是基于某种事件处理机制来应对用户的输入操作,例如鼠标单击或按键按下等常见操作。具体来说,鼠标单击或按键按下操作被称为一个“事件”,而事件发生后程序采取的应对措施则被称为“事件处理”。事件的检测和处理通常是程序内部自动进行的,编程者无须考虑事件的处理过程,只需告诉程序事件发生后要执行什么操作就可以了。

我们希望玩家用鼠标单击图片块后,能够将其移动。那么又该如何告诉程序单击鼠标后进行移动操作呢?对于这个问题,Pgzero也提供了方便的解决办法,即提供一个on_mouse_down()函数来处理鼠标单击事件。具体来说,将鼠标单击后要执行的操作代码写入该函数中,则当鼠标单击事件发生后,程序便会自动执行该函数中的操作。

为了获取鼠标事件的具体信息,on_mouse_down()函数还提供了两个参数,分别是pos和button,前者表示鼠标单击处的坐标值,后者表示鼠标的按键值。在拼图游戏中,只需使用pos参数即可。需要注意的是,pos其实是一个元组类型的变量,它包含两个数值:pos[0]表示鼠标单击处的水平坐标;pos[1]表示鼠标单击处的垂直坐标。当鼠标单击事件发生时,程序会自动地把单击处的坐标值保存在pos元组中。可以编写下面的代码测试一下:
在这里插入图片描述

运行一下程序,然后在程序窗口中单击鼠标,Mu编辑器的下方便会输出单击处的坐标值。

现在程序能够响应鼠标单击操作了,可是如何在鼠标单击图片块后让它移动呢?首先选中鼠标所单击的图片块,然后看看它能否移动,如果可以则改变它的位置。

2.3.2 选取图片块

下面先看看如何选取鼠标所单击的图片块。根据前面的介绍,我们知道可以通过pos变量来获取鼠标单击处的坐标值,但是该坐标仅仅表示鼠标指针所指向的像素点坐标,而在同一个图片块内的不同位置单击鼠标,你会发现pos的值都不相同。如何通过pos的值来确定究竟单击的是哪个图片块呢?

由于各个图片块都是显示在窗口中某个方格内的,于是可以通过pos的值求得鼠标单击的图片块位于哪个方格,进而确定所要选取的图片块是哪一块。可以将on_mouse_down()函数修改为如下代码:

def on_mouse_down(pos):
    grid_x = pos[0] // SIZE
    grid_y = pos[1] // SIZE

在上面的代码中,定义了两个变量grid_x和grid_y,分别表示图片块方格的水平索引值(即列号)和垂直索引值(即行号),然后用鼠标点的水平坐标(pos[0])对图片块尺寸SIZE做“取整”运算,得到方格的水平索引值,并保存到grid_x中;用鼠标点的垂直坐标(pos[1])对SIZE“取整”运算,得到方格的垂直索引值,并保存到grid_y中。

接着循环遍历图片块列表pics,看看哪个图片块所在方格的水平和垂直索引值与grid_x和grid_y的值相一致。为此在on_mouse_down()函数中加入如下代码:
在这里插入图片描述

上面的代码定义了变量thispic,用于保存满足要求的图片块,而该图片正是通过鼠标单击所选取的图片块。

2.3.3 判断图片块能否移动

选取图片块后并不能马上移动它的位置,因为移动图片块需要满足一个基本条件,即它的周围要有空白的方格,以便图片块能够放入其中。根据游戏规则,图片块可以朝水平或垂直方向移动,一次移动一个方格的距离。于是需要分别对上、下、左、右四个方向进行检查,看看图片块能否朝某个方向移动。下面以向上检查为例进行说明。

若要向上移动图片块,首先要保证该图片块所在的方格不能位于最上面一行,即grid_y的值要大于零。其次要看它上方紧邻的方格中是否存在图片块,若不存在则可以移动,否则不能移动。

对于某图片块所在的方格来说,其上方相邻方格的水平索引值也是grid_x,垂直索引值则为grid_y-1。而为了判断上方的方格是否存在图片块,又需要遍历一次pics列表,逐一检查每个图片块所在方格的水平和垂直索引值是否分别与grid_x和grid_y-1的值相等。若没有找到满足条件的图片块,说明上方的方格是个空白块,于是可以将当前图片块向上移动一格。

其他三个方向的检查过程也是类似的。

2.3.4 采用模块化编程方法

不难看出,我们的程序中出现了重复的操作,例如在选取鼠标所单击的图片块时,以及在判断图片块能否向上移动时,都涉及对pics列表的循环遍历操作,而且循环体中的代码逻辑也是完全一致的,仅仅是个别的数值不相同。目前仅仅针对向上的方向进行检查,如果再对其他三个方向进行检查,那么类似的代码还要重复编写三次。有没有办法来减少重复编写代码呢?办法当然是有的,可采用模块化的编程方法来解决这个问题。

说明:

所谓模块化编程方法,是指将程序的编写划分为多个模块进行,每个模块负责完成某一个特定的功能。这样做不仅能够提高程序的可读性,而且能够重复利用各个功能模块,从而有效地简化代码编写工作。对于具体的编程语言来说,模块化编程可以借助函数来实现,即把相同或类似的操作定义为函数,并为函数设置适当的参数来处理不同的数据。

对于我们的程序,可以定义一个函数get_pic(),用来获取某个方格中的图片块。同时为其设置两个参数,分别表示方格的水平和垂直索引值。

接下来改写之前编写的代码,将列表的循环遍历操作加入get_pic()函数中,代码如下所示:

def get_pic(grid_x, grid_y):
    for pic in pics:
        if pic.x // SIZE == grid_x and pic.y // SIZE == grid_y:
            return pic
    return None

可以看到,get_pic()函数会循环遍历pics列表,并返回满足条件的图片块。若所有图片块都不满足条件,则返回一个空白块(用None表示)。因此该函数不仅可用于获取鼠标单击的图片块,还可用来检查某个方格是否存在图片块。这就有效地避免了代码的重复编写。

2.3.5 改变图片块的位置

移动图片块的操作很简单,就是将图片块的位置移动一个方格的距离(即SIZE常量的值)。例如在向上的检查中,若发现上方是一个空白块,将当前图片块的纵坐标值减去SIZE即可。对on_mouse_down()函数进行如下修改:
在这里插入图片描述

在上面的代码中,首先将鼠标单击处的坐标值转换为方格的水平和垂直索引值。然后以方格的索引值作为参数来调用get_pic()函数,并将返回的图片块保存在thispic变量中。倘若该变量值不为None,表示鼠标单击的是一个图片块而不是空白块,便再次调用get_pic()函数来判断该图片块的上方是否为空白块。若是则说明该图片块可以朝上方移动,于是将图片块的y属性减去SIZE的值。

对于其他方向的操作也可以编写类似的代码。

2.3.6 减少程序的缩进层级

再仔细观察一下on_mouse_down()函数中的代码结构,可以看到代码的缩进层级比较多(最多处缩进了三级),从而会对代码的可读性造成一定影响。其实可以采用另一种写法来减少缩进的层级,即在条件语句中使用return关键字,例如以下代码所示:

if thispic == None:
        return

上面的代码表示,如果thispic的值为None,则立即返回。什么叫作立即返回呢?立即返回的意思就是说函数中只要执行了return语句,程序便会直接跳出该函数,那么函数中剩余的其他语句将不会被执行。对于我们的程序来说,如果发现鼠标单击的是空白块而不是图片块,就可以直接跳出on_mouse_down()函数,而无须继续执行后面的操作。

提示:

这样的编写方式并不影响程序的实际运行效果,但采用这种写法却可以减少一级缩进,从而让程序的结构更加简洁。

试着将之前的代码进行改写,并加入图片块朝其他方向移动的检查语句。修改后的on_mouse_down()函数如下所示:

def on_mouse_down(pos):
    grid_x = pos[0] // SIZE
    grid_y = pos[1] // SIZE
    thispic = get_pic(grid_x, grid_y)
    if thispic == None:
        return
    if grid_y > 0 and get_pic(grid_x, grid_y - 1) == None:
        thispic.y -= SIZE
        return
    if grid_y < 2 and get_pic(grid_x, grid_y + 1) == None:
        thispic.y += SIZE
        return
    if grid_x > 0 and get_pic(grid_x - 1, grid_y) == None:
        thispic.x -= SIZE
        return
    if grid_x < 2 and get_pic(grid_x + 1, grid_y) == None:
        thispic.x += SIZE
        return

可以看到,现在的代码最多只有两级缩进,代码的结构变得更加简单,可读性也相应地提高了。另外需要注意的是,程序在对各个方向进行检查并处理之后,都要调用return语句及时返回,以避免重复的移动操作(例如向上移动之后又立刻向下移回来)。

运行一下程序,看看是否可以用鼠标操作图片块移动了呢?

2.4 实现游戏结束

现在游戏是可以玩,但它不会完,即游戏无法结束,就算是玩家将图片块拼好后游戏也不会给出任何信息提示。这显然不能算是一个完整的游戏。对于玩家来说,玩游戏主要是获得乐趣,以及完成目标后的成就感,因此游戏要对玩家的操作及时做出反馈,明确告诉玩家是否达成了目标。

对于我们的拼图游戏来说,当玩家将图片块拼合成完整图像后,游戏要停止运行,并以文字或声音的形式告诉玩家完成了游戏。那么怎样知道玩家是否将拼图拼好了呢?下面编写程序进行检查。

2.4.1 检查拼图是否完成

若想知道拼图是否完成,需要知道各个图片块在拼图中的位置是否正确。对于某个图片块来说,怎样的位置才算正确呢?

回顾一下图2.1,可以看到,若所有图片块按照图中的位置摆放,则形成了一副完整的图像。同时注意到,各图片块的文件名是从左上至右下,按照从0到8的顺序依次进行编号的。最初创建图片块角色时,正是按照这样的编号顺序将它们加入列表中的,并且还为每个图片块定义了index属性,专门用来记录其对应的编号值。

然而,当调用随机函数打乱列表次序后,各图片块的位置不再按照编号顺序进行排列了。游戏的目标实际上就是通过移动图片块,以将其摆放到正确的位置上来,即让所有图片块按照0至8的编号顺序,从左上至右下依次摆放在程序窗口各方格中,如图2.4所示。

在这里插入图片描述

那怎么知道每个图片块是否放置在正确的位置上呢?可以按照图2.4中的编号顺序,依次获取每个方格中的图片块,看看它的index属性所记录的编号值是否与图中的编号相一致。若所有图片块的编号值都与图中的编号一致,则说明拼图完成了。换句话说,只要有一块的编号值与图中不一致,则说明拼图没有完成。由于最后一个图片块并没有加入列表中,因此只需要对前8个图片块的编号进行判定。在update()函数中编写如下代码:

在这里插入图片描述

上述代码通过一个计数循环进行判定,循环变量i从0到7的不同取值,对应着图2.4中各方格的编号值。在每次循环中,对i取余得到方格的列号,取整得到方格的行号。然后以列号和行号作为参数来调用get_pic()函数,从而获取该方格中的图片块,并保存到pic变量中。接着进行判断:如果pic为空白块,或者pic的index属性值与i的值不相等,说明该方格中的图片块编号不正确,程序便调用return语句直接返回;如果所有图片块的编号都是正确的,说明拼图完成了,return语句将不会被调用,程序会继续执行for循环之后的语句。于是便可以将游戏结束时需要执行的操作代码编写在for语句之后。

2.4.2 显示最后一张图片

游戏终于要完成了,耶!先别急着庆祝,仔细考虑一下游戏结束时还要做什么事情,以便让游戏显得更加完整。首先要播放一小段音乐来庆祝一下,同时还要在窗口中显示游戏完成的文字信息。对了,还有一件事差点忘记,就是拼图中还缺了一幅,游戏结束时要补上去。

先看看怎样将最后一块图片补充完整。为此还需要再定义一个图片块角色,并为其指定位置。代码如下所示:
在这里插入图片描述

上述代码定义了变量lastpic来保存最后一个图片块。由于最后一块所在方格的行号和列号都是2,于是分别用2乘以图片块尺寸SIZE,并赋给它的left和top属性。接着可以调用lastpic角色的draw()方法将它显示出来。

需要注意的是,我们希望最后这张图片在游戏结束时才显示出来,而不是一开始就显示。怎样才能做到这一点呢?具体来说,要设法对游戏的状态进行标识,只有当游戏状态为结束时才显示最后的图片块。这可以借助布尔变量来实现。

说明:

在Python语言中,布尔变量是一种基本的数据类型,它有两个取值:True和False,分别对应两个不同的状态值。为了表示游戏中的某种状态,经常会在游戏编程中使用到布尔变量。

对于拼图游戏来说,可以定义一个布尔变量用来标识游戏是否结束,True表示结束,False表示没有结束。于是在程序的前面加上一行代码:

finished = False

这里的变量finished就是一个布尔变量,它的初值为False,表示游戏尚未结束。接着对update()函数进行修改,代码如下(粗体部分表示新添加的部分):

def update():
    global finished
    if finished:
        return

    for i in range(8):
        pic = get_pic(i % 3, i // 3)
        if(pic == None or pic.index != i):
            return
    finished = True

上述代码首先判断布尔变量finished的值,若其为True,则说明游戏已经结束,于是调用return语句立即返回,余下的语句将不再执行;若finished的值不为True,则继续执行后面的for循环来判定拼图是否完成,若完成将会执行最后一行代码,将finished的值设置为True,表示游戏结束了。

提示:

finished是程序中的全局变量,它是在函数之外定义的,若要在函数内部对它的值进行修改,需要使用global关键字进行声明。

最后在draw()函数中加入以下代码:

if finished == True:
        lastpic.draw()

上述代码中,if语句的条件用来判断finished的值是否为True,若是则执行lastpic角色的draw()方法。

运行一下程序,你会发现最后一张图片并没有马上显示出来。你可以试着玩一下游戏,看看拼图完成时最后的图片块是否会显示。

2.4.3 播放声音效果

为了进一步完善游戏,可以考虑在游戏结束时播放一点声音效果,用来庆祝拼图完成,这会让玩家的游戏体验更加美好。那又如何让游戏播放声音呢?

说明:

准确来说,游戏中的声音分为两种类型,一种叫作音乐,另一种叫作音效。游戏音乐是一段比较长的声音,具有特定的旋律,通常作为景音乐来烘托游戏的气氛;游戏音效则是比较短小的声音,往往伴随着角色的动作或特定的事件而播放,用来增强游戏的交互效果。

Pgzero为游戏音乐和游戏音效分别提供了便捷的播放手段,而在拼图游戏中,只需要播放一小段游戏结束的音效就足够了。

首先准备好需要播放的音效文件。注意Pgzero只支持wav和ogg类型的音效文件,而且要将音效文件放入编辑器的sounds文件夹之中。可以单击Mu编辑器上方的“音效”按钮来打开sounds文件夹,然后将准备好的音效文件复制到该文件夹中。这里准备了一段充满胜利喜悦的音效,将其文件命名为win.wav。

接着在update()函数的最后编写如下一行代码:

sounds.win.play()

看到了吧,就是如此简单!程序使用关键字sounds,再加上音效文件名,就可以直接生成一个音效对象,然后调用该对象的play()方法来播放音效。是否再一次感受到Pgzero的便捷和强大呢?

现在重新运行一下游戏,再试着将拼图完成,看看游戏结束时能否听到声音效果呢?想必感觉很美妙吧!

2.4.4 显示文字信息

接下来再接再厉,完成最后一个小功能,就是显示游戏结束的文字信息。作为游戏提供给玩家的反馈,仅有声音是不够的,往往还得实现视觉层面的反馈,而文字信息便是最简单最直接的视觉反馈形式。

如同绘制基本的图形或图像,Pgzero也提供了非常简便的方式,用来在程序窗口中绘制文字。这可以借助screen.draw.text()方法来实现。例如希望游戏结束时,在窗口中央显示一串红色的巨大字符“Finished!”,便可以在draw()函数中编写如下代码:

if finished == True:
        lastpic.draw()
        screen.draw.text("Finished!", center=(WIDTH // 2, HEIGHT // 2),
                          fontsize=50, color="red")

可以看到,在screen.draw.text()方法中,最先传入的参数是要显示的字符串,接下来的参数center表示文字中心点的坐标位置;fontsize表示文字的大小;color表示文字的颜色。

运行一下游戏,当拼图完成后你会看到如图2.5所示的游戏结束画面。

在这里插入图片描述

至此我们完成了全部的拼图游戏代码编写,接下来好好放松一下,尽情地玩自己编写的游戏吧!

2.5 回顾与总结

在本章中,我们学习了如何编写一个拼图游戏。首先讨论了如何自动地从多个图片文件来创建图片块角色,并通过列表统一管理。然后学习了如何使用随机函数打乱列表中的图片块次序,并为各个图片块设置坐标。接下来着重介绍了如何对鼠标单击事件进行处理,使得玩家可以操作鼠标来移动图片块。还详细讨论了移动图片块的具体条件及操作步骤。最后对游戏结束的判定方法进行了细致的描述,同时简要介绍了如何播放游戏音效,以及如何在游戏中显示文字信息。

本章涉及的Pgzero库的新特性总结如表2.1所示。

在这里插入图片描述

下面给出拼图游戏的完整源程序代码。

# -*- coding:gb18030 -*-
# 拼图游戏源代码puzzle.py

import random
SIZE = 96             # 图片块尺寸为96
WIDTH = SIZE * 3      # 屏幕宽度
HEIGHT = SIZE * 3     # 屏幕高度
finished = False      # 游戏结束标记
pics = []             # 图片块列表

# 循环生成前8个图片块,并加入列表
for i in range(8):
    pic = Actor("puzzle_pic" + str(i))
    pic.index = i     # 图片块索引值
    pics.append(pic)

# 随机打乱列表中的图片块次序
random.shuffle(pics)

# 为列表中的图片设置初始位置
for i in range(8):
    pics[i].left = i % 3 * SIZE
    pics[i].top = i // 3 * SIZE

# 创建最后一个图片块
lastpic =Actor("puzzle_pic8")
lastpic.left = 2 * SIZE
lastpic.top = 2 * SIZE


# 更新游戏逻辑
def update():
    global finished
    if finished:
        return
    # 检查拼图是否完成
    for i in range(8):
        pic = get_pic(i % 3, i // 3)
        if(pic == None or pic.index != i):
            return
    finished = True
    sounds.win.play()   # 播放胜利的音效


# 绘制游戏角色
def draw():
    screen.fill((255, 255, 255))
    # 绘制前8个图片块
    for pic in pics:
        pic.draw()
    # 若游戏结束,绘制最后一块,并显示结束文字
    if finished == True:
        lastpic.draw()
        screen.draw.text("Finished!", center=(WIDTH // 2, HEIGHT // 2),
                          fontsize=50, color="red")


# 检测鼠标按下事件
def on_mouse_down(pos):
    if finished:
        return
    grid_x = pos[0] // SIZE
    grid_y = pos[1] // SIZE
    # 获取当前鼠标点击的图片块
    thispic = get_pic(grid_x, grid_y)
    if thispic == None:
        return
    # 判断图片块是否可以向上移动
    if grid_y > 0 and get_pic(grid_x, grid_y - 1) == None:
        thispic.y -= SIZE
        return
    # 判断图片块是否可以向下移动
    if grid_y < 2 and get_pic(grid_x, grid_y + 1) == None:
        thispic.y += SIZE
        return
    # 判断图片块是否可以向左移动
    if grid_x > 0 and get_pic(grid_x - 1, grid_y) == None:
        thispic.x -= SIZE
        return
    # 判断图片块是否可以向右移动
    if grid_x < 2 and get_pic(grid_x + 1, grid_y) == None:
        thispic.x += SIZE
        return


# 获取某个方格处的图片块,参数为方格的水平与垂直索引值
def get_pic(grid_x, grid_y):
    for pic in pics:
        if pic.x // SIZE == grid_x and pic.y // SIZE == grid_y:
            return pic
    return None

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值