驾驶飞船

接下来,我们将赋予玩家左右移动飞船的能力。我们将编写在玩家按下向右或向左箭头键时响应的代码。我们将首先关注向右移动,然后我们将应用相同的原则来控制向左移动。当我们添加这段代码时,您将学习如何控制屏幕上图像的移动以及如何响应用户输入。

响应按键

每当玩家按下某个键时,该按键就会在 Pygame 中注册为一个事件。每个事件都由 pygame.event.get() 方法获取。我们需要在我们的 _check_events() 方法中指定我们希望游戏检查的事件类型。每次按键都被注册为 KEYDOWN 事件。

当 Pygame 检测到 KEYDOWN 事件时,我们需要检查按下的键是否是触发某个动作的键。 例如,如果玩家按下右箭头键,我们想增加船的 rect.x 值以将船向右移动:

alien_invasion.py
def _check_events(self):
    """Respond to keypresses and mouse events."""
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit()
        elif event.type == pygame.KEYDOWN: (1)
            if event.key == pygame.K_RIGHT: (2)
                # Move the ship to the right.
                self.ship.rect.x += 1 (3)

在 _check_events() 内部,我们向事件循环添加一个 elif 块,以在 Pygame 检测到 KEYDOWN 事件时做出响应❶。 我们检查按下的键 event.key 是否是右箭头键❷。 右箭头键由 pygame.K_RIGHT 表示。 如果按下右箭头键,我们将 self.ship.rect.x 的值增加 1 ❸,从而将船向右移动。

当你现在运行 alien_invasion.py 时,每次按下右箭头键时,飞船应该向右移动一个像素。这是一个开始,但这并不是控制船只的有效方法。让我们通过允许连续移动来改进这种控制。

允许持续移动

当玩家按住右箭头键时,我们希望飞船继续向右移动,直到玩家松开按键。我们会让游戏检测到 pygame.KEYUP 事件,这样我们就会知道何时释放右箭头键;然后我们将使用 KEYDOWN 和 KEYUP 事件以及一个名为 moving_right 的标志来实现连续运动。

当 moving_right 标志为 False 时,船将静止不动。当玩家按下右箭头键时,我们会将标志设置为 True,当玩家释放该键时,我们将再次将标志设置为 False。

Ship 类控制船的所有属性,因此我们将为其提供一个名为 moving_right 的属性和一个 update() 方法来检查 moving_right 标志的状态。 如果该标志设置为 True,则 update() 方法将更改船舶的位置。 我们将在每次通过 while 循环时调用此方法一次,以更新船舶的位置。

以下是对 Ship 的更改:

class Ship:
    """A class to manage the ship."""
    def __init__(self, ai_game):
        --snip--
        # Start each new ship at the bottom center of the screen.
        self.rect.midbottom = self.screen_rect.midbottom
        # Movement flag; start with a ship that's not moving.
        self.moving_right = False (1)
    def update(self): (2)
        """Update the ship's position based on the movement flag."""
        if self.moving_right:
            self.rect.x += 1
    def blitme(self):
        --snip--

我们在 __init__() 方法中添加一个 self.moving_right 属性,并将其初始设置为 False ❶。 然后我们添加 update(),如果 flag 为 True 则将船向右移动 ❷。 update() 方法将从类外部调用,因此它不被视为辅助方法。

现在我们需要修改 _check_events() 以便 moving_right 在按下右箭头键时设置为 True,在释放键时设置为 False:

alien_invasion.py
def _check_events(self):
    """Respond to keypresses and mouse events."""
    for event in pygame.event.get():
        --snip--
        elif event.type == pygame.KEYDOWN:
            if event.key == pygame.K_RIGHT:
                self.ship.moving_right = True (1)
        elif event.type == pygame.KEYUP: (2)
            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):
    """Start the main loop for the game."""
    while True:
        self._check_events()
        self.ship.update() (1)
        self._update_screen()
        self.clock.tick(60)

