这一节中,我们将把前面学到的内容集中在一起(包括动画精灵、碰撞检测和事件),建立一个简单的“球拍与球”游戏,类似于 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 的模块显示文本。可以这样来使用。  
 术语箱
计算机图形学中,渲染(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 呢?这有下面几个原因。  
 如果做了上述修改,并运行程序,你会看到玩家现在有 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 检测按键时使用的事件类型名是什么?   
- MOUSEMOVE 事件的哪个属性指出了鼠标位于窗口的哪个位置?   
- 如何找出 Pygame 中下一个可用的事件编号(例如,如果你想添加一个用户事件)?   
- 如何创建一个定时器在 Pygame 中生成定时器事件?   
- 在 Pygame 窗口中显示文本时要使用什么对象?   
- 要让文本出现在一个 Pygame 窗口中,需要哪 3 个步骤?   
动手试一试  
 - 如果球没有碰到球拍的顶边,而是碰到了球拍的左右两边,有没有什么奇怪的现象发生?它会在球拍中间持续反弹一段时间。你明白这是为什么吗?你能解决这个问题吗?我在后面的答案中给出了一个解决方案,不过在看答案之前你自己先试试看。   
- 试着重写这个程序(代码清单 18-4 或代码清单 18-5),让球的反弹有点随机性。可以改变球在球拍或墙上反弹的方式,使用随机的速度,或者也可 以采用你能想到的其他做法。(我们在第 15 章见过 random.randint 和 random.random,所以你应该知道如何生成随机数,包括整数和浮点数。)