这一节中,我们将把前面学到的内容集中在一起(包括动画精灵、碰撞检测和事件),建立一个简单的“球拍与球”游戏,类似于 Pong。
先来看一个简单的单机版本。我们的游戏需要:
一个来回反弹的球;
一个打球的球拍;
一种控制球拍的方法;
一种记录分数并在窗口上显示分数的方法;
一种确定有几条“命”的方法——你有几次机会。
我们将在构建程序过程中逐个分析以上的需求。
从前的美好时光
Pong 是最早人们在家里玩的视频游戏之一。原来的 Pong 游戏没有任何软件——只是一堆电路!那时还没有家用计算机。Pong 要插入到你的电视上,你要用操纵杆来控制“球拍”。下面是这个游戏在电视屏幕上的效果图:
很少有人知道的秘密:
奶奶不仅是一个 Pong 游戏高手,还是乒乓球世界冠军呢!
球
我们之前使用的沙滩球对于 Pong 游戏来说有点大。我们需要小一点的球。卡特和我为这个游戏想出了这个有些滑稽的网球小人:
嘿,如果你被球拍打来打去,也会吓得够呛!
我们将在这个游戏中使用动画精灵,所以需要为我们的球建立一个精灵,然后为它创建一个实例。我们将使用包含 __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 的模块显示文本。可以这样来使用。
术语箱
计算机图形学中,渲染(render)是指绘制某个东西,或者让它可见。
在这里,字符串就是玩家的分数(不过首先必须把它从一个 int 转换为一个 string)。
我们需要类似下面的代码,要放在代码清单 18-4 中的事件循环前面(而且要在 paddle=MyPaddleClass([270,400]) 代码行后面):
第一行中的第一个参数(这里是 None)可以告诉 Pygame 我们希望使用什么字体(类型样式)。通过传入 None,就是在告诉 Pygame 要使用一个默认字体。
然后,在事件循环内部,我们需要这样的代码:
这样每次循环时都会重绘分数文本。
当然了,卡特,我们还没有创建 points 变量(我正打算这么做呢)。在创建 font 对象的代码前面增加这样一行代码:
score = 0
现在,要跟踪分数……因为我们已经用球的 move 方法检测了球什么时候碰到窗口的顶边(来完成反弹),所以只需要在这里再增加几行:
Traceback (most recent call last): File \"C:...\", line 59, in <module>myBall.move File \"C:...\", line 24, in movescore = score + 1UnboundLocalError: local variable \'score\'referenced before assignment
唉呀!我们忘记命名空间的问题了。还记得第 15 章中那个又大又长的解释吗?现在可以看到命名空间的一个实际例子了。尽管我们确实有一个名为 score 的变量,但是这里试图从 Ball 类的 move 方法中使用这个变量。这个类在寻找一个名为 score 的局部变量,而这个局部变量并不存在。实际上,我们希望使用先前已经创建的全局变量,所以只需要告诉 move 方法使用全局变量 score,如下:
def move(self): global score
还要让 score_font(分数的 font 对象)和 score_surf(包含渲染文本的表面块)作为全局变量,因为它们是用 move 方法更新的。所以代码实际上应当像这样:
def move(self): global score, score_font, score_surf
现在应该能正常工作了!再试试看。应该能看到窗口左上角的分数,而且当你把球弹到窗口顶边时这个分数应该会增加。
跟踪还有几条命
现在来跟踪还有几条命。对目前来说,如果漏了球,它就会从窗口底边掉下去,再也看不到了。我们希望给玩家 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 呢?这有下面几个原因。
如果做了上述修改,并运行程序,你会看到玩家现在有 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 游戏添加一些声音。
你学到了什么
在这一章,你学到了以下内容。
测试题
1. 程序可以响应哪两种事件?
2. 处理事件的代码叫什么?
3. Pygame 检测按键时使用的事件类型名是什么?
4. MOUSEMOVE 事件的哪个属性指出了鼠标位于窗口的哪个位置?
5. 如何找出 Pygame 中下一个可用的事件编号(例如,如果你想添加一个用户事件)?
6. 如何创建一个定时器在 Pygame 中生成定时器事件?
7. 在 Pygame 窗口中显示文本时要使用什么对象?
8. 要让文本出现在一个 Pygame 窗口中,需要哪 3 个步骤?
动手试一试
1. 如果球没有碰到球拍的顶边,而是碰到了球拍的左右两边,有没有什么奇怪的现象发生?它会在球拍中间持续反弹一段时间。你明白这是为什么吗?你能解决这个问题吗?我在后面的答案中给出了一个解决方案,不过在看答案之前你自己先试试看。
2. 试着重写这个程序(代码清单 18-4 或代码清单 18-5),让球的反弹有点随机性。可以改变球在球拍或墙上反弹的方式,使用随机的速度,或者也可以采用你能想到的其他做法。(我们在第 15 章见过 random.randint 和 random.random,所以你应该知道如何生成随机数,包括整数和浮点数。)