在我们检查键盘事件之后和更新屏幕之前,船舶的位置将被更新。 这允许船舶的位置根据玩家输入进行更新,并确保在将船舶绘制到屏幕时使用更新后的位置。

当你运行 alien_invasion.py 并按住右箭头键时,飞船应该不断向右移动,直到你释放该键。

左右移动

既然船可以连续向右移动,那么向左添加移动就很简单了。同样,我们将修改 Ship 类和 _check_events() 方法。 以下是 Ship 中 __init__()update() 的相关更改:

ship.py
def __init__(self, ai_game):
    --snip--
    # Movement flags; start with a ship that's not moving.
    self.moving_right = False
    self.moving_left = False

def update(self):
    """Update the ship's position based on movement flags."""
    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 向左移动,则右箭头键始终具有优先权。当玩家在改变方向时可能暂时按住两个键时,使用两个 if 块可以使移动更加准确。

我们必须对 _check_events() 添加两处内容:

alien_invasion.py
def _check_events(self):
    """Respond to keypresses and mouse events."""
    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 时,您应该能够连续向右和向左移动飞船。如果你同时按住两个键,船应该停止移动。

接下来,我们将进一步完善船舶的运动。 让我们调整船的速度并限制船可以移动的距离,这样它就不会消失在屏幕的两侧。

调整飞船的速度

目前,船在 while 循环中每个周期移动一个像素,但我们可以通过将 ship_speed 属性添加到 Settings 类来更好地控制船的速度。 我们将使用此属性来确定每次通过循环时将船移动多远。 这是 settings.py 中的新属性:

settings.py
class Settings:
    """A class to store all settings for Alien Invasion."""
    def __init__(self):
        --snip--
        # Ship settings
        self.ship_speed = 1.5

我们将 ship_speed 的初始值设置为1.5。 当船现在移动时,它的位置在每次通过循环时调整 1.5 像素(而不是 1 像素)。

我们使用浮点数来设置速度,以便在我们稍后提高游戏节奏时更好地控制船速。但是,x 等 rect 属性只存储整数值,因此我们需要对 Ship 进行一些修改:

ship.py
class Ship:
    """A class to manage the ship."""
    def __init__(self, ai_game):
        """Initialize the ship and set its starting position."""
        self.screen = ai_game.screen
        self.settings = ai_game.settings (1)
        --snip--
        # Start each new ship at the bottom center of the screen.
        self.rect.midbottom = self.screen_rect.midbottom
        # Store a float for the ship's exact horizontal position.
        self.x = float(self.rect.x) (2)
        # Movement flags; start with a ship that's not moving.
        self.moving_right = False
        self.moving_left = False

    def update(self):
        """Update the ship's position based on movement flags."""
        # Update the ship's x value, not the rect.
        if self.moving_right:
            self.x += self.settings.ship_speed (3)
        if self.moving_left:
            self.x -= self.settings.ship_speed
        # Update rect object from self.x.
        self.rect.x = self.x (4)

    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.x 的整数部分会被分配给 self.rect.x,但这对于显示船舶来说很好。

现在我们可以改变 ship_speed 的值,任何大于1的值都会使船移动得更快。这将有助于使飞船做出足够快的反应以击落外星人,并且可以让我们随着玩家在游戏中的进行而改变游戏的节奏。

限制飞船的活动范围

此时,如果您按住箭头键足够长的时间,飞船就会从屏幕的任一边缘消失。让我们纠正这个问题,以便飞船在到达屏幕边缘时停止移动。我们通过修改 Ship 中的 update() 方法来做到这一点:

ship.py
def update(self):
    """Update the ship's position based on movement flags."""
    # Update the ship's x value, not the rect.
    if self.moving_right and self.rect.right < self.screen_rect.right: (1)
        self.x += self.settings.ship_speed
    if self.moving_left and self.rect.left > 0: (2)
        self.x -= self.settings.ship_speed

    # Update rect object from self.x.
    self.rect.x = self.x

