结束游戏

玩一个不会输的游戏有什么乐趣和挑战?如果玩家没有足够快地击落舰队,我们会让外星人在接触时摧毁飞船。同时,我们会限制玩家使用的飞船数量,当外星人到达屏幕下方时,我们就会摧毁飞船。当玩家用完所有飞船后,游戏就会结束。

检测外星飞船碰撞

我们将首先检查外星人与飞船之间的碰撞,以便在外星人撞上飞船时作出适当反应。在更新 AlienInvasion 中每个外星人的位置后,我们将立即检查外星人与飞船的碰撞:

def _update_aliens(self):
    --snip--
    self.aliens.update()

    # Look for alien-ship collisions.
    if pygame.sprite.spritecollideany(self.ship, self.aliens): (1)
        print("Ship hit!!!") (2)
python

spritecollideany() 函数有两个参数:一个精灵和一个组。该函数查找与精灵碰撞的组中的任何成员,并在找到与精灵碰撞的成员后立即停止在组中循环。 在这里,它循环遍历外星人组并返回它发现的第一个与飞船相撞的外星人。

如果没有发生碰撞,spritecollideany() 返回 None 并且 if 块不会执行 ❶。 如果它发现一个与飞船相撞的外星人,它会返回该外星人,并且执行 if 块:它会打印 Ship hit!!! ❷. 当外星人击中飞船时,我们需要执行许多任务:删除所有剩余的外星人和子弹,重新调整飞船的位置,并创建一支新的舰队。在我们编写代码来完成所有这些之前,我们想知道我们检测外星飞船碰撞的方法是否正确工作。编写 print() 调用是确保我们正确检测这些冲突的简单方法。

现在,当您运行《外星人入侵》时,会出现消息 Ship hit!!! 每当外星人闯入飞船时,航站楼就会出现。当您测试此功能时,请将 fleet_drop_speed 设置为更高的值,例如 50 或 100,以便外星人更快地到达您的飞船。

应对外星飞船碰撞

现在,我们需要弄清楚外星人与飞船相撞时会发生什么。我们不会销毁飞船实例并创建一个新实例,而是通过游戏中的跟踪统计来计算飞船被撞击的次数。跟踪统计对于计分也很有用。

让我们编写一个新类 GameStats 来跟踪游戏统计数据,并将其保存为 game_stats.py

game_stats.py
class GameStats:
    """Track statistics for Alien Invasion."""

    def __init__(self, ai_game):
        """Initialize statistics."""
        self.settings = ai_game.settings
        self.reset_stats() (1)

    def reset_stats(self):
        """Initialize statistics that can change during the game."""
        self.ships_left = self.settings.ship_limit
python

我们将为《异形入侵》的整个运行过程创建一个 GameStats 实例,但每次玩家开始新游戏时,我们都需要重置一些统计数据。为此,我们将在 reset_stats() 方法中初始化大部分统计数据,而不是直接在 __init__() 中初始化。我们将在 __init__() 中调用该方法,这样在首次创建 GameStats 实例时,统计数据就会被正确设置❶。但我们也可以在玩家开始新游戏时随时调用 reset_stats()。现在我们只有一个统计数据,即 ships_left,其值会在整个游戏过程中发生变化。

玩家开始时拥有的战舰数量应作为 ship_limit 储存在 settings.py 中:

settings.py
# Ship settings
self.ship_speed = 1.5
self.ship_limit = 3
python

我们还需要对 alien_invasion.py 进行一些修改,以创建 GameStats 实例。首先,我们要更新文件顶部的导入语句:

import sys
from time import sleep

import pygame

from settings import Settings
from game_stats import GameStats
from ship import Ship
--snip--
python

我们从 Python 标准库中的 time 模块导入 sleep() 函数,这样当飞船被击中时,我们就可以暂停游戏片刻。我们还导入了 GameStats

我们将在 init() 中创建一个 GameStats 实例:

