这一节中,我们将把前面学到的内容集中在一起(包括动画精灵、碰撞检测和事件),建立一个简单的“球拍与球”游戏,类似于 Pong。
从前的美好时光
Pong 是最早人们在家里玩的视频游戏之一。原来的 Pong 游戏没有任何软件——只是一堆电路!那时还没有家用计算机。Pong 要插入到你的电视上,你要用操纵杆来控制“球拍”。下面是这个游戏在电视屏幕上的效果图:
很少有人知道的秘密:
奶奶不仅是一个 Pong 游戏高手,还是乒乓球世界冠军呢!
先来看一个简单的单机版本。我们的游戏需要:
一个来回反弹的球;
一个打球的球拍;
一种控制球拍的方法;
一种记录分数并在窗口上显示分数的方法;
一种确定有几条“命”的方法——你有几次机会。
我们将在构建程序过程中逐个分析以上的需求。
球
我们之前使用的沙滩球对于 Pong 游戏来说有点大。我们需要小一点的球。Carter 和我为这个游戏想出了这个有些滑稽的网球小人:
嘿,如果你被球拍打来打去,也会吓得够呛!
我们将在这个游戏中使用动画精灵,所以需要为我们的球建立一个精灵,然后为它创建一个实例。我们将使用包含 __init__ 和 move 方法的 Ball 类。
创建球的实例时,我们会告诉它使用哪个图像、球的速度以及球的起始位置:
myBall = MyBallClass(/'wackyball.bmp/', ball_speed, [50, 50])
还需要把这个球增加到一个组,以便完成球和球拍之间的碰撞检测。可以创建组,同时把球增加到这个组:
ballGroup = pygame.sprite.Group(myBall)
球拍
对于球拍,我们仍然坚持 Pong 游戏的传统,只是使用一个简单的矩形。我们将要使用一个白色背景,所以把球拍创建为一个黑色矩形。也要为球拍建立一个精灵类和实例:
注意,对于球拍,我们并没有加载图像文件:这里只是用黑色填充一个矩形表面来创建一个图像。不过,每个精灵都需要一个 image 属性,所以我们使用 Surface.convert 方法把表面转换为一个图像。
这个球拍只能左右移动,不能上下移动。我们让球拍的 x 位置(它的左右位置)跟着鼠标移动,所以用户可以用鼠标来控制球拍。因为这个工作在事件循环中完成,所以球拍不需要一个单独的 move 方法。
控制球拍
上一节已经提到过,我们将用鼠标控制球拍。这里要使用 MOUSEMOTION 事件,这说明只要鼠标在 Pygame 窗口内部移动,球拍就会移动。由于鼠标在 Pygame 窗口内时 Pygame 才能“看到”鼠标,所以球拍会自动限制在窗口的边界以内。我们将让球拍的中心跟随鼠标移动。
代码应当像这样:
elif event.type == pygame.MOUSEMOTION: paddle.rect.centerx = event.pos[0]
event.pos 是一个列表,包含鼠标位置的 [x, y] 值。所以 event.pos[0] 会提供鼠标移动时的 x 位置。当然,如果鼠标在左边界或右边界上,球拍会有一半在窗口之外,不过这是可以的。
还需要最后一点:球和球拍之间的碰撞检测。我们就是利用这种“碰撞”才能用球拍“打”球。出现碰撞时,只需让球的 y 速度反向(所以如果球在向下走,碰到球拍时它会反弹,开始向上移动)。代码如下:
if pygame.sprite.spritecollide(paddle, ballGroup, False): myBall.speed[1] = -myBall.speed[1]
还要记住每次循环时都要重绘。如果把这些内容都集中在一起,就得到了一个非常基本的类似 Pong 的程序。代码清单 18-4 给出了(至今为止)完整的代码。
代码清单 18-4 PyPong 的第一个版本
运行这个程序时应该能得到下面的结果。
也许吧,这可能不是最让人兴奋的游戏,不过我们只是刚刚起步,才开始在 Pygame 中编写游戏。下面再向我们的 PyPong 游戏加些东西。
记录分数并用 pygame.font 显示
我们要跟踪两个方面:还有几条命以及得了多少分。为了力求简单,每次球碰到窗口顶边时我们会给 1 分。另外给每个玩家 3 条命。
还需要一种方法来显示这个分数。Pygame 使用一个名为 font 的模块显示文本。可以这样来使用。
建立一个 font 对象,告诉 Pygame 你想要的字体样式和大小。
渲染文本,向字体对象传入一个字符串,它会返回一个绘制有这个文本的新的表面。
把这个表面块移到显示表面。
术语箱
计算机图形学中,渲染(render )是指绘制某个东西,或者让它可见。
在这里,字符串就是分数(不过首先必须把它从一个 int 转换为一个 string)。
我们需要类似下面的代码,要放在代码清单 18-4 中的事件循环前面(而且要在screen.fill([255, 255,255])代码行后面):
第一行中的第一个参数(这里是 None)可以告诉 Pygame 我们希望使用什么字体(类型样式)。通过传入 None,就是在告诉 Pygame 要使用一个默认字体。
然后,在事件循环内部,我们需要这样的代码:
这样每次循环时都会重绘分数文本。
当然了,Carter,我们还没有建立 points 变量。(我正打算创建这个变量呢。)在创建 font 对象的代码前面增加这样一行代码:
points = 0
现在,要跟踪分数……因为我们已经检测了球什么时候碰到窗口的顶边(来完成反弹),所以只需要在这里再增加几行:
Traceback (most recent call last): File /"C:.../", line 59, in <module>myBall.move File /"C:.../", line 24, in movepoints = points + 1UnboundLocalError: local variable /'points/'referenced before assignment
唉呀!我们忘记命名空间的问题了。还记得第 15 章中那个又大又长的解释吗?现在可以看到命名空间的一个实际例子了。尽管我们确实有一个名为 points 的变量,但是这里试图从 Ball 类的 move 方法中使用这个变量。这个类在寻找一个名为 points 的局部变量,而这个局部变量并不存在。实际上,我们希望使用先前已经创建的全局变量,所以只需要告诉 move 方法使用全局变量 points,如下:
def move(self): global points
还要让 score_text 作为一个全局变量,所以代码实际上应当像这样:
def move(self): global points, score_text
现在应该能正常工作了!再试试看。应该能看到窗口左上角的分数,而且当你把球弹到窗口顶边时这个分数应该会增加。
跟踪还有几条命
现在来跟踪还有几条命。对目前来说,如果漏了球,它就会从窗口底边掉下去,再也看不到了。我们希望给玩家 3 条命或者 3 个机会,所以下面建立一个名为 lives 的变量,把它设置为 3。
lives = 3
玩家漏了球而且球掉到窗口底边后,要将 lives 减 1,等待几秒,然后重新开始,又提供一个新球:
if myBall.rect.top >= screen.get_rect.bottom: lives = lives - 1 pygame.time.delay(2000) myBall.rect.topleft = [50, 50]
这个代码要放在 while 循环中。顺便说一句,为什么对于球我们会写成 myBall.rect,而对于 screen 要写为 get_rect 呢?这有下面几个原因。
myBall 是一个动画精灵,动画精灵都包含一个 rect。
screen 是一个表面,而表面不包含 rect。可以用 get_rect 函数找到包围一个表面的 rect。
如果做了上述修改,并运行程序,你会看到玩家现在有 3 条命。
增加一个生命计数器
很多游戏会给玩家多条命,大多数这样的游戏都会采用某种方法显示还剩下几条命。我们这个游戏也可以做到这一点。
一种简单的方法是显示一些球,剩几条命就显示几个球。可以把这些球放在右上角。以下是画出生命计数器的 for 循环中使用的小公式:
for i in range (lives): width = screen.get_rect.width screen.blit(myBall.image, [width - 40 * i, 20])
这个代码也要放在主 while 循环中,应当放在事件循环前面(但要在 screen.blit(score_text, textpos) 代码行之后)。
游戏结束
最后还需要增加一点:当玩家丢掉最后一条命时要显示一个“游戏结束”的消息。我们要建立两个字体对象,分别包含我们的消息和玩家的最后分数,渲染这两个文本(创建绘有文本的表面),再将这些表面块移到 screen。
另外还要在最后一局结束后避免球再次出现。为了做到这一点,要建立一个 done 变量告诉我们何时游戏结束。运行在主 while 循环中的以下代码会完成这项工作。
把所有这些内容集中在一起,可以得到最终的 PyPong 程序,如代码清单 18-5 所示。
代码清单 18-5 最终的 PyPong 代码
如果运行代码清单 18-5 中的 代码,应该能看到这样的结果。
如果在编辑器中注意观察,可以看到这大约有 75 行代码(加上一些空行)。这是目前为止我们创建的最大的程序了,虽然运行时看起来很简单,但却包含了丰富的内容。
下一章,我们将要学习 Pygame 中的声音,另外还会向这个 PyPong 游戏添加一些声音。
你学到了什么
在这一章,你学到了以下内容。
事件。
Pygame 事件循环。
事件处理。
键盘事件。
鼠标事件。
定时器事件(以及用户事件类型)。
pygame.font(用于向 Pygame 程序添加文本)。
把所有内容集中在一起建立一个游戏!
测试题
程序可以响应哪两种事件?
处理事件的代码叫什么?
Pygame 检测按键时使用的事件类型名是什么?
MOUSEMOVE 事件的哪个属性指出了鼠标位于窗口的哪个位置?
如何找出 Pygame 中下一个可用的事件编号(例如,如果你想添加一个用户事件)?
如何创建一个定时器在 Pygame 中生成定时器事件?
在 Pygame 窗口中显示文本时要使用什么对象?
要让文本出现在一个 Pygame 窗口中,需要哪 3 个步骤?
动手试一试
如果球没有碰到球拍的顶边,而是碰到了球拍的左右两边,有没有什么奇怪的现象发生?它会在球拍中间持续反弹一段时间。你明白这是为什么吗?你能解决这个问题吗?我在后面的答案中给出了一个解决方案,不过在看答案之前你自己先试试看。
试着重写这个程序(代码清单 18-4 或代码清单 18-5),让球的反弹有点随机性。可以改变球在球拍或墙上反弹的方式,使用随机的速度,或者也可 以采用你能想到的其他做法。(我们在第 15 章见过 random.randint 和 random.random,所以你应该知道如何生成随机数,包括整数和浮点数。)