此代码在更改 self.x 的值之前检查船舶的位置。代码 self.rect.right 返回船舶矩形右边缘的 x 坐标。如果这个值小于 self.screen_rect.right 返回的值,则船还没有到达屏幕的右边缘❶。左边缘也是如此:如果矩形左侧的值大于0,则船还没有到达屏幕的左边缘❷。这可以确保在调整 self.x 的值之前船舶处于这些范围内。

当你现在运行 alien_invasion.py 时,飞船应该在屏幕的任一边缘停止移动。这很酷;我们所做的只是在 if 语句中添加一个条件测试,但感觉就像船撞到了屏幕任一边缘的墙壁或力场!

重构_check_events()

随着我们继续开发游戏,_check_events() 方法的长度会增加,所以让我们将 _check_events() 分成两个独立的方法:一个处理 KEYDOWN 事件,另一个处理 KEYUP 事件:

alien_invasion.py
def _check_events(self):
    """Respond to keypresses and mouse events."""
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit()
        elif event.type == pygame.KEYDOWN:
            self._check_keydown_events(event)
        elif event.type == pygame.KEYUP:
            self._check_keyup_events(event)

def _check_keydown_events(self, event):
    """Respond to keypresses."""
    if event.key == pygame.K_RIGHT:
        self.ship.moving_right = True
    elif event.key == pygame.K_LEFT:
        self.ship.moving_left = True

def _check_keyup_events(self, event):
    """Respond to key releases."""
    if event.key == pygame.K_RIGHT:
        self.ship.moving_right = False
    elif event.key == pygame.K_LEFT:
        self.ship.moving_left = False

我们制作了两个新的辅助方法:_check_keydown_events() 和 _check_keyup_events()。 每个都需要一个 self 参数和一个 event 参数。这两个方法的主体是从 _check_events() 复制而来的,我们用对新方法的调用替换了旧代码。 现在,有了更清晰的代码结构,_check_events() 方法变得更简单,这将使开发对玩家输入的进一步响应变得更容易。

按Q键退出

现在我们可以有效地响应按键,我们可以添加另一种退出游戏的方法。 每次测试新功能时,单击游戏窗口顶部的 X 来结束游戏会很乏味,因此我们将添加一个键盘快捷键,以在玩家按 Q 时结束游戏:

alien_invasion.py
def _check_keydown_events(self, event):
    --snip--
    elif event.key == pygame.K_LEFT:
        self.ship.moving_left = True
    elif event.key == pygame.K_q:
        sys.exit()

在 _check_keydown_events() 中,我们添加了一个新块,当玩家按 Q 时结束游戏。现在,在测试时,您可以按 Q 关闭游戏,而不是使用光标关闭窗口。

在全屏模式下运行游戏

Pygame 具有全屏模式,与在常规窗口中运行游戏相比,您可能更喜欢这种模式。有些游戏在全屏模式下看起来更好,而在某些系统上,游戏在全屏模式下的整体表现可能更好。

要以全屏模式运行游戏,请在 __init__() 中进行以下更改:

alien_invasion.py
def __init__(self):
    """Initialize the game, and create game resources."""
    pygame.init()
    self.settings = Settings()
    self.screen = pygame.display.set_mode((0, 0), pygame.FULLSCREEN) (1)
    self.settings.screen_width = self.screen.get_rect().width (2)
    self.settings.screen_height = self.screen.get_rect().height
    pygame.display.set_caption("Alien Invasion")

创建屏幕表面(surface)时,我们传递尺寸为 (0, 0) 和参数 pygame.FULLSCREEN ❶。这告诉 Pygame 计算出填满屏幕的窗口大小。因为我们提前不知道屏幕的宽度和高度,所以我们在屏幕创建后更新这些设置❷。 我们使用屏幕矩形(rect)的宽度(width)和高度(height)属性来更新设置(settings)对象。

如果您喜欢游戏在全屏模式下的外观或行为,请保留这些设置。如果您更喜欢游戏自己的窗口,您可以恢复到我们为游戏设置特定屏幕尺寸的原始方法。

在全屏模式下运行游戏之前,请确保您可以按 Q 退出; Pygame 不提供在全屏模式下退出游戏的默认方法。