十九、碰撞检测
原文:
inventwithpython.com/invent4thed/chapter19.html
译者:飞龙
协议:CC BY-NC-SA 4.0
碰撞检测涉及确定屏幕上的两个物体何时相互接触(即发生碰撞)。碰撞检测对于游戏非常有用。例如,如果玩家触碰到敌人,他们可能会失去生命值。或者如果玩家触碰到硬币,他们应该自动捡起它。碰撞检测可以帮助确定游戏角色是否站在坚实的地面上,或者他们脚下只有空气。
在我们的游戏中,碰撞检测将确定两个矩形是否重叠。本章的示例程序将涵盖这种基本技术。我们还将看看我们的pygame
程序如何通过键盘和鼠标接受玩家的输入。这比我们为文本程序所做的调用input()
函数要复杂一些。但在 GUI 程序中使用键盘要更加互动,而在我们的文本游戏中甚至无法使用鼠标。这两个概念将使您的游戏更加令人兴奋!
本章涵盖的主题
-
Clock
对象 -
pygame
中的键盘输入 -
pygame
中的鼠标输入 -
碰撞检测
-
在迭代列表时不修改列表
碰撞检测程序的示例运行
在这个程序中,玩家使用键盘的箭头键在屏幕上移动一个黑色的方块。较小的绿色方块代表食物,出现在屏幕上,方块触碰到它们时会“吃”掉它们。玩家可以在窗口的任何地方点击以创建新的食物方块。此外,按 ESC 键退出程序,按 X 键将玩家传送到屏幕上的随机位置。
图 19-1 显示了程序完成后的样子。
图 19-1: pygame 碰撞检测程序的屏幕截图
碰撞检测程序的源代码
开始一个新文件,输入以下代码,然后将其保存为collisionDetection.py。如果在输入此代码后出现错误,请使用在线 diff 工具将您输入的代码与本书代码进行比较,网址为www.nostarch.com/inventwithpython#diff
。
collision Detection.py
import pygame, sys, random
from pygame.locals import *
# Set up pygame.
pygame.init()
mainClock = pygame.time.Clock()
# Set up the window.
WINDOWWIDTH = 400
WINDOWHEIGHT = 400
windowSurface = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT),
0, 32)
pygame.display.set_caption('Collision Detection')
# Set up the colors.
BLACK = (0, 0, 0)
GREEN = (0, 255, 0)
WHITE = (255, 255, 255)
# Set up the player and food data structures.
foodCounter = 0
NEWFOOD = 40
FOODSIZE = 20
player = pygame.Rect(300, 100, 50, 50)
foods = []
for i in range(20):
foods.append(pygame.Rect(random.randint(0, WINDOWWIDTH - FOODSIZE),
random.randint(0, WINDOWHEIGHT - FOODSIZE), FOODSIZE, FOODSIZE))
# Set up movement variables.
moveLeft = False
moveRight = False
moveUp = False
moveDown = False
MOVESPEED = 6
# Run the game loop.
while True:
# Check for events.
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
sys.exit()
if event.type == KEYDOWN:
# Change the keyboard variables.
if event.key == K_LEFT or event.key == K_a:
moveRight = False
moveLeft = True
if event.key == K_RIGHT or event.key == K_d:
moveLeft = False
moveRight = True
if event.key == K_UP or event.key == K_w:
moveDown = False
moveUp = True
if event.key == K_DOWN or event.key == K_s:
moveUp = False
moveDown = True
if event.type == KEYUP:
if event.key == K_ESCAPE:
pygame.quit()
sys.exit()
if event.key == K_LEFT or event.key == K_a:
moveLeft = False
if event.key == K_RIGHT or event.key == K_d:
moveRight = False
if event.key == K_UP or event.key == K_w:
moveUp = False
if event.key == K_DOWN or event.key == K_s:
moveDown = False
if event.key == K_x:
player.top = random.randint(0, WINDOWHEIGHT -
player.height)
player.left = random.randint(0, WINDOWWIDTH -
player.width)
if event.type == MOUSEBUTTONUP:
foods.append(pygame.Rect(event.pos[0], event.pos[1],
FOODSIZE, FOODSIZE))
foodCounter += 1
if foodCounter >= NEWFOOD:
# Add new food.
foodCounter = 0
foods.append(pygame.Rect(random.randint(0, WINDOWWIDTH -
FOODSIZE), random.randint(0, WINDOWHEIGHT - FOODSIZE),
FOODSIZE, FOODSIZE))
# Draw the white background onto the surface.
windowSurface.fill(WHITE)
# Move the player.
if moveDown and player.bottom < WINDOWHEIGHT:
player.top += MOVESPEED
if moveUp and player.top > 0:
player.top -= MOVESPEED
if moveLeft and player.left > 0:
player.left -= MOVESPEED
if moveRight and player.right < WINDOWWIDTH:
player.right += MOVESPEED
# Draw the player onto the surface.
pygame.draw.rect(windowSurface, BLACK, player)
# Check whether the player has intersected with any food squares.
for food in foods[:]:
if player.colliderect(food):
foods.remove(food)
# Draw the food.
for i in range(len(foods)):
pygame.draw.rect(windowSurface, GREEN, foods[i])
# Draw the window onto the screen.
pygame.display.update()
mainClock.tick(40)
导入模块
pygame
碰撞检测程序导入了与第 18 章中的动画程序相同的模块,还有random
模块:
import pygame, sys, random
from pygame.locals import *
使用时钟来控制程序的节奏
第 5 到 17 行大部分做的事情与动画程序相同:它们初始化了pygame
,设置了WINDOWHEIGHT
和WINDOWWIDTH
,并分配了颜色和方向常量。
然而,第 6 行是新的:
mainClock = pygame.time.Clock()
在动画程序中,调用time.sleep(0.02)
会减慢程序的运行速度,以防止它运行得太快。虽然这个调用在所有计算机上都会暂停 0.02 秒,但程序的其余部分的速度取决于计算机的速度。如果我们希望这个程序在任何计算机上以相同的速度运行,我们需要一个函数,在快速计算机上暂停时间更长,在慢速计算机上暂停时间更短。
pygame.time.Clock
对象可以在任何计算机上暂停适当的时间。第 110 行在游戏循环内调用了mainClock.tick(40)
。对Clock
对象的tick()
方法的调用等待足够的时间,以便它以大约 40 次迭代每秒的速度运行,无论计算机的速度如何。这确保游戏永远不会比您预期的速度更快。对tick()
的调用应该只出现一次在游戏循环中。
设置窗口和数据结构
第 19 到 22 行设置了一些用于在屏幕上出现的食物方块的变量:
# Set up the player and food data structures.
foodCounter = 0
NEWFOOD = 40
FOODSIZE = 20
foodCounter
变量将从值0
开始,NEWFOOD
为40
,FOODSIZE
为20
。稍后我们将看到这些变量在创建食物时如何使用。
第 23 行设置了玩家位置的pygame.Rect
对象:
player = pygame.Rect(300, 100, 50, 50)
player
变量有一个pygame.Rect
对象,表示方块的大小和位置。玩家的方块将像动画程序中的方块一样移动(参见“移动每个方块”在第 280 页),但在这个程序中,玩家可以控制方块的移动方向。
接下来,我们设置了一些代码来跟踪食物方块:
foods = []
for i in range(20):
foods.append(pygame.Rect(random.randint(0, WINDOWWIDTH - FOODSIZE),
random.randint(0, WINDOWHEIGHT - FOODSIZE), FOODSIZE, FOODSIZE))
程序将使用foods
列表来跟踪每个食物方块的Rect
对象。第 25 和 26 行在屏幕周围随机放置了 20 个食物方块。您可以使用random.randint()
函数来生成随机的 x 和 y 坐标。
在第 26 行,程序调用pygame.Rect()
构造函数来返回一个新的pygame.Rect
对象。它将表示一个新食物方块的位置和大小。pygame.Rect()
的前两个参数是左上角的 x 和 y 坐标。您希望随机坐标在0
和窗口大小减去食物方块大小之间。如果将随机坐标设置在0
和窗口大小之间,那么食物方块可能会被推到窗口之外,就像图 19-2 中一样。
pygame.Rect()
的第三个和第四个参数是食物方块的宽度和高度。宽度和高度都是FOODSIZE
常量中的值。
图 19-2:对于 400×400 窗口中的 100×100 方块,将左上角设置为 400 会将矩形放在窗口外。要在内部,左边缘应该设置为 300。
pygame.Rect()
的第三个和第四个参数是食物方块的宽度和高度。宽度和高度都是FOODSIZE
常量中的值。
设置跟踪移动的变量
从第 29 行开始,代码设置了一些变量,用于跟踪玩家方块的移动方向:
# Set up movement variables.
moveLeft = False
moveRight = False
moveUp = False
moveDown = False
这四个变量具有布尔值,用于跟踪哪个箭头键被按下,并最初设置为False
。例如,当玩家按下键盘上的左箭头键移动方块时,moveLeft
被设置为True
。当他们松开键时,moveLeft
被设置回False
。
第 34 到 43 行几乎与以前的pygame
程序中的代码相同。这些行处理游戏循环的开始以及玩家退出程序时的操作。我们将跳过对此代码的解释,因为我们在上一章中已经涵盖过了。
处理事件
pygame
模块可以根据鼠标或键盘的用户输入生成事件。以下是pygame.event.get()
可以返回的事件:
QUIT
当玩家关闭窗口时生成。
KEYDOWN
当玩家按下键时生成。具有key
属性,告诉按下了哪个键。还有一个mod
属性,告诉按下该键时是否按下了 SHIFT、CTRL、ALT 或其他键。
KEYUP
当玩家释放键时生成。具有与KEYDOWN
类似的key
和mod
属性。
MOUSEMOTION
:每当鼠标在窗口上移动时生成。具有pos
属性(缩写为position),返回窗口中鼠标位置的元组(x, y)
。rel
属性还返回一个(x, y)
元组,但它给出自上一个MOUSEMOTION
事件以来的相对坐标。例如,如果鼠标从(200, 200)
向左移动 4 像素到(196, 200)
,那么rel
将是元组值(-4, 0)
。button
属性返回一个三个整数的元组。元组中的第一个整数是左鼠标按钮,第二个整数是中间鼠标按钮(如果存在),第三个整数是右鼠标按钮。如果鼠标移动时它们没有被按下,则这些整数将为0
,如果它们被按下,则为1
。
MOUSEBUTTONDOWN
:当鼠标在窗口中按下按钮时生成。此事件具有pos
属性,它是鼠标按下按钮时鼠标位置的(x, y)
元组。还有一个button
属性,它是从1
到5
的整数,告诉哪个鼠标按钮被按下,如表 19-1 中所述。
MOUSEBUTTONUP
:当鼠标按钮释放时生成。这与MOUSEBUTTONDOWN
具有相同的属性。
当生成MOUSEBUTTONDOWN
事件时,它具有button
属性。button
属性是与鼠标可能具有的不同类型的按钮相关联的值。例如,左键的值为1
,右键的值为3
。表 19-1 列出了鼠标事件的所有button
属性,但请注意,鼠标可能没有这里列出的所有button
值。
表 19-1: button
属性值
button 的值 | 鼠标按钮 |
---|---|
1 | 左键 |
2 | 中键 |
3 | 右键 |
4 | 滚轮向上滚动 |
5 | 滚轮向下滚动 |
我们将使用这些事件来让玩家使用KEYDOWN
事件和鼠标按钮点击来控制框。
处理 KEYDOWN 事件
处理按键和释放事件的代码从第 44 行开始;它包括KEYDOWN
事件类型:
if event.type == KEYDOWN:
如果事件类型是KEYDOWN
,则Event
对象具有一个key
属性,指示按下了哪个键。当玩家按下箭头键或 WASD 键(发音为wazz-dee,这些键与箭头键的布局相同,但位于键盘左侧)时,我们希望移动框。我们将使用if
语句来检查按下的键,以便确定框应该移动的方向。
第 46 行将key
属性与K_LEFT
和K_a
进行比较,它们是表示键盘上左箭头键和 WASD 中 A 的pygame.locals
常量。第 46 至 57 行检查每个箭头和 WASD 键:
# Change the keyboard variables.
if event.key == K_LEFT or event.key == K_a:
moveRight = False
moveLeft = True
if event.key == K_RIGHT or event.key == K_d:
moveLeft = False
moveRight = True
if event.key == K_UP or event.key == K_w:
moveDown = False
moveUp = True
if event.key == K_DOWN or event.key == K_s:
moveUp = False
moveDown = True
当按下这些键之一时,代码告诉 Python 将相应的移动变量设置为True
。Python 还会将相反方向的移动变量设置为False
。
例如,当按下左箭头键时,程序执行第 47 和 48 行。在这种情况下,Python 将moveLeft
设置为True
,moveRight
设置为False
(即使moveRight
可能已经是False
,Python 也会将其设置为False
,以确保)。
在第 46 行,event.key
可以等于K_LEFT
或K_a
。如果按下左箭头键,则event.key
中的值将设置为与K_LEFT
相同的值,如果按下 A 键,则设置为与K_a
相同的值。
通过执行第 47 和 48 行的代码,如果按键是K_LEFT
或K_a
,则左箭头键和 A 键将执行相同的操作。W、A、S 和 D 键用作更改移动变量的替代键,让玩家可以使用左手而不是右手。您可以在图 19-3 中看到这两组键的示例。
图 19-3:WASD 键可以编程为与箭头键执行相同的操作。
字母和数字键的常量很容易找到:A 键的常量是K_a
,B 键的常量是K_b
,依此类推。3 键的常量是K_3
。表 19-2 列出了其他键盘键的常用常量变量。
**表 19-2:**键盘键的常量变量
pygame 常量变量 | 键盘键 |
---|---|
K_LEFT | 左箭头 |
K_RIGHT | 右箭头 |
K_UP | 上箭头 |
K_DOWN | 下箭头 |
K_ESCAPE | ESC |
K_BACKSPACE | 退格键 |
K_TAB | TAB |
K_RETURN | RETURN 或 ENTER |
K_SPACE | 空格键 |
K_DELETE | DEL |
K_LSHIFT | 左 SHIFT |
K_RSHIFT | 右 SHIFT |
K_LCTRL | 左 CTRL |
K_RCTRL | 右 CTRL |
K_LALT | 左 ALT |
K_RALT | 右 ALT |
K_HOME | HOME |
K_END | END |
K_PAGEUP | PGUP |
K_PAGEDOWN | PGDN |
K_F1 | F1 |
K_F2 | F2 |
K_F3 | F3 |
K_F4 | F4 |
K_F5 | F5 |
K_F6 | F6 |
K_F7 | F7 |
K_F8 | F8 |
K_F9 | F9 |
K_F10 | F10 |
K_F11 | F11 |
K_F12 | F12 |
处理 KEYUP 事件
当玩家释放他们按下的键时,将生成一个KEYUP
事件:
if event.type == KEYUP:
如果玩家释放的键是 ESC,则 Python 应终止程序。请记住,在pygame
中,您必须在调用sys.exit()
函数之前调用pygame.quit()
函数,我们在第 59 到 61 行中这样做:
if event.key == K_ESCAPE:
pygame.quit()
sys.exit()
第 62 到 69 行如果释放了该方向键,则将移动变量设置为False
:
if event.key == K_LEFT or event.key == K_a:
moveLeft = False
if event.key == K_RIGHT or event.key == K_d:
moveRight = False
if event.key == K_UP or event.key == K_w:
moveUp = False
if event.key == K_DOWN or event.key == K_s:
moveDown = False
通过KEYUP
事件将移动变量设置为False
会使框停止移动。
传送玩家
您还可以将传送添加到游戏中。如果玩家按下 X 键,则第 71 和 72 行将玩家框的位置设置为窗口上的随机位置:
if event.key == K_x:
player.top = random.randint(0, WINDOWHEIGHT -
player.height)
player.left = random.randint(0, WINDOWWIDTH -
player.width)
第 70 行检查玩家是否按下了 X 键。然后,第 71 行设置一个随机的 x 坐标,将玩家传送到窗口的高度减去玩家矩形的高度之间。第 72 行执行类似的代码,但是针对 y 坐标。这使玩家可以通过按 X 键在窗口周围传送,但他们无法控制将传送到哪里——这是完全随机的。
添加新的食物方块
玩家可以通过两种方式向屏幕添加新的食物方块。他们可以单击窗口中希望新食物方块出现的位置,或者他们可以等到游戏循环迭代NEWFOOD
次数,这样新的食物方块将在窗口上随机生成。
我们首先看一下如何通过玩家的鼠标输入添加食物:
if event.type == MOUSEBUTTONUP:
foods.append(pygame.Rect(event.pos[0], event.pos[1],
FOODSIZE, FOODSIZE))
鼠标输入与键盘输入一样通过事件处理。当玩家在单击鼠标后释放鼠标按钮时,将发生MOUSEBUTTONUP
事件。
第 75 行中,x 坐标存储在event.pos[0]
中,y 坐标存储在event.pos[1]
中。第 75 行创建一个新的Rect
对象来表示一个新的食物方块,并将其放置在MOUSEBUTTONUP
事件发生的地方。通过向foods
列表添加新的Rect
对象,代码在屏幕上显示一个新的食物方块。
除了可以由玩家自行添加外,食物方块还可以通过第 77 到 81 行的代码自动生成:
foodCounter += 1
if foodCounter >= NEWFOOD:
# Add new food.
foodCounter = 0
foods.append(pygame.Rect(random.randint(0, WINDOWWIDTH -
FOODSIZE), random.randint(0, WINDOWHEIGHT - FOODSIZE),
FOODSIZE, FOODSIZE))
变量foodCounter
跟踪应添加食物的频率。每次游戏循环迭代时,foodCounter
在第 77 行增加1
。
一旦foodCounter
大于或等于常量NEWFOOD
,foodCounter
将被重置,并且通过第 81 行生成一个新的食物方块。您可以通过调整第 21 行上的NEWFOOD
来改变添加新食物方块的速度。
84 行只是用白色填充窗口表面,我们在“处理玩家退出时”和第 279 页中已经讨论过了,所以我们将继续讨论玩家如何在屏幕上移动。
在窗口中移动玩家
我们已将移动变量(moveDown
,moveUp
,moveLeft
和moveRight
)设置为True
或False
,具体取决于玩家按下了哪些键。现在我们需要移动玩家的方框,该方框由存储在player
中的pygame.Rect
对象表示。我们将通过调整player
的 x 和 y 坐标来实现这一点。
# Move the player.
if moveDown and player.bottom < WINDOWHEIGHT:
player.top += MOVESPEED
if moveUp and player.top > 0:
player.top -= MOVESPEED
if moveLeft and player.left > 0:
player.left -= MOVESPEED
if moveRight and player.right < WINDOWWIDTH:
player.right += MOVESPEED
如果moveDown
设置为True
(并且玩家的方框底部不在窗口的底部之下),则第 88 行将通过将MOVESPEED
添加到玩家当前的top
属性来向下移动玩家的方框。第 89 到 94 行对其他三个方向执行相同的操作。
在窗口上绘制玩家
第 97 行在窗口上绘制玩家的方框:
# Draw the player onto the surface.
pygame.draw.rect(windowSurface, BLACK, player)
在移动方框之后,第 97 行将其绘制在新位置。传递给第一个参数的windowSurface
告诉 Python 在哪个Surface
对象上绘制矩形。存储在BLACK
变量中的(0, 0, 0)
告诉 Python 绘制黑色矩形。存储在player
变量中的Rect
对象告诉 Python 要绘制的矩形的位置和大小。
检查碰撞
在绘制食物方块之前,程序需要检查玩家的方框是否与任何方块重叠。如果是,则需要从foods
列表中删除该方块。这样,Python 就不会绘制任何盒子已经吃掉的食物方块。
我们将在第 101 行使用所有Rect
对象都具有的碰撞检测方法colliderect()
:
# Check whether the player has intersected with any food squares.
for food in foods[:]:
if player.colliderect(food):
foods.remove(food)
在每次for
循环迭代中,将foods
(复数)列表中的当前食物方块放入变量food
(单数)中。pygame.Rect
对象的colliderect()
方法将玩家矩形的pygame.Rect
对象作为参数,并在两个矩形发生碰撞时返回True
,如果它们没有发生碰撞,则返回False
。如果为True
,第 102 行将从foods
列表中移除重叠的食物方块。
不要在迭代列表时更改列表
请注意,这个for
循环与我们以前看到的任何其他for
循环略有不同。如果您仔细看第 100 行,它并不是在foods
上进行迭代,而是在foods[:]
上进行迭代。
记住切片的工作原理。foods[:2]
将计算列表的副本,其中包含从开头到(但不包括)索引2
的项目。foods[:]
将为您提供包含从开头到结尾的项目的列表的副本。基本上,foods[:]
创建一个新列表,其中包含foods
中所有项目的副本。这是复制列表的一种更简洁的方法,比如在第 10 章的井字棋游戏中getBoardCopy()
函数所做的。
在迭代列表时,您不能添加或删除项目。如果 foods 列表的大小始终在变化,Python 可能会丢失 food 变量的下一个值应该是什么。想象一下,当有人添加或删除果冻豆时,要数出罐子里果冻豆的数量会有多困难。
但是,如果您迭代列表的副本(并且副本永远不会更改),则从原始列表中添加或删除项目将不会成为问题。
在窗口上绘制食物方块
第 105 行和 106 行的代码类似于我们用来为玩家绘制黑色方框的代码:
# Draw the food.
for i in range(len(foods)):
pygame.draw.rect(windowSurface, GREEN, foods[i])
第 105 行循环遍历foods
列表中的每个食物方块,第 106 行将食物方块绘制到windowSurface
上。
现在玩家和食物方块都在屏幕上,窗口已准备好更新,因此我们在第 109 行调用update()
方法,并通过在我们之前创建的Clock
对象上调用tick()
方法来完成程序:
# Draw the window onto the screen.
pygame.display.update()
mainClock.tick(40)
程序将继续通过游戏循环并保持更新,直到玩家退出。
总结
本章介绍了碰撞检测的概念。在图形游戏中,检测两个矩形之间的碰撞是如此普遍,以至于pygame
为pygame.Rect
对象提供了自己的碰撞检测方法,名为colliderect()
。
这本书中的前几个游戏都是基于文本的。程序的输出是打印在屏幕上的文本,输入是玩家在键盘上输入的文本。但是图形程序也可以接受键盘和鼠标输入。
此外,这些程序可以在玩家按下或释放单个键时响应单个按键。玩家不必输入整个响应并按下 ENTER 键。这样可以实现即时反馈和更加互动的游戏。
这个交互式程序很有趣,但让我们超越绘制矩形。在第 20 章中,你将学习如何使用pygame
加载图像和播放音效。
二十、使用声音和图像
原文:
inventwithpython.com/invent4thed/chapter20.html
译者:飞龙
协议:CC BY-NC-SA 4.0
在第 18 章和第 19 章中,您学习了如何制作具有图形并可以接受键盘和鼠标输入的 GUI 程序。您还学会了如何绘制不同的形状。在本章中,您将学习如何向游戏中添加声音、音乐和图像。
本章涵盖的主题
-
声音和图像文件
-
绘制和调整精灵的大小
-
添加音乐和声音
-
切换声音开关
使用精灵添加图像
sprite是屏幕上用作图形的一部分的单个二维图像。图 20-1 显示了一些示例 sprite。
图 20-1:一些精灵的示例
精灵图像绘制在背景上。您可以水平翻转精灵图像,使其面向另一边。您还可以在同一窗口上多次绘制相同的精灵图像,并且可以调整精灵的大小,使其比原始精灵图像大或小。背景图像也可以被视为一个大精灵。图 20-2 显示了精灵一起使用。
图 20-2:一个完整的场景,精灵绘制在背景上
下一个程序将演示如何使用pygame
播放声音和绘制精灵。
声音和图像文件
精灵存储在计算机上的图像文件中。pygame
可以使用几种图像格式。要了解图像文件使用的格式,请查看文件名的末尾(最后一个句点之后)。这称为文件扩展名。例如,文件player.png是 PNG 格式。pygame
支持的图像格式包括 BMP、PNG、JPG 和 GIF。
您可以从网络浏览器下载图像。在大多数网络浏览器上,您可以通过右键单击网页中的图像,然后从出现的菜单中选择“保存”来这样做。记住您保存图像文件的硬盘位置,因为您需要将下载的图像文件复制到与 Python 程序的*.py*文件相同的文件夹中。您还可以使用诸如 Microsoft Paint 或 Tux Paint 之类的绘图程序创建自己的图像。
pygame
支持的声音文件格式为 MIDI、WAV 和 MP3。您可以像下载图像文件一样从互联网下载音效文件,但音频文件必须是这三种格式之一。如果您的计算机有麦克风,您还可以录制声音并制作自己的 WAV 文件以在游戏中使用。
Sprites and Sounds 程序的示例运行
本章的程序与第 19 章的碰撞检测程序相同。但是,在本程序中,我们将使用精灵而不是普通的方块。我们将使用一个人的精灵来代表玩家,而不是黑色方块,以及樱桃的精灵,而不是绿色的食物方块。我们还将播放背景音乐,并在玩家精灵吃掉樱桃时添加声音效果。
在这个游戏中,玩家精灵将吃掉樱桃精灵,并且在吃掉樱桃时,它会变大。当您运行程序时,游戏将看起来像图 20-3。
图 20-3:Sprites and Sounds 游戏的屏幕截图
Sprites and Sounds 程序的源代码
开始一个新文件,输入以下代码,然后将其保存为spritesAndSounds.py。你可以从本书的网站www.nostarch.com/inventwithpython/
下载我们在本程序中使用的图像和声音文件。将这些文件放在与spritesAndSounds.py程序相同的文件夹中。
如果在输入此代码后出现错误,请使用在线的 diff 工具比较你输入的代码和书中的代码,网址为www.nostarch.com/inventwithpython#diff
。
spritesAnd Sounds.py
import pygame, sys, time, random
from pygame.locals import *
# Set up pygame.
pygame.init()
mainClock = pygame.time.Clock()
# Set up the window.
WINDOWWIDTH = 400
WINDOWHEIGHT = 400
windowSurface = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT),
0, 32)
pygame.display.set_caption('Sprites and Sounds')
# Set up the colors.
WHITE = (255, 255, 255)
# Set up the block data structure.
player = pygame.Rect(300, 100, 40, 40)
playerImage = pygame.image.load('player.png')
playerStretchedImage = pygame.transform.scale(playerImage, (40, 40))
foodImage = pygame.image.load('cherry.png')
foods = []
for i in range(20):
foods.append(pygame.Rect(random.randint(0, WINDOWWIDTH - 20),
random.randint(0, WINDOWHEIGHT - 20), 20, 20))
foodCounter = 0
NEWFOOD = 40
# Set up keyboard variables.
moveLeft = False
moveRight = False
moveUp = False
moveDown = False
MOVESPEED = 6
# Set up the music.
pickUpSound = pygame.mixer.Sound('pickup.wav')
pygame.mixer.music.load('background.mid')
pygame.mixer.music.play(-1, 0.0)
musicPlaying = True
# Run the game loop.
while True:
# Check for the QUIT event.
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
sys.exit()
if event.type == KEYDOWN:
# Change the keyboard variables.
if event.key == K_LEFT or event.key == K_a:
moveRight = False
moveLeft = True
if event.key == K_RIGHT or event.key == K_d:
moveLeft = False
moveRight = True
if event.key == K_UP or event.key == K_w:
moveDown = False
moveUp = True
if event.key == K_DOWN or event.key == K_s:
moveUp = False
moveDown = True
if event.type == KEYUP:
if event.key == K_ESCAPE:
pygame.quit()
sys.exit()
if event.key == K_LEFT or event.key == K_a:
moveLeft = False
if event.key == K_RIGHT or event.key == K_d:
moveRight = False
if event.key == K_UP or event.key == K_w:
moveUp = False
if event.key == K_DOWN or event.key == K_s:
moveDown = False
if event.key == K_x:
player.top = random.randint(0, WINDOWHEIGHT -
player.height)
player.left = random.randint(0, WINDOWWIDTH -
player.width)
if event.key == K_m:
if musicPlaying:
pygame.mixer.music.stop()
else:
pygame.mixer.music.play(-1, 0.0)
musicPlaying = not musicPlaying
if event.type == MOUSEBUTTONUP:
foods.append(pygame.Rect(event.pos[0] - 10,
event.pos[1] - 10, 20, 20))
foodCounter += 1
if foodCounter >= NEWFOOD:
# Add new food.
foodCounter = 0
foods.append(pygame.Rect(random.randint(0, WINDOWWIDTH - 20),
random.randint(0, WINDOWHEIGHT - 20), 20, 20))
# Draw the white background onto the surface.
windowSurface.fill(WHITE)
# Move the player.
if moveDown and player.bottom < WINDOWHEIGHT:
player.top += MOVESPEED
if moveUp and player.top > 0:
player.top -= MOVESPEED
if moveLeft and player.left > 0:
player.left -= MOVESPEED
if moveRight and player.right < WINDOWWIDTH:
player.right += MOVESPEED
# Draw the block onto the surface.
windowSurface.blit(playerStretchedImage, player)
# Check whether the block has intersected with any food squares.
for food in foods[:]:
if player.colliderect(food):
foods.remove(food)
player = pygame.Rect(player.left, player.top,
player.width + 2, player.height + 2)
playerStretchedImage = pygame.transform.scale(playerImage,
(player.width, player.height))
if musicPlaying:
pickUpSound.play()
# Draw the food.
for food in foods:
windowSurface.blit(foodImage, food)
# Draw the window onto the screen.
pygame.display.update()
mainClock.tick(40)
设置窗口和数据结构
这个程序中的大部分代码与第 19 章中的碰撞检测程序相同。我们只关注添加精灵和声音的部分。首先,在第 12 行,让我们将标题栏的标题设置为描述这个程序的字符串:
pygame.display.set_caption('Sprites and Sounds')
为了设置标题,你需要将字符串'Sprites and Sounds'
传递给pygame.display.set_caption()
函数。
添加一个精灵
现在我们已经设置好标题,我们需要实际的精灵。我们将使用三个变量来表示玩家,而不是之前程序中只使用一个。
# Set up the block data structure.
player = pygame.Rect(300, 100, 40, 40)
playerImage = pygame.image.load('player.png')
playerStretchedImage = pygame.transform.scale(playerImage, (40, 40))
foodImage = pygame.image.load('cherry.png')
第 18 行的player
变量将存储一个Rect
对象,用于跟踪玩家的位置和大小。player
变量不包含玩家的图像。在程序开始时,玩家的左上角位于(300, 100),玩家的初始高度和宽度为 40 像素。
表示玩家的第二个变量是第 19 行的playerImage
。pygame.image.load()
函数传递了要加载的图像文件的文件名字符串。返回值是一个Surface
对象,其中包含图像文件中的图形。我们将这个Surface
对象存储在playerImage
中。
改变精灵的大小
在第 20 行,我们将使用pygame.transform
模块中的一个新函数。pygame.transform.scale()
函数可以缩小或放大精灵。第一个参数是一个带有图像的Surface
对象。第二个参数是一个元组,表示第一个参数中图像的新宽度和高度。scale()
函数返回一个带有以新尺寸绘制的图像的Surface
对象。在本章的程序中,当玩家吃更多樱桃时,我们将使玩家精灵拉伸变大。我们将原始图像存储在playerImage
变量中,而拉伸后的图像存储在playerStretchedImage
变量中。
在第 21 行,我们再次调用load()
来创建一个带有樱桃图像的Surface
对象。确保player.png和cherry.png文件与spritesAndSounds.py文件在同一个文件夹中;否则,pygame
将无法找到它们并报错。
设置音乐和声音
接下来需要加载声音文件。pygame
中有两个用于声音的模块。pygame.mixer
模块可以在游戏过程中播放短声音效果。pygame.mixer.music
模块可以播放背景音乐。
添加声音文件
调用pygame.mixer.Sound()
构造函数来创建一个pygame.mixer.Sound
对象(简称为Sound
对象)。这个对象有一个play()
方法,当调用时会播放声音效果。
# Set up the music.
pickUpSound = pygame.mixer.Sound('pickup.wav')
pygame.mixer.music.load('background.mid')
pygame.mixer.music.play(-1, 0.0)
musicPlaying = True
第 39 行调用pygame.mixer.music.load()
来加载背景音乐,第 40 行调用pygame.mixer.music.play()
来开始播放它。第一个参数告诉pygame
在第一次播放后播放背景音乐的次数。因此,传递5
会导致pygame
播放背景音乐六次。在这里,我们传递参数-1
,这是一个特殊值,使背景音乐永远重复播放。
play()
的第二个参数是开始播放声音文件的时间点。传递0.0
将从开头播放背景音乐。传递2.5
将从开头开始播放背景音乐 2.5 秒。
最后,musicPlaying
变量具有一个布尔值,告诉程序是否应该播放背景音乐和声音效果。给玩家选择在没有声音的情况下运行程序是很好的。
切换声音的开关
按 M 键可以打开或关闭背景音乐。如果musicPlaying
设置为True
,则当前正在播放背景音乐,我们应该通过调用pygame.mixer.music.stop()
来停止它。如果musicPlaying
设置为False
,则当前没有播放背景音乐,我们应该通过调用play()
来开始播放。第 79 到 84 行使用if
语句来实现这一点:
if event.key == K_m:
if musicPlaying:
pygame.mixer.music.stop()
else:
pygame.mixer.music.play(-1, 0.0)
musicPlaying = not musicPlaying
无论音乐是否正在播放,我们都希望切换musicPlaying
中的值。切换布尔值意味着将值设置为其当前值的相反值。musicPlaying = not musicPlaying
这一行将变量设置为False
,如果它当前为True
,或者如果它当前为False
,则将其设置为True
。想象一下切换就像你打开或关闭灯开关时发生的事情:切换灯开关会将其设置为相反的设置。
在窗口上绘制玩家
请记住,存储在playerStretchedImage
中的值是一个Surface
对象。第 110 行使用blit()
将玩家的精灵绘制到窗口的Surface
对象上(存储在windowSurface
中):
# Draw the block onto the surface.
windowSurface.blit(playerStretchedImage, player)
blit()
方法的第二个参数是一个Rect
对象,指定在Surface
对象上绘制精灵的位置。程序使用存储在player
中的Rect
对象,它跟踪玩家在窗口中的位置。
检查碰撞
这段代码与以前的程序中的代码类似,但有几行新代码:
if player.colliderect(food):
foods.remove(food)
player = pygame.Rect(player.left, player.top,
player.width + 2, player.height + 2)
playerStretchedImage = pygame.transform.scale(playerImage,
(player.width, player.height))
if musicPlaying:
pickUpSound.play()
当玩家精灵吃掉樱桃之一时,其大小增加两个像素的高度和宽度。在第 116 行,一个比旧的Rect
对象大两个像素的新Rect
对象将被分配为player
的新值。
虽然Rect
对象表示玩家的位置和大小,但玩家的图像存储在playerStretchedImage
中,作为Surface
对象。在第 117 行,程序通过调用scale()
创建一个新的拉伸图像。
拉伸图像通常会使图像略微失真。如果你不断地重新拉伸已经拉伸过的图像,失真会迅速累积。但通过每次将原始图像拉伸到新的尺寸——通过传递playerImage
,而不是playerStretchedImage
,作为scale()
的第一个参数——你只会使图像失真一次。
最后,第 119 行调用存储在pickUpSound
变量中的Sound
对象上的play()
方法。但只有当musicPlaying
设置为True
时才会这样做(这意味着声音已打开)。
在窗口上绘制樱桃
在以前的程序中,你调用pygame.draw.rect()
函数为foods
列表中存储的每个Rect
对象绘制一个绿色的正方形。然而,在这个程序中,你想要绘制樱桃精灵。调用blit()
方法并传递存储在foodImage
中的Surface
对象,上面绘制了樱桃图像:
# Draw the food.
for food in foods:
windowSurface.blit(foodImage, food)
food
变量包含foods
中的每个Rect
对象,在每次for
循环中告诉blit()
方法在哪里绘制foodImage
。
总结
你已经为你的游戏添加了图像和声音。这些称为精灵的图像看起来比以前的程序中使用的简单绘制的形状要好得多。精灵可以被缩放(即拉伸)到更大或更小的尺寸,因此我们可以以任何我们想要的尺寸显示精灵。本章介绍的游戏还有一个背景并播放声音效果。
现在我们知道如何创建窗口,显示精灵,绘制基本图形,收集键盘和鼠标输入,播放声音,并实现碰撞检测,我们准备在pygame
中创建一个图形游戏。第 21 章将所有这些元素结合起来,打造我们迄今为止最先进的游戏。
二十一、有声音和图像的《躲避者》游戏
原文:
inventwithpython.com/invent4thed/chapter21.html
译者:飞龙
协议:CC BY-NC-SA 4.0
前面的四章介绍了 pygame
模块,并演示了如何使用它的许多功能。在本章中,我们将利用这些知识创建一个名为《躲避者》的图形游戏。
本章涵盖的主题
-
pygame.FULLSCREEN
标志 -
move_ip() Rect
方法 -
实现作弊码
-
修改《躲避者》游戏
在《躲避者》游戏中,玩家控制一个精灵(玩家角色),必须躲避从屏幕顶部掉落的一大堆坏人。玩家能够躲避坏人的时间越长,他们的得分就会越高。
只是为了好玩,我们还将在这个游戏中添加一些作弊模式。如果玩家按住 X 键,每个坏人的速度都会降低到超慢的速度。如果玩家按住 Z 键,坏人将会改变方向,向上而不是向下移动。
基本 pygame 数据类型的回顾
在我们开始制作《躲避者》之前,让我们回顾一下 pygame
中使用的一些基本数据类型:
pygame.Rect
Rect
对象表示矩形空间的位置和大小。位置由 Rect
对象的 topleft
属性(或 topright
、bottomleft
和 bottomright
属性)确定。这些角属性是 x 和 y 坐标的整数元组。大小由 width
和 height
属性确定,这些属性是指示矩形有多长或多高的整数像素。Rect
对象有一个 colliderect()
方法,用于检查它们是否与另一个 Rect
对象发生碰撞。
pygame.Surface
Surface
对象是有色像素区域。Surface
对象表示一个矩形图像,而 Rect
对象只表示一个矩形空间和位置。Surface
对象有一个 blit()
方法,用于将一个 Surface
对象上的图像绘制到另一个 Surface
对象上。pygame.display.set_mode()
函数返回的 Surface
对象是特殊的,因为在该 Surface
对象上绘制的任何东西在调用 pygame.display.update()
时会显示在用户的屏幕上。
pygame.event.Event
pygame.event
模块在用户提供键盘、鼠标或其他输入时生成 Event
对象。pygame.event.get()
函数返回这些 Event
对象的列表。您可以通过检查其 type
属性来确定 Event
对象的类型。QUIT
、KEYDOWN
和 MOUSEBUTTONUP
是一些事件类型的示例(有关所有事件类型的完整列表,请参见“处理事件”第 292 页)。
pygame.font.Font
pygame.font
模块使用 Font
数据类型,表示 pygame
中文本使用的字体。传递给 pygame.font.SysFont()
的参数是字体名称的字符串(通常传递 None
作为字体名称以获取默认系统字体)和字体大小的整数。
pygame.time.Clock
pygame.time
模块中的 Clock
对象有助于防止我们的游戏运行得比玩家能看到的更快。Clock
对象有一个 tick()
方法,可以传递我们希望游戏运行的每秒帧数(FPS)。FPS 越高,游戏运行得越快。
《躲避者》的示例运行
当您运行这个程序时,游戏将会看起来像图 21-1。
图 21-1:《躲避者》游戏的屏幕截图
《躲避者》的源代码
在一个新文件中输入以下代码,并将其保存为 dodger.py。您可以从 www.nostarch.com/inventwithpython/
下载代码、图像和声音文件。将图像和声音文件放在与 dodger.py 相同的文件夹中。
如果在输入此代码后出现错误,请使用在线 diff 工具将你输入的代码与本书代码进行比较,网址为 www.nostarch.com/inventwithpython#diff
。
dodger.py
import pygame, random, sys
from pygame.locals import *
WINDOWWIDTH = 600
WINDOWHEIGHT = 600
TEXTCOLOR = (0, 0, 0)
BACKGROUNDCOLOR = (255, 255, 255)
FPS = 60
BADDIEMINSIZE = 10
BADDIEMAXSIZE = 40
BADDIEMINSPEED = 1
BADDIEMAXSPEED = 8
ADDNEWBADDIERATE = 6
PLAYERMOVERATE = 5
def terminate():
pygame.quit()
sys.exit()
def waitForPlayerToPressKey():
while True:
for event in pygame.event.get():
if event.type == QUIT:
terminate()
if event.type == KEYDOWN:
if event.key == K_ESCAPE: # Pressing ESC quits.
terminate()
return
def playerHasHitBaddie(playerRect, baddies):
for b in baddies:
if playerRect.colliderect(b['rect']):
return True
return False
def drawText(text, font, surface, x, y):
textobj = font.render(text, 1, TEXTCOLOR)
textrect = textobj.get_rect()
textrect.topleft = (x, y)
surface.blit(textobj, textrect)
# Set up pygame, the window, and the mouse cursor.
pygame.init()
mainClock = pygame.time.Clock()
windowSurface = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT))
pygame.display.set_caption('Dodger')
pygame.mouse.set_visible(False)
# Set up the fonts.
font = pygame.font.SysFont(None, 48)
# Set up sounds.
gameOverSound = pygame.mixer.Sound('gameover.wav')
pygame.mixer.music.load('background.mid')
# Set up images.
playerImage = pygame.image.load('player.png')
playerRect = playerImage.get_rect()
baddieImage = pygame.image.load('baddie.png')
# Show the "Start" screen.
windowSurface.fill(BACKGROUNDCOLOR)
drawText('Dodger', font, windowSurface, (WINDOWWIDTH / 3),
(WINDOWHEIGHT / 3))
drawText('Press a key to start.', font, windowSurface,
(WINDOWWIDTH / 3) - 30, (WINDOWHEIGHT / 3) + 50)
pygame.display.update()
waitForPlayerToPressKey()
topScore = 0
while True:
# Set up the start of the game.
baddies = []
score = 0
playerRect.topleft = (WINDOWWIDTH / 2, WINDOWHEIGHT - 50)
moveLeft = moveRight = moveUp = moveDown = False
reverseCheat = slowCheat = False
baddieAddCounter = 0
pygame.mixer.music.play(-1, 0.0)
while True: # The game loop runs while the game part is playing.
score += 1 # Increase score.
for event in pygame.event.get():
if event.type == QUIT:
terminate()
if event.type == KEYDOWN:
if event.key == K_z:
reverseCheat = True
if event.key == K_x:
slowCheat = True
if event.key == K_LEFT or event.key == K_a:
moveRight = False
moveLeft = True
if event.key == K_RIGHT or event.key == K_d:
moveLeft = False
moveRight = True
if event.key == K_UP or event.key == K_w:
moveDown = False
moveUp = True
if event.key == K_DOWN or event.key == K_s:
moveUp = False
moveDown = True
if event.type == KEYUP:
if event.key == K_z:
reverseCheat = False
score = 0
if event.key == K_x:
slowCheat = False
score = 0
if event.key == K_ESCAPE:
terminate()
if event.key == K_LEFT or event.key == K_a:
moveLeft = False
if event.key == K_RIGHT or event.key == K_d:
moveRight = False
if event.key == K_UP or event.key == K_w:
moveUp = False
if event.key == K_DOWN or event.key == K_s:
moveDown = False
if event.type == MOUSEMOTION:
# If the mouse moves, move the player to the cursor.
playerRect.centerx = event.pos[0]
playerRect.centery = event.pos[1]
# Add new baddies at the top of the screen, if needed.
if not reverseCheat and not slowCheat:
baddieAddCounter += 1
if baddieAddCounter == ADDNEWBADDIERATE:
baddieAddCounter = 0
baddieSize = random.randint(BADDIEMINSIZE, BADDIEMAXSIZE)
newBaddie = {'rect': pygame.Rect(random.randint(0,
WINDOWWIDTH - baddieSize), 0 - baddieSize,
baddieSize, baddieSize),
'speed': random.randint(BADDIEMINSPEED,
BADDIEMAXSPEED),
'surface':pygame.transform.scale(baddieImage,
(baddieSize, baddieSize)),
}
baddies.append(newBaddie)
# Move the player around.
if moveLeft and playerRect.left > 0:
playerRect.move_ip(-1 * PLAYERMOVERATE, 0)
if moveRight and playerRect.right < WINDOWWIDTH:
playerRect.move_ip(PLAYERMOVERATE, 0)
if moveUp and playerRect.top > 0:
playerRect.move_ip(0, -1 * PLAYERMOVERATE)
if moveDown and playerRect.bottom < WINDOWHEIGHT:
playerRect.move_ip(0, PLAYERMOVERATE)
# Move the baddies down.
for b in baddies:
if not reverseCheat and not slowCheat:
b['rect'].move_ip(0, b['speed'])
elif reverseCheat:
b['rect'].move_ip(0, -5)
elif slowCheat:
b['rect'].move_ip(0, 1)
# Delete baddies that have fallen past the bottom.
for b in baddies[:]:
if b['rect'].top > WINDOWHEIGHT:
baddies.remove(b)
# Draw the game world on the window.
windowSurface.fill(BACKGROUNDCOLOR)
# Draw the score and top score.
drawText('Score: %s' % (score), font, windowSurface, 10, 0)
drawText('Top Score: %s' % (topScore), font, windowSurface,
10, 40)
# Draw the player's rectangle.
windowSurface.blit(playerImage, playerRect)
# Draw each baddie.
for b in baddies:
windowSurface.blit(b['surface'], b['rect'])
pygame.display.update()
# Check if any of the baddies have hit the player.
if playerHasHitBaddie(playerRect, baddies):
if score > topScore:
topScore = score # Set new top score.
break
mainClock.tick(FPS)
# Stop the game and show the "Game Over" screen.
pygame.mixer.music.stop()
gameOverSound.play()
drawText('GAME OVER', font, windowSurface, (WINDOWWIDTH / 3),
(WINDOWHEIGHT / 3))
drawText('Press a key to play again.', font, windowSurface,
(WINDOWWIDTH / 3) - 80, (WINDOWHEIGHT / 3) + 50)
pygame.display.update()
waitForPlayerToPressKey()
gameOverSound.stop()
导入模块
Dodger 游戏导入了与之前的 pygame
程序相同的模块:pygame
、random
、sys
和 pygame.locals
。
import pygame, random, sys
from pygame.locals import *
pygame.locals
模块包含了 pygame
使用的几个常量变量,比如事件类型(QUIT
,KEYDOWN
等)和键盘按键(K_ESCAPE
,K_LEFT
等)。通过使用 from pygame.locals import *
语法,你可以在源代码中直接使用 QUIT
而不是 pygame.locals.QUIT
。
设置常量变量
第 4 到 7 行设置了窗口尺寸、文本颜色和背景颜色的常量:
WINDOWWIDTH = 600
WINDOWHEIGHT = 600
TEXTCOLOR = (0, 0, 0)
BACKGROUNDCOLOR = (255, 255, 255)
我们使用常量变量是因为它们比我们手动输入的值更具描述性。例如,windowSurface.fill(BACKGROUNDCOLOR)
这一行比 windowSurface.fill((255, 255, 255))
更容易理解。
你可以通过改变常量变量来轻松改变游戏。通过改变第 4 行的 WINDOWWIDTH
,你会自动改变代码中所有使用 WINDOWWIDTH
的地方。如果你使用的是值 600
,那么你需要在代码中每次出现 600
的地方都进行修改。改变常量的值一次会更容易。
在第 8 行,你设置了 FPS
的常量,即每秒帧数,你希望游戏运行的帧数。
FPS = 60
帧 是通过游戏循环的单次迭代绘制的屏幕。你将 FPS
传递给第 186 行的 mainClock.tick()
方法,以便函数知道暂停程序的时间。这里 FPS
设置为 60
,但你可以将 FPS
更改为更高的值以使游戏运行更快,或者更改为更低的值以减慢游戏速度。
第 9 到 13 行设置了更多的坏蛋下落的常量变量:
BADDIEMINSIZE = 10
BADDIEMAXSIZE = 40
BADDIEMINSPEED = 1
BADDIEMAXSPEED = 8
ADDNEWBADDIERATE = 6
坏蛋的宽度和高度将在 BADDIEMINSIZE
和 BADDIEMAXSIZE
之间。坏蛋在屏幕上下落的速度将在 BADDIEMINSPEED
和 BADDIEMAXSPEED
之间,每次游戏循环迭代的像素数。并且每经过 ADDNEWBADDIERATE
次游戏循环迭代,一个新的坏蛋将被添加到窗口顶部。
最后,PLAYERMOVERATE
存储了玩家角色在游戏循环的每次迭代中在窗口中移动的像素数(如果角色正在移动):
PLAYERMOVERATE = 5
通过增加这个数字,你可以增加角色移动的速度。
定义函数
你将为这个游戏创建几个函数。terminate()
和 waitForPlayerToPressKey()
函数将分别结束和暂停游戏,playerHasHitBaddie()
函数将跟踪玩家与坏蛋的碰撞,drawText()
函数将在屏幕上绘制得分和其他文本。
结束和暂停游戏
pygame
模块要求你同时调用 pygame.quit()
和 sys.exit()
来结束游戏。第 16 到 18 行将它们都放入一个名为 terminate()
的函数中。
def terminate():
pygame.quit()
sys.exit()
现在你只需要调用 terminate()
而不是同时调用 pygame.quit()
和 sys.exit()
。
有时你会希望暂停程序,直到玩家按下一个键,比如在游戏开始时出现 Dodger 标题文本或者在结束时显示 Game Over 时。第 20 到 24 行创建了一个名为 waitForPlayerToPressKey()
的新函数:
def waitForPlayerToPressKey():
while True:
for event in pygame.event.get():
if event.type == QUIT:
terminate()
在这个函数内部,有一个无限循环,只有在接收到 KEYDOWN
或 QUIT
事件时才会中断。在循环开始时,pygame.event.get()
返回一个 Event
对象列表供检查。
如果玩家在程序等待玩家按键时关闭了窗口,pygame
将生成一个 QUIT
事件,在第 23 行通过 event.type
进行检查。如果玩家退出,Python 将在第 24 行调用 terminate()
函数。
如果游戏收到KEYDOWN
事件,它应首先检查是否按下了 ESC 键:
if event.type == KEYDOWN:
if event.key == K_ESCAPE: # Pressing ESC quits.
terminate()
return
如果玩家按下 ESC,则程序应该终止。如果不是这种情况,那么执行将跳过第 27 行的if
块,直接到return
语句,退出waitForPlayerToPressKey()
函数。
如果没有生成QUIT
或KEYDOWN
事件,代码将继续循环。由于循环什么也不做,这将使游戏看起来像已经冻结,直到玩家按下键。
跟踪坏人碰撞
如果玩家的角色与坏人之一发生碰撞,则playerHasHitBaddie()
函数将返回True
:
def playerHasHitBaddie(playerRect, baddies):
for b in baddies:
if playerRect.colliderect(b['rect']):
return True
return False
baddies
参数是坏人字典数据结构的列表。这些字典中的每一个都有一个’rect’键,该键的值是表示坏人大小和位置的Rect
对象。
playerRect
也是一个Rect
对象。Rect
对象有一个名为colliderect()
的方法,如果Rect
对象与传递给它的Rect
对象发生碰撞,则返回True
。否则,colliderect()
返回False
。
第 31 行的for
循环遍历baddies
列表中的每个坏人字典。如果任何这些坏人与玩家的角色发生碰撞,则playerHasHitBaddie()
返回True
。如果代码成功遍历baddies
列表中的所有坏人而没有检测到碰撞,则playerHasHitBaddie()
返回False
。
向窗口绘制文本
在窗口上绘制文本涉及一些步骤,我们通过drawText()
来完成。这样,当我们想要在屏幕上显示玩家得分或游戏结束文本时,只需要调用一个函数。
def drawText(text, font, surface, x, y):
textobj = font.render(text, 1, TEXTCOLOR)
textrect = textobj.get_rect()
textrect.topleft = (x, y)
surface.blit(textobj, textrect)
首先,第 37 行的render()
方法调用创建了一个Surface
对象,以特定字体呈现文本。
接下来,您需要知道Surface
对象的大小和位置。您可以使用get_rect() Surface
方法获取包含此信息的Rect
对象。
从第 38 行的get_rect()
返回的Rect
对象中复制了Surface
对象的宽度和高度信息。第 39 行通过为其topleft
属性设置一个新的元组值来更改Rect
对象的位置。
最后,第 40 行将渲染文本的Surface
对象绘制到传递给drawText()
函数的Surface
对象上。在pygame
中显示文本比简单调用print()
函数需要更多步骤。但是,如果将此代码放入名为drawText()
的单个函数中,那么您只需要调用此函数即可在屏幕上显示文本。
初始化 pygame 和设置窗口
现在常量变量和函数已经完成,我们将开始调用设置窗口和时钟的pygame
函数:
# Set up pygame, the window, and the mouse cursor.
pygame.init()
mainClock = pygame.time.Clock()
第 43 行通过调用pygame.init()
函数设置了pygame
。第 44 行创建了一个pygame.time.Clock()
对象,并将其存储在mainClock
变量中。这个对象将帮助我们防止程序运行得太快。
第 45 行创建了一个用于窗口显示的新Surface
对象:
windowSurface = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT))
请注意,pygame.display.set_mode()
只传递了一个参数:一个元组。pygame.display.set_mode()
的参数不是两个整数,而是一个包含两个整数的元组。您可以通过传递一个包含WINDOWWIDTH
和WINDOWHEIGHT
常量变量的元组来指定此Surface
对象(和窗口)的宽度和高度。
pygame.display.set_mode()
函数有第二个可选参数。您可以传递pygame.FULLSCREEN
常量以使窗口填满整个屏幕。看一下对第 45 行的修改:
windowSurface = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT),
pygame.FULLSCREEN)
WINDOWWIDTH
和WINDOWHEIGHT
参数仍然用于窗口的宽度和高度,但图像将被拉伸以适应屏幕。尝试在全屏模式和非全屏模式下运行程序。
第 46 行将窗口的标题设置为字符串’Dodger’:
pygame.display.set_caption('Dodger')
此标题将显示在窗口顶部的标题栏中。
在 Dodger 中,鼠标光标不应该可见。您希望鼠标能够移动玩家角色在屏幕上移动,但鼠标光标会妨碍角色图像。我们可以用一行代码使鼠标不可见:
pygame.mouse.set_visible(False)
调用pygame.mouse.set_visible(False)
告诉pygame
使光标不可见。
设置字体、声音和图像对象
由于我们在这个程序中在屏幕上显示文本,我们需要为文本提供一个Font
对象给pygame
模块使用。第 50 行通过调用pygame.font.SysFont()
创建了一个Font
对象:
# Set up the fonts.
font = pygame.font.SysFont(None, 48)
传递None
使用默认字体。传递48
给字体一个 48 点的大小。
接下来,我们将创建Sound
对象并设置背景音乐:
# Set up sounds.
gameOverSound = pygame.mixer.Sound('gameover.wav')
pygame.mixer.music.load('background.mid')
pygame.mixer.Sound()
构造函数创建一个新的Sound
对象,并将对此对象的引用存储在gameOverSound
变量中。在您自己的游戏中,您可以创建任意数量的Sound
对象,每个对象都有不同的声音文件。
pygame.mixer.music.load()
函数加载一个声音文件用于背景音乐。这个函数不返回任何对象,一次只能加载一个背景音乐文件。背景音乐将在游戏期间持续播放,但Sound
对象只会在玩家撞到坏人而输掉游戏时播放。
您可以为这个游戏使用任何 WAV 或 MIDI 文件。一些声音文件可以从本书的网站www.nostarch.com/inventwithpython/
下载。您也可以为这个游戏使用自己的声音文件,只要您将文件命名为gameover.wav和background.mid,或者更改第 53 和 54 行使用的字符串以匹配您想要的文件名。
接下来,您将加载用于玩家角色和坏人的图像文件:
# Set up images.
playerImage = pygame.image.load('player.png')
playerRect = playerImage.get_rect()
baddieImage = pygame.image.load('baddie.png')
角色的图像存储在player.png中,坏人的图像存储在baddie.png中。所有坏人看起来都一样,所以你只需要一个图像文件。您可以从本书的网站www.nostarch.com/inventwithpython/
下载这些图像。
显示开始画面
游戏刚开始时,Python 应该在屏幕上显示 Dodger 标题。您还希望告诉玩家他们可以通过按任意键开始游戏。这个画面出现是为了让玩家在运行程序后有时间准备开始玩。
在 63 和 64 行,我们编写代码调用drawText()
函数:
# Show the "Start" screen.
windowSurface.fill(BACKGROUNDCOLOR)
drawText('Dodger', font, windowSurface, (WINDOWWIDTH / 3),
(WINDOWHEIGHT / 3))
drawText('Press a key to start.', font, windowSurface,
(WINDOWWIDTH / 3) - 30, (WINDOWHEIGHT / 3) + 50)
pygame.display.update()
waitForPlayerToPressKey()
我们将向此函数传递五个参数:
-
您希望出现的文本字符串
-
您希望字符串出现的字体
-
文本将被渲染到的
Surface
对象 -
在
Surface
对象上的 x 坐标,用于绘制文本 -
在
Surface
对象上的 y 坐标,用于绘制文本
这可能看起来是一个很多参数的函数调用,但请记住,每次调用此函数调用将替换五行代码。这缩短了程序,并使查找错误变得更容易,因为要检查的代码更少。
waitForPlayerToPressKey()
函数通过循环暂停游戏,直到生成KEYDOWN
事件。然后执行中断循环,程序继续运行。
开始游戏
现在所有函数都已定义,我们可以开始编写主游戏代码。第 68 行及以后将调用我们之前定义的函数。程序首次运行时,topScore
变量的值为0
。每当玩家输掉游戏并且得分大于当前最高分时,最高分将被替换为这个更大的分数。
topScore = 0
while True:
从第 69 行开始的无限循环在技术上不是游戏循环。游戏循环处理游戏运行时的事件和绘制窗口。相反,这个while
循环在每次玩家开始新游戏时迭代。当玩家输掉游戏并且游戏重置时,程序的执行会循环回到第 69 行。
一开始,您还希望将baddies
设置为空列表:
# Set up the start of the game.
baddies = []
score = 0
baddies
变量是一个包含以下键的字典对象列表:
'rect’描述了坏人的位置和大小的Rect
对象。
'speed’坏人下落的速度。这个整数表示每次游戏循环迭代的像素。
'surface’拥有缩放的坏人图像绘制在上面的Surface
对象。这是绘制到pygame.display.set_mode()
返回的Surface
对象的Surface
。
第 72 行将玩家的分数重置为0
。
玩家的起始位置在屏幕中央,距离底部 50 像素,由第 73 行设置:
playerRect.topleft = (WINDOWWIDTH / 2, WINDOWHEIGHT - 50)
第 73 行元组的第一个项目是左边缘的 x 坐标,第二个项目是顶边缘的 y 坐标。
接下来,我们设置玩家移动和作弊的变量:
moveLeft = moveRight = moveUp = moveDown = False
reverseCheat = slowCheat = False
baddieAddCounter = 0
移动变量moveLeft
、moveRight
、moveUp
和moveDown
都设置为False
。reverseCheat
和slowCheat
变量也设置为False
。只有当玩家按住 Z 和 X 键启用这些作弊时,它们才会被设置为True
。
baddieAddCounter
变量是一个计数器,告诉程序何时在屏幕顶部添加一个新的坏人。baddieAddCounter
的值每次游戏循环迭代时增加 1。(这类似于“添加新食物方块”中的代码在第 295 页。)
当baddieAddCounter
等于ADDNEWBADDIERATE
时,baddieAddCounter
重置为0
,并在屏幕顶部添加一个新的坏人。(这个检查稍后在第 130 行进行。)
背景音乐在第 77 行开始播放,调用了pygame.mixer.music.play()
函数:
pygame.mixer.music.play(-1, 0.0)
因为第一个参数是-1
,pygame
会无限重复播放音乐。第二个参数是一个浮点数,表示音乐开始播放的秒数。传递0.0
意味着音乐从头开始播放。
游戏循环
游戏循环的代码不断更新游戏世界的状态,改变玩家和坏人的位置,处理由pygame
生成的事件,并在屏幕上绘制游戏世界。所有这些都会在几十次每秒发生,使游戏实时运行。
第 79 行是主游戏循环的开始:
while True: # The game loop runs while the game part is playing.
score += 1 # Increase score.
第 80 行在游戏循环的每次迭代中增加玩家的分数。玩家能够在不失去的情况下走得越久,他们的分数就越高。循环只有在玩家输掉游戏或退出程序时才会退出。
处理键盘事件
程序将处理四种类型的事件:QUIT
、KEYDOWN
、KEYUP
和MOUSEMOTION
。
第 82 行是事件处理代码的开始:
for event in pygame.event.get():
if event.type == QUIT:
terminate()
它调用pygame.event.get()
,返回一个Event
对象列表。每个Event
对象表示自上次调用pygame.event.get()
以来发生的事件。代码检查Event
对象的type
属性,看看它是什么类型的事件,然后相应地处理它。
如果Event
对象的type
属性等于QUIT
,那么用户已经关闭了程序。QUIT
常量变量是从pygame.locals
模块导入的。
如果事件的类型是KEYDOWN
,玩家已经按下了一个键:
if event.type == KEYDOWN:
if event.key == K_z:
reverseCheat = True
if event.key == K_x:
slowCheat = True
第 87 行检查事件是否描述了按下Z
键,条件为event.key == K_z
。如果条件为True
,Python 将reverseCheat
变量设置为True
以激活反向作弊。类似地,第 89 行检查是否按下X
键以激活减速作弊。
第 91 到 102 行检查事件是否由玩家按下箭头或 WASD 键生成。这段代码类似于前几章的与键盘相关的代码。
如果事件的类型是KEYUP
,玩家已经释放了一个键:
if event.type == KEYUP:
if event.key == K_z:
reverseCheat = False
score = 0
if event.key == K_x:
slowCheat = False
score = 0
第 105 行检查玩家是否释放了 Z 键,这将停用反向作弊。在这种情况下,第 106 行将reverseCheat
设置为False
,第 107 行将分数重置为0
。分数重置是为了阻止玩家使用作弊。
第 108 行到第 110 行对 X 键和慢速作弊做了同样的事情。释放 X 键时,slowCheat
设置为False
,玩家的分数重置为0
。
在游戏进行期间,玩家可以随时按 ESC 键退出:
if event.key == K_ESCAPE:
terminate()
第 111 行通过检查event.key == K_ESCAPE
来确定释放的键是否是 ESC。如果是,第 112 行调用terminate()
函数退出程序。
第 114 行到第 121 行检查玩家是否停止按住箭头或 WASD 键之一。在这种情况下,代码将相应的移动变量设置为False
。这类似于第 19 章和第 20 章程序中的移动代码。
处理鼠标移动
现在你已经处理了键盘事件,让我们处理可能生成的任何鼠标事件。《躲避球》游戏如果玩家点击了鼠标按钮,不会有任何反应,但是当玩家移动鼠标时会有反应。这给玩家在游戏中控制角色的两种方式:键盘或鼠标。
MOUSEMOTION
事件在鼠标移动时生成:
if event.type == MOUSEMOTION:
# If the mouse moves, move the player to the cursor.
playerRect.centerx = event.pos[0]
playerRect.centery = event.pos[1]
type
设置为MOUSEMOTION
的Event
对象还有一个名为pos
的属性,用于存储鼠标事件的位置。pos
属性存储了鼠标光标在窗口中移动的 x 和 y 坐标的元组。如果事件的类型是MOUSEMOTION
,玩家的角色将移动到鼠标光标的位置。
第 125 行和第 126 行将玩家角色的中心 x 和 y 坐标设置为鼠标光标的 x 和 y 坐标。
添加新的坏蛋
在游戏循环的每次迭代中,代码将baddieAddCounter
变量增加一:
# Add new baddies at the top of the screen, if needed.
if not reverseCheat and not slowCheat:
baddieAddCounter += 1
只有在作弊未启用时才会发生。请记住,只要按住 Z 和 X 键,reverseCheat
和slowCheat
就会设置为True
。在按住 Z 和 X 键时,baddieAddCounter
不会增加。因此,新的坏蛋不会出现在屏幕顶部。
当baddieAddCounter
达到ADDNEWBADDIERATE
中的值时,是时候在屏幕顶部添加一个新的坏蛋了。首先,将baddieAddCounter
重置为0
:
if baddieAddCounter == ADDNEWBADDIERATE:
baddieAddCounter = 0
baddieSize = random.randint(BADDIEMINSIZE, BADDIEMAXSIZE)
newBaddie = {'rect': pygame.Rect(random.randint(0,
WINDOWWIDTH - baddieSize), 0 - baddieSize,
baddieSize, baddieSize),
'speed': random.randint(BADDIEMINSPEED,
BADDIEMAXSPEED),
'surface':pygame.transform.scale(baddieImage,
(baddieSize, baddieSize)),
}
第 132 行生成了坏蛋的像素大小。大小将是BADDIEMINSIZE
和BADDIEMAXSIZE
之间的随机整数,这些常量分别在第 9 行和第 10 行设置为10
和40
。
第 133 行是创建新坏蛋数据结构的地方。请记住,baddies
的数据结构只是一个带有键'rect'
、'speed'
和'surface'
的字典。'rect'
键保存对存储坏蛋位置和大小的Rect
对象的引用。对pygame.Rect()
构造函数的调用有四个参数:区域顶部边缘的 x 坐标、区域左边缘的 y 坐标、像素宽度和像素高度。
坏蛋需要出现在窗口顶部的随机位置,因此将random.randint(0, WINDOWWIDTH - baddieSize)
传递给坏蛋左边缘的 x 坐标。之所以传递WINDOWWIDTH - baddieSize
而不是WINDOWWIDTH
,是因为如果坏蛋的左边缘太靠右,那么坏蛋的一部分将超出窗口边缘,不会在屏幕上可见。
坏蛋的底边应该位于窗口顶边的上方。窗口顶边的 y 坐标是0
。为了将坏蛋的底边放在那里,将顶边设置为0 - baddieSize
。
坏蛋的宽度和高度应该相同(图像是一个正方形),因此将baddieSize
传递给第三个和第四个参数。
坏人在屏幕上移动的速度设置在'speed'
键中。将其设置为BADDIEMINSPEED
和BADDIEMAXSPEED
之间的随机整数。
然后,在第 138 行,将新创建的坏人数据结构添加到坏人数据结构列表中:
baddies.append(newBaddie)
程序使用这个列表来检查玩家是否与任何坏人发生了碰撞,并确定在窗口上绘制坏人的位置。
移动玩家角色和坏人
四个移动变量moveLeft
、moveRight
、moveUp
和moveDown
在pygame
生成KEYDOWN
和KEYUP
事件时分别设置为True
和`False。
如果玩家的角色向左移动,并且玩家角色的左边缘大于0
(即窗口的左边缘),那么playerRect
应该向左移动:
# Move the player around.
if moveLeft and playerRect.left > 0:
playerRect.move_ip(-1 * PLAYERMOVERATE, 0)
move_ip()
方法将Rect
对象的位置水平或垂直移动一定数量的像素。move_ip()
的第一个参数是将Rect
对象向右移动的像素数(要向左移动,传递一个负整数)。第二个参数是将Rect
对象向下移动的像素数(要向上移动,传递一个负整数)。例如,playerRect.move_ip(10, 20)
将使Rect
对象向右移动 10 个像素,向下移动 20 个像素,playerRect.move_ip(-5, -15)
将使Rect
对象向左移动 5 个像素,向上移动 15 个像素。
move_ip()
末尾的ip代表“原地”。这是因为该方法改变了Rect
对象本身,而不是返回具有更改的新Rect
对象。还有一个move()
方法,它不会改变Rect
对象,而是在新位置创建并返回一个新的Rect
对象。
你总是会移动playerRect
对象的像素数为PLAYERMOVERATE
。要得到一个整数的负形式,将其乘以-1
。在第 142 行,由于PLAYERMOVERATE
中存储了5
,表达式-1 * PLAYERMOVERATE
的值为-5
。因此,调用playerRect.move_ip(-1 * PLAYERMOVERATE, 0)
将使playerRect
的位置向左移动 5 个像素。
第 143 到 148 行对其他三个方向进行了相同的操作:右、上和下。
if moveRight and playerRect.right < WINDOWWIDTH:
playerRect.move_ip(PLAYERMOVERATE, 0)
if moveUp and playerRect.top > 0:
playerRect.move_ip(0, -1 * PLAYERMOVERATE)
if moveDown and playerRect.bottom < WINDOWHEIGHT:
playerRect.move_ip(0, PLAYERMOVERATE)
在第 143 到 148 行的三个if
语句中,检查其移动变量是否设置为True
,并且玩家的Rect
对象的边缘是否在窗口内。然后调用move_ip()
来移动Rect
对象。
现在,代码循环遍历baddies
列表中的每个坏人数据结构,使它们向下移动一点:
# Move the baddies down.
for b in baddies:
if not reverseCheat and not slowCheat:
b['rect'].move_ip(0, b['speed'])
如果没有激活任何作弊码,那么坏人的位置向下移动与其速度(存储在'speed'
键中)相等的像素数。
实现作弊码
如果反向作弊被激活,那么坏人应该向上移动 5 个像素:
elif reverseCheat:
b['rect'].move_ip(0, -5)
将move_ip()
的第二个参数传递为-5
将使Rect
对象向上移动 5 个像素。
如果慢速作弊被激活,那么坏人仍然应该向下移动,但速度为每次游戏循环迭代 1 个像素:
elif slowCheat:
b['rect'].move_ip(0, 1)
当慢速作弊被激活时,坏人的正常速度(同样存储在坏人数据结构的'speed'
键中)将被忽略。
移除坏人
任何掉到窗口底部以下的坏人都应该从baddies
列表中移除。记住,不应该在迭代列表时添加或移除列表项。不要使用for
循环迭代baddies
列表,而是使用baddies
列表的副本进行迭代。要创建这个副本,使用空切片操作符[:]
:
# Delete baddies that have fallen past the bottom.
for b in baddies[:]:
第 160 行的for
循环使用变量b
来遍历baddies[:]
中的当前项。如果坏人在窗口的底部以下,我们应该将其移除,这在第 162 行中完成:
if b['rect'].top > WINDOWHEIGHT:
baddies.remove(b)
b
字典是baddies[:]
列表中的当前坏蛋数据结构。列表中的每个坏蛋数据结构都是一个带有'rect'
键的字典,该键存储一个Rect
对象。因此,b['rect']
是坏蛋的Rect
对象。最后,top
属性是矩形区域顶部边缘的 y 坐标。请记住,y 坐标向下增加。因此,b['rect'].top > WINDOWHEIGHT
将检查坏蛋的顶部边缘是否在窗口底部以下。如果这个条件为True
,那么第 162 行将从baddies
列表中删除坏蛋数据结构。
绘制窗口
在更新所有数据结构之后,应使用pygame
的图像函数绘制游戏世界。因为游戏循环每秒执行多次,当坏蛋和玩家在新位置绘制时,它们看起来就像是平稳移动的。
在绘制任何其他内容之前,第 165 行填充整个屏幕以擦除先前绘制的任何内容:
# Draw the game world on the window.
windowSurface.fill(BACKGROUNDCOLOR)
请记住,windowSurface
中的Surface
对象很特殊,因为它是由pygame.display.set_mode()
返回的。因此,在该Surface
对象上绘制的任何内容都将在调用pygame.display.update()
后出现在屏幕上。
绘制玩家得分
第 168 和 169 行在窗口的左上角渲染了当前得分和最高得分的文本。
# Draw the score and top score.
drawText('Score: %s' % (score), font, windowSurface, 10, 0)
drawText('Top Score: %s' % (topScore), font, windowSurface,
10, 40)
'Score: %s' % (score)
表达式使用字符串插值将score
变量的值插入字符串中。这个字符串、存储在font
变量中的Font
对象、用于绘制文本的Surface
对象,以及文本应放置的 x 和 y 坐标都被传递给drawText()
方法,该方法将处理对render()
和blit()
方法的调用。
对于最高得分,做同样的事情。将40
作为 y 坐标传递,而不是0
,这样最高得分的文本就会出现在当前得分的文本下方。
绘制玩家角色和坏蛋
关于玩家的信息保存在两个不同的变量中。playerImage
是一个包含玩家角色图像的所有彩色像素的Surface
对象。playerRect
是一个存储玩家角色大小和位置的Rect
对象。
blit()
方法在windowSurface
上绘制玩家角色的图像(在playerImage
中)在playerRect
的位置:
# Draw the player's rectangle.
windowSurface.blit(playerImage, playerRect)
第 175 行的for
循环在windowSurface
对象上绘制每个坏蛋:
# Draw each baddie.
for b in baddies:
windowSurface.blit(b['surface'], b['rect'])
baddies
列表中的每个项目都是一个字典。字典的'surface'
和'rect'
键包含了带有坏蛋图像的Surface
对象和带有位置和大小信息的Rect
对象。
现在,所有内容都已经绘制到windowSurface
上,我们需要更新屏幕,以便玩家可以看到其中的内容:
pygame.display.update()
通过调用update()
将这个Surface
对象绘制到屏幕上。
检查碰撞
第 181 行检查玩家是否与任何坏蛋发生碰撞,调用playerHasHitBaddie()
。如果玩家的角色与baddies
列表中的任何一个坏蛋发生碰撞,则此函数将返回True
。否则,该函数返回False
。
# Check if any of the baddies have hit the player.
if playerHasHitBaddie(playerRect, baddies):
if score > topScore:
topScore = score # Set new top score.
break
如果玩家的角色撞到了坏蛋,并且当前得分高于最高得分,那么第 182 和 183 行将更新最高得分。程序的执行会在第 184 行跳出游戏循环,并移动到第 189 行,结束游戏。
为了防止计算机尽可能快地运行游戏循环(这对玩家来说太快了),调用mainClock.tick()
来暂停游戏很短的时间:
mainClock.tick(FPS)
这个暂停时间将足够长,以确保每秒大约进行40
次(存储在FPS
变量内部的值)游戏循环迭代。
游戏结束画面
当玩家失败时,游戏停止播放背景音乐,并播放“游戏结束”音效:
# Stop the game and show the "Game Over" screen.
pygame.mixer.music.stop()
gameOverSound.play()
第 189 行调用pygame.mixer.music
模块中的stop()
函数来停止背景音乐。第 190 行调用gameOverSound
中存储的Sound
对象的play()
方法。
然后,第 192 行和第 193 行调用drawText()
函数将“游戏结束”文本绘制到windowSurface
对象上:
drawText('GAME OVER', font, windowSurface, (WINDOWWIDTH / 3),
(WINDOWHEIGHT / 3))
drawText('Press a key to play again.', font, windowSurface,
(WINDOWWIDTH / 3) - 80, (WINDOWHEIGHT / 3) + 50)
pygame.display.update()
waitForPlayerToPressKey()
第 194 行调用update()
来将这个Surface
对象绘制到屏幕上。在显示这个文本后,游戏会停止,直到玩家按下键,调用waitForPlayerToPressKey()
函数。
玩家按下键后,程序执行从第 195 行的waitForPlayerToPressKey()
调用返回。根据玩家按键的时间长短,可能会播放“游戏结束”音效。为了在新游戏开始之前停止这个音效,第 197 行调用gameOverSound.stop()
:
gameOverSound.stop()
我们的图形游戏就到这里了!
修改躲避者游戏
你可能会发现游戏太容易或太难。幸运的是,游戏很容易修改,因为我们花时间使用常量变量而不是直接输入值。现在,我们只需要修改常量变量中设置的值就可以改变游戏。
例如,如果你想让游戏总体运行速度变慢,可以将第 8 行的FPS
变量更改为较小的值,比如20
。这将使坏人和玩家角色移动得更慢,因为游戏循环每秒只执行20
次,而不是40
次。
如果你只想减慢坏人的速度而不是玩家的速度,那么将BADDIEMAXSPEED
更改为较小的值,比如4
。这将使所有坏人在游戏循环中的每次迭代之间移动 1(BADDIEMINSPEED
中的值)到 4 个像素,而不是 1 到 8 个像素。
如果你想让游戏有更少但更大的坏人,而不是许多较小的坏人,那么将ADDNEWBADDIERATE
增加到12
,BADDIEMINSIZE
增加到40
,BADDIEMAXSIZE
增加到80
。现在,坏人每 12 次游戏循环添加一次,而不是每 6 次,所以坏人的数量将减少一半。但为了保持游戏的趣味性,坏人会更大。
保持基本游戏不变,你可以修改任何常量变量,从而显著影响游戏的玩法。不断尝试新的常量变量值,直到找到最喜欢的值组合。
总结
与我们的文本游戏不同,躲避者看起来真的像一款现代电脑游戏。它有图形和音乐,并且使用鼠标。虽然pygame
提供函数和数据类型作为构建块,但是你作为程序员将它们组合在一起,创造出有趣的互动游戏。
你可以做到这一切,因为你知道如何逐步指导计算机做事,一行一行地。通过使用计算机的语言,你可以让它为你进行数字计算和绘图。这是一项有用的技能,我希望你会继续学习更多关于 Python 编程的知识。(还有很多东西要学!)
现在开始发挥你的想象力,创造属于自己的游戏。祝你好运!
上一页:第 20 章 - 使用声音和图像