def __init__(self):
    --snip--
    self.screen = pygame.display.set_mode((self.settings.screen_width, self.settings.screen_height))
    pygame.display.set_caption("Alien Invasion")

    # Create an instance to store game statistics.
    self.stats = GameStats(self)

    self.ship = Ship(self)
    --snip--
python

我们在创建游戏窗口后,但在定义飞船等其他游戏元素前创建实例。

当外星人撞上飞船时,我们将从剩余的飞船数量中减去 1,摧毁所有现有的外星人和子弹,创建一支新的舰队,并将飞船重新定位在屏幕中央。我们还会暂停游戏片刻,让玩家注意到碰撞,并在新舰队出现之前重新集结。

让我们把大部分代码放在一个名为 _ship_hit() 的新方法中。当有外星人击中飞船时,我们将从 _update_aliens() 中调用这个方法:

alien_invasion.py
def _ship_hit(self):
    """Respond to the ship being hit by an alien."""
    # Decrement ships_left.
    self.stats.ships_left -= 1 (1)

    # Get rid of any remaining bullets and aliens.
    self.bullets.empty() (2)
    self.aliens.empty()

    # Create a new fleet and center the ship.
    self._create_fleet() (3)
    self.ship.center_ship()

    # Pause.
    sleep(0.5) (4)
python

新方法 _ship_hit() 协调了外星人击中飞船时的反应。在 _ship_hit() 方法中,剩下的飞船数量会减少 1 ❶,然后我们清空 bullets 组和 aliens 组 ❷。

接下来,我们创建一个新的舰队,并将飞船❸ 置中。(稍后我们将为飞船添加 center_ship() 方法)。然后,我们在更新所有游戏元素后,但在屏幕上绘制出任何变化之前,添加一个暂停,这样玩家就可以看到他们的飞船被击中了❹。sleep() 调用会让程序暂停执行半秒,足够长的时间让玩家看到外星人击中了飞船。当 sleep() 函数结束后,代码执行将转入 _update_screen() 方法,该方法将新的舰队绘制到屏幕上。

_update_aliens() 方法中,当有异形击中飞船时,我们会调用 _ship_hit() 方法来代替 print() 方法:

alien_invasion.py
def _update_aliens(self):
    --snip--
    if pygame.sprite.spritecollideany(self.ship, self.aliens):
        self._ship_hit()
python

下面是新方法 center_ship(),它属于 ship.py 中:

ship.py
def center_ship(self):
    """Center the ship on the screen."""
    self.rect.midbottom = self.screen_rect.midbottom
    self.x = float(self.rect.x)
python

我们按照 __init__() 中的方法将飞船居中。居中后,我们重置 self.x 属性,这样就可以跟踪飞船的准确位置。

请注意,我们从不制造多于一艘的船只;我们在整个游戏中只制造一个船只实例,并在船只被击中时重新调整其位置。统计数据 ships_left 将告诉我们玩家的飞船何时耗尽。

运行游戏,射杀几个外星人,让外星人撞击飞船。游戏应该暂停,然后会出现一个新的舰队,飞船重新位于屏幕下方的中心位置。

到达屏幕底部的外星人

如果有外星人到达屏幕底部,我们会让游戏做出与外星人撞击飞船时相同的反应。要检查何时发生这种情况,请在 alien_invasion.py 中添加一个新方法:

alien_invasion.py
def _check_aliens_bottom(self):
    """Check if any aliens have reached the bottom of the screen."""
    for alien in self.aliens.sprites():
        if alien.rect.bottom >= self.settings.screen_height: (1)

            # Treat this the same as if the ship got hit.
            self._ship_hit()
            break
python

方法 _check_aliens_bottom() 检查是否有外星人到达屏幕底部。当异形的 rect.bottom 值大于或等于屏幕高度❶ 时,它就到达了底部。如果有外星人到达底部,我们就会调用 _ship_hit()。如果有一个外星人到达底部,就没有必要检查其余的,因此我们在调用 _ship_hit() 后跳出循环。

