首页 » 与孩子一起学编程 » 与孩子一起学编程全文在线阅读

《与孩子一起学编程》18.5 另一个游戏 PyPong

关灯直达底部

这一节中,我们将把前面学到的内容集中在一起(包括动画精灵、碰撞检测和事件),建立一个简单的“球拍与球”游戏,类似于 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 程序添加文本)。

  • 把所有内容集中在一起建立一个游戏!

测试题

 
  1. 程序可以响应哪两种事件?

  2. 处理事件的代码叫什么?

  3. Pygame 检测按键时使用的事件类型名是什么?

  4. MOUSEMOVE 事件的哪个属性指出了鼠标位于窗口的哪个位置?

  5. 如何找出 Pygame 中下一个可用的事件编号(例如,如果你想添加一个用户事件)?

  6. 如何创建一个定时器在 Pygame 中生成定时器事件?

  7. 在 Pygame 窗口中显示文本时要使用什么对象?

  8. 要让文本出现在一个 Pygame 窗口中,需要哪 3 个步骤?

动手试一试

 
  1. 如果球没有碰到球拍的顶边,而是碰到了球拍的左右两边,有没有什么奇怪的现象发生?它会在球拍中间持续反弹一段时间。你明白这是为什么吗?你能解决这个问题吗?我在后面的答案中给出了一个解决方案,不过在看答案之前你自己先试试看。

  2. 试着重写这个程序(代码清单 18-4 或代码清单 18-5),让球的反弹有点随机性。可以改变球在球拍或墙上反弹的方式,使用随机的速度,或者也可 以采用你能想到的其他做法。(我们在第 15 章见过 random.randintrandom.random,所以你应该知道如何生成随机数,包括整数和浮点数。)