12.6 驾驶飞船
下面来让玩家能够左右移动飞船。我们将编写代码,在用户按左或右箭头键时做出响应。我们将首先专注于向右移动,再使用同样的原理来控制向左移动。通过这样做,你将学会如何控制屏幕图像的移动。
12.6.1 响应按键
每当用户按键时,都将在Pygame中注册一个事件。事件都是通过方法pygame.event.get() 获取的,因此需要在方法checkevents() 中指定要检查哪些类型的事件。每次按键都被注册为一个KEYDOWN 事件。
Pygame检测到KEYDOWN 事件时,需要检查按下的是否是触发行动的键。例如,如果玩家按下的是右箭头键,就增大飞船的rect.centerx 值,将飞船向右移动:
alien_invasion.py
def checkevents(self):
"""响应按键和鼠标事件。"""
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
❶ elif event.type == pygame.KEYDOWN:
❷ if event.key == pygame.K_RIGHT:
# 向右移动飞船。
❸ self.ship.rect.x += 1
在方法checkevents() 中,为事件循环添加一个elif 代码块,以便在Pygame检测到KEYDOWN 事件时做出响应(见❶)。我们检查按下键(event.key )是否是右箭头键(pygame.K_RIGHT )(见❷)。如果是,就将self.ship.rect.centerx 的值加1,从而将飞船向右移动(见❸)。
如果现在运行alien_invasion.py,则每按右箭头键一次,飞船都将向右移动1像素。这是一个开端,但并非控制飞船的高效方式。下面来改进控制方式,允许持续移动。
12.6.2 允许持续移动
玩家按住右箭头键不放时,我们希望飞船不断向右移动,直到玩家松开为止。我们将让游戏检测pygame.KEYUP 事件,以便知道玩家何时松开右箭头键。然后,结合使用KEYDOWN 和KEYUP 事件以及一个名为moving_right 的标志来实现持续移动。
当标志moving_right 为False 时,飞船不会移动。玩家按下右箭头键时,我们将该标志设置为True ,在玩家松开时将该标志重新设置为False 。
飞船的属性都由Ship 类控制,因此要给这个类添加一个名为moving_right 的属性和一个名为update() 的方法。方法update() 检查标志moving_right 的状态。如果该标志为True ,就调整飞船的位置。我们将在while 循环中调用这个方法,以调整飞船的位置。
下面是对Ship 类所做的修改:
ship.py
class Ship:
"""管理飞船的类"""
def init(self, ai_game):
—snip—
# 对于每艘新飞船,都将其放在屏幕底部的中央。
self.rect.midbottom = self.screen_rect.midbottom
# 移动标志。
❶ self.moving_right = False
❷ def update(self):
"""根据移动标志调整飞船的位置。"""
if self.moving_right:
self.rect.x += 1
def blitme(self):
—snip—
在方法init() 中,添加属性self.moving_right ,并将其初始值设置为False (见❶)。接下来,添加方法update() ,在前述标志为True 时向右移动飞船(见❷)。方法update() 将通过Ship 实例来调用,因此不是辅助方法。
接下来,需要修改checkevents() ,使其在玩家按下右箭头键时将moving_right 设置为True ,并在玩家松开时将moving_right 设置为False :
alien_invasion.py
def checkevents(self):
"""响应按键和鼠标事件。"""
for event in pygame.event.get():
—snip—
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_RIGHT:
❶ self.ship.moving_right = True
❷ elif event.type == pygame.KEYUP:
if event.key == pygame.K_RIGHT:
self.ship.moving_right = False
在❶处,修改游戏在玩家按下右箭头键时响应的方式:不直接调整飞船的位置,而只是将moving_right 设置为True 。在❷处,添加一个新的elif 代码块,用于响应KEYUP 事件:玩家松开右箭头键(K_RIGHT )时,将moving_right 设置为False 。
最后,需要修改run_game() 中的while 循环,以便每次执行循环时都调用飞船的方法update() :
alien_invasion.py
def run_game(self):
"""开始游戏主循环。"""
while True:
self.checkevents()
self.ship.update()
self.updatescreen()
飞船的位置将在检测到键盘事件后(但在更新屏幕前)更新。这样,玩家输入时,飞船的位置将更新,从而确保使用更新后的位置将飞船绘制到屏幕上。
如果现在运行alien_invasion.py并按住右箭头键,飞船将持续向右移动,直到松开为止。
12.6.3 左右移动
现在飞船能够持续向右移动了,添加向左移动的逻辑也很容易。我们将再次修改Ship 类和方法checkevents() 。下面显示了对Ship 类的方法init() 和update() 所做的相关修改:
ship.py
def init(self, ai_game):
—snip—
# 移动标志
self.moving_right = False
self.moving_left = False
def update(self):
"""根据移动标志调整飞船的位置。"""
if self.moving_right:
self.rect.x += 1
if self.moving_left:
self.rect.x -= 1
在方法init() 中,添加标志self.moving_left 。在方法update() 中,添加一个if 代码块而不是elif 代码块,这样如果玩家同时按下了左右箭头键,将先增加再减少飞船的rect.x 值,即飞船的位置保持不变。如果使用一个elif 代码块来处理向左移动的情况,右箭头键将始终处于优先地位。从向左移动切换到向右移动时,玩家可能同时按住左右箭头键,此时前面的做法让移动更准确。
还需对checkevents() 做两方面的调整:
alien_invasion.py
def checkevents(self):
"""响应按键和鼠标事件。"""
for event in pygame.event.get():
—snip—
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_RIGHT:
self.ship.moving_right = True
elif event.key == pygame.K_LEFT:
self.ship.moving_left = True
elif event.type == pygame.KEYUP:
if event.key == pygame.K_RIGHT:
self.ship.moving_right = False
elif event.key == pygame.K_LEFT:
self.ship.moving_left = False
如果因玩家按下K_LEFT 键而触发了KEYDOWN 事件,就将moving_left 设置为True 。如果因玩家松开K_LEFT 而触发了KEYUP 事件,就将moving_left 设置为False 。这里之所以可以使用elif 代码块,是因为每个事件都只与一个键相关联。如果玩家同时按下左右箭头键,将检测到两个不同的事件。
如果此时运行alien_invasion.py,将能够持续左右移动飞船。如果同时按下左右箭头键,飞船将纹丝不动。
下面来进一步优化飞船的移动方式:调整飞船的速度,以及限制飞船的移动距离,以免其消失在屏幕之外。
12.6.4 调整飞船的速度
当前,每次执行while 循环时,飞船最多移动1像素,但可在Settings 类中添加属性ship_speed ,用于控制飞船的速度。我们将根据这个属性决定飞船在每次循环时最多移动多远。下面演示了如何在settings.py中添加这个新属性:
settings.py
class Settings:
"""存储游戏《外星人入侵》中所有设置的类。"""
def init(self):
—snip—
# 飞船设置
self.ship_speed = 1.5
将ship_speed 的初始值设置为1.5 。现在需要移动飞船时,每次循环将移动1.5像素而不是1像素。
通过将速度设置指定为小数值,可在后面加快游戏节奏时更细致地控制飞船的速度。然而,rect 的x 等属性只能存储整数值,因此需要对Ship 类做些修改:
ship.py
class Ship:
"""管理飞船的类"""
❶ def init(self, ai_game):
"""初始化飞船并设置其初始位置。"""
self.screen = ai_game.screen
self.settings = ai_game.settings
—snip—
# 对于每艘新飞船,都将其放在屏幕底部的中央。
—snip—
# 在飞船的属性x中存储小数值。
❷ self.x = float(self.rect.x)
# 移动标志
self.moving_right = False
self.moving_left = False
def update(self):
"""根据移动标志调整飞船的位置。"""
# 更新飞船而不是rect对象的x值。
if self.moving_right:
❸ self.x += self.settings.ship_speed
if self.moving_left:
self.x -= self.settings.ship_speed
# 根据self.x更新rect对象。
❹ self.rect.x = self.x
def blitme(self):
—snip—
在❶处,给Ship 类添加属性settings ,以便能够在update() 中使用它。鉴于现在调整飞船的位置时,将增减一个单位为像素的小数值,因此需要将位置赋给一个能够存储小数值的变量。可使用小数来设置rect 的属性,但rect 将只存储这个值的整数部分。为准确存储飞船的位置,定义一个可存储小数值的新属性self.x (见❷)。使用函数float() 将self.rect.x 的值转换为小数,并将结果赋给self.x 。
现在在update() 中调整飞船的位置时,将self.x 的值增减settings.ship_speed 的值(见❸)。更新self.x 后,再根据它来更新控制飞船位置的self.rect.x (见❹)。self.rect.x 只存储self.x 的整数部分,但对显示飞船而言,这问题不大。
现在可以修改ship_speed 的值了。只要它的值大于1,飞船的移动速度就会比以前更快。这有助于让飞船的反应速度足够快,以便射杀外星人,还让我们能够随着游戏的进行加快游戏的节奏。
注意 如果你使用的是macOS,可能发现即便ship_speed 的值很大,飞船的移动速度还是很慢。要修复这种问题,可在全屏模式下运行游戏,我们稍后就将实现这种功能。
12.6.5 限制飞船的活动范围
当前,如果玩家按住箭头键的时间足够长,飞船将飞到屏幕之外,消失得无影无踪。下面来修复这种问题,让飞船到达屏幕边缘后停止移动。为此,将修改Ship 类的方法update() :
ship.py
def update(self):
"""根据移动标志调整飞船的位置。"""
# 更新飞船而不是rect对象的x值。
❶ if self.moving_right and self.rect.right < self.screen_rect.right:
self.x += self.settings.ship_speed
❷ if self.moving_left and self.rect.left > 0:
self.x -= self.settings.ship_speed
# 根据self.x更新rect对象。
self.rect.x = self.x
上述代码在修改self.x 的值之前检查飞船的位置。self.rect.right 返回飞船外接矩形右边缘的 坐标。如果这个值小于self.screen_rect.right 的值,就说明飞船未触及屏幕右边缘(见❶)。左边缘的情况与此类似:如果rect 左边缘的
坐标大于零,就说明飞船未触及屏幕左边缘(见❷)。这确保仅当飞船在屏幕内时,才调整self.x 的值。
如果此时运行alien_invasion.py,飞船将在触及屏幕左边缘或右边缘后停止移动。真是太神奇了!只在if 语句中添加一个条件测试,就让飞船在到达屏幕左右边缘时像被墙挡住了一样。
12.6.6 重构checkevents()
随着游戏的开发,方法checkevents() 将越来越长。因此将其部分代码放在两个方法中,其中一个处理KEYDOWN 事件,另一个处理KEYUP 事件:
alien_invasion.py
def checkevents(self):
"""响应鼠标和按键事件。"""
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
elif event.type == pygame.KEYDOWN:
self.checkkeydown_events(event)
elif event.type == pygame.KEYUP:
self.checkkeyup_events(event)
def checkkeydown_events(self, event):
"""响应按键。"""
if event.key == pygame.K_RIGHT:
self.ship.moving_right = True
elif event.key == pygame.K_LEFT:
self.ship.moving_left = True
def checkkeyup_events(self, event):
"""响应松开。"""
if event.key == pygame.K_RIGHT:
self.ship.moving_right = False
elif event.key == pygame.K_LEFT:
self.ship.moving_left = False
我们创建了两个新的辅助方法:checkkeydown_events() 和checkkeyup_events() 。它们都包含形参self 和event 。这两个方法的代码是从checkevents() 中复制而来的,因此将方法checkevents() 中相应的代码替换成了对这两个新方法的调用。现在,方法checkevents() 更简单,代码结构也更清晰,在其中响应玩家输入时将更容易。
12.6.7 按Q键退出
能够高效地响应按键后,我们来添加另一种退出游戏的方式。当前,每次测试新功能时,都需要单击游戏窗口顶部的X按钮来结束游戏,实在是太麻烦了。因此,我们来添加一个结束游戏的键盘快捷键—— Q键:
alien_invasion.py
def checkkeydown_events(self, event):
—snip—
elif event.key == pygame.K_LEFT:
self.ship.moving_left = True
elif event.key == pygame.K_q:
sys.exit()
在checkkeydown_events() 中,添加一个代码块,用于在玩家按Q键时结束游戏。现在测试该游戏时,你可按Q键来结束游戏,而无须使用鼠标将窗口关闭。
12.6.8 在全屏模式下运行游戏
Pygame支持全屏模式,你可能会更喜欢在这种模式下而非常规窗口中运行游戏。有些游戏在全屏模式下看起来更舒服,而在macOS系统中用全屏模式运行会提升性能。
要在全屏模式下运行该游戏,可在init() 中做如下修改:
alien_invasion.py
def init(self):
"""初始化游戏并创建游戏资源。"""
pygame.init()
self.settings = Settings()
❶ self.screen = pygame.display.set_mode((0, 0), pygame.FULLSCREEN)
❷ self.settings.screen_width = self.screen.get_rect().width
self.settings.screen_height = self.screen.get_rect().height
pygame.display.set_caption("Alien Invasion")
创建屏幕时,传入了尺寸(0, 0) 以及参数pygame.FULLSCREEN (见❶)。这让Pygame生成一个覆盖整个显示器的屏幕。由于无法预先知道屏幕的宽度和高度,要在创建屏幕后更新这些设置(见❷):使用屏幕的rect 的属性width 和height 来更新对象settings 。
如果你喜欢这款游戏在全屏模式下的外观和行为,请保留这些设置。如果你更喜欢这款游戏在独立的窗口中运行,可恢复到原来采用的方法——将屏幕尺寸设置为特定的值。
注意 在全屏模式下运行这款游戏之前,请确认能够按Q键退出,因为Pygame默认不提供在全屏模式下退出游戏的方式。