我们将在 _update_aliens() 中调用该方法:

alien_invasion.py
def _update_aliens(self):
    --snip--
    # Look for alien-ship collisions.
    if pygame.sprite.spritecollideany(self.ship, self.aliens):
        self._ship_hit()

        # Look for aliens hitting the bottom of the screen.
        self._check_aliens_bottom()
python

我们会在更新所有外星人的位置和查找外星人与飞船碰撞后调用 _check_aliens_bottom()。现在,每当飞船被外星人撞击或外星人到达屏幕底部时,就会出现一个新的舰队。

游戏结束

外星入侵 现在感觉更完整了,但游戏永远不会结束。剩下的战舰值只会越来越负。让我们添加一个 game_active 标志,这样我们就能在玩家耗尽船只时结束游戏。我们将在 AlienInvasion__init__() 方法末尾设置这个标志:

alien_invasion.py
def __init__(self):
    --snip--
    # Start Alien Invasion in an active state.
    self.game_active = True
python

现在,我们在 _ship_hit() 中添加代码,当玩家用完所有船只时,将 game_active 设置为 False

alien_invasion.py
def _ship_hit(self):
    """Respond to ship being hit by alien."""
    if self.stats.ships_left > 0:
        # Decrement ships_left.
        self.stats.ships_left -= 1
        --snip--
        # Pause.
        sleep(0.5)
    else:
        self.game_active = False
python

_ship_hit() 的大部分内容没有变化。我们将所有现有代码移到了一个 if 代码块中,该代码块会进行测试,以确保玩家至少还有一艘飞船。如果是,我们会创建一支新舰队,暂停并继续前进。如果玩家没有剩余船只,我们会将 game_active 设置为 False

确定游戏的某些部分何时应该运行

我们需要确定游戏中哪些部分应该始终运行,哪些部分只有在游戏激活时才运行:

alien_invasion.py
def run_game(self):
    """Start the main loop for the game."""
    while True:
        self._check_events()

        if self.game_active:
            self.ship.update()
            self._update_bullets()
            self._update_aliens()

        self._update_screen()
        self.clock.tick(60)
python

在主循环中,即使游戏处于非活动状态,我们也始终需要调用 _check_events()。例如,我们仍然需要知道用户是否按 Q 键退出游戏或点击按钮关闭窗口。我们还要继续更新屏幕,以便在等待玩家是否选择开始新游戏的同时对屏幕进行更改。其余的函数调用只需在游戏处于活动状态时进行,因为游戏处于非活动状态时,我们不需要更新游戏元素的位置。

现在,当你玩 外星入侵 游戏时,游戏会在你用完所有飞船后冻结。

亲身体验

13-6. 游戏结束:在 "侧向射击" 游戏中,记录飞船被击中的次数和外星人被飞船击中的次数。决定结束游戏的适当条件,并在出现这种情况时停止游戏。

总结

在本章中,你将学习如何通过创建一支外星人舰队,为游戏添加大量相同的元素。你使用嵌套循环创建了一个元素网格,并通过调用每个元素的 update() 方法使大量游戏元素移动。你学会了控制屏幕上物体的方向,并对特定情况做出反应,例如当舰队到达屏幕边缘时。当子弹击中外星人和外星人击中飞船时,你们会检测到碰撞并做出反应。您还学会了如何跟踪游戏中的统计数据,并使用 game_active 标志来确定游戏何时结束。

在这个项目的下一章,也是最后一章,我们将添加一个 "播放" 按钮,这样玩家就可以选择何时开始他们的第一场游戏,以及游戏结束后是否再玩一次。每次玩家击落整个舰队时,我们都会加快游戏速度,并添加一个计分系统。最终的结果将是一个完全可玩的游戏!