射击

现在让我们添加发射子弹的能力。我们将编写当玩家按下空格键时发射子弹的代码,该子弹由一个小矩形表示。然后子弹将在屏幕上直线移动,直到消失在屏幕顶部。

添加子弹设置

__init__() 方法的末尾,我们将更新 settings.py 以包含新 Bullet 类所需的值:

settings.py
def __init__(self):
    --snip--
    # Bullet settings
    self.bullet_speed = 2.0
    self.bullet_width = 3
    self.bullet_height = 15
    self.bullet_color = (60, 60, 60)

这些设置创建了宽度为 3 像素、高度为 15 像素的深灰色子弹符号。子弹的行进速度会比船快一点。

创建Bullet类

现在创建一个 bullet.py 文件来存储我们的 Bullet 类。 这是 bullet.py 的第一部分:

bullet.py
import pygame
from pygame.sprite import Sprite

class Bullet(Sprite):
    """A class to manage bullets fired from the ship."""

    def __init__(self, ai_game):
        """Create a bullet object at the ship's current position."""
        super().__init__()
        self.screen = ai_game.screen
        self.settings = ai_game.settings
        self.color = self.settings.bullet_color

        # Create a bullet rect at (0, 0) and then set correct position.
        self.rect = pygame.Rect(0, 0, self.settings.bullet_width,self.settings.bullet_height) (1)
        self.rect.midtop = ai_game.ship.rect.midtop (2)

        # Store the bullet's position as a float.
        self.y = float(self.rect.y) (3)

Bullet 类继承自 Sprite,我们从 pygame.sprite 模块导入它。 当您使用 Sprite 时,您可以将游戏中的相关元素分组并立即对所有分组的元素进行操作。 要创建子弹实例,__init__() 需要 AlienInvasion 的当前实例,并且我们调用 super() 来从 Sprite 正确继承。 我们还设置屏幕和设置对象以及项目符号颜色的属性。

接下来我们创建项目符号的 rect 属性❶。 子弹不是基于图像的,因此我们必须使用 pygame.Rect() 类从头开始构建一个矩形。 此类需要矩形左上角的 x 和 y 坐标,以及矩形的宽度和高度。 我们在 (0, 0) 处初始化矩形,但我们会将其移动到下一行中的正确位置,因为子弹的位置取决于船舶的位置。 我们从 self.settings 中存储的值获取项目符号的宽度和高度。

我们将子弹的 midtop 属性设置为与飞船的 midtop 属性相匹配❷。 这将使子弹从船的顶部出现,使其看起来像是从船上发射的。 我们使用浮点数作为子弹的 y 坐标,这样我们就可以对子弹的速度进行微调❸。

这是 bullet.py 的第二部分,update() 和 draw_bullet() :

bullet.py
def update(self):
    """Move the bullet up the screen."""
    # Update the exact position of the bullet.
    self.y -= self.settings.bullet_speed (1)
    # Update the rect position.
    self.rect.y = self.y (2)

def draw_bullet(self):
    """Draw the bullet to the screen."""
    pygame.draw.rect(self.screen, self.color, self.rect) (3)

update() 方法管理子弹的位置。 当子弹发射时,它会在屏幕上向上移动,这对应于递减的 y 坐标值。为了更新位置,我们从 self.y 中减去 settings.bullet_speed 中存储的数量❶。 然后我们使用 self.y 的值来设置 self.rect.y 的值❷。

Bullet_speed 设置允许我们随着游戏的进展或根据需要提高子弹的速度以改进游戏的行为。 一旦子弹发射,我们就永远不会改变它的 x 坐标值,因此即使船移动,它也会沿直线垂直移动。

当我们想要绘制子弹时,我们调用 draw_bullet()。 draw.rect() 函数用存储在 self.color ❸ 中的颜色填充由项目符号矩形定义的屏幕部分。

将子弹存储到编组中

现在我们已经定义了 Bullet 类和必要的设置,我们可以编写代码在玩家每次按空格键时发射子弹。我们将在 AlienInvasion 中创建一个组来存储所有活动子弹,以便我们可以管理已经发射的子弹。该组将是 pygame.sprite.Group 类的一个实例,其行为类似于一个具有一些额外功能的列表,这在构建游戏时很有帮助。 我们将使用该组在每次通过主循环时将项目符号绘制到屏幕上并更新每个项目符号的位置。

首先,我们将导入新的 Bullet 类:

alien_invasion.py
--snip--
from ship import Ship
from bullet import Bullet

接下来我们将在 __init__() 中创建保存 bullets 符号的组:

alien_invasion.py
def __init__(self):
    --snip--
    self.ship = Ship(self)
    self.bullets = pygame.sprite.Group()

然后我们需要在 while 循环时更新子弹的位置:

alien_invasion.py
def run_game(self):
    """Start the main loop for the game."""
    while True:
        self._check_events()
        self.ship.update()
        self.bullets.update() (1)
        self._update_screen()
        self.clock.tick(60)

当您对组调用 update() 时,该组会自动为组中的每个 sprite 调用 update() 。 self.bullets.update() 行为我们放置在 bullet 符号组中的每个 bullet 符号调用 bullet.update() 。

开火

在 AlienInvasion 中,我们需要修改 _check_keydown_events() 以在玩家按下空格键时发射子弹。我们不需要更改 _check_keyup_events(),因为释放空格键时不会发生任何事情。我们还需要修改 _update_screen() 以确保在调用 flip() 之前将每个 bullet 符号绘制到屏幕上。

当我们发射子弹时,会有一些工作要做,所以让我们编写一个新方法 _fire_bullet() 来处理这项工作:

alien_invasion.py
def _check_keydown_events(self, event):
    --snip--
    elif event.key == pygame.K_q:
        sys.exit()
    elif event.key == pygame.K_SPACE:  (1)
        self._fire_bullet()

def _check_keyup_events(self, event):
    --snip--

def _fire_bullet(self):
    """Create a new bullet and add it to the bullets group."""
    new_bullet = Bullet(self) (2)
    self.bullets.add(new_bullet) (3)

def _update_screen(self):
    """Update images on the screen, and flip to the new screen."""
    self.screen.fill(self.settings.bg_color)
    for bullet in self.bullets.sprites(): (4)
        bullet.draw_bullet()
    self.ship.blitme()

    pygame.display.flip()
    --snip--

当按下空格键时我们调用 _fire_bullet() ❶。 在 _fire_bullet() 中,我们创建了一个 Bullet 的实例,并将其命名为 new_bullet ❷。 然后我们使用 add() 方法将其添加到组 bullet 符号中❸。 add() 方法与 append() 类似,但它是专门为Pygame 组编写的。

Bullets.sprites() 方法返回 bullet 符号组中所有 sprites 的列表。 为了将所有发射的子弹绘制到屏幕上,我们循环遍历 bullets 中的 sprites ,并对每个 sprites 调用 draw_bullet() ❹。 我们将此循环放置在绘制船的代码行之前,这样子弹就不会从船的顶部开始。

当你现在运行 alien_invasion.py 时,你应该能够左右移动飞船并发射任意数量的子弹。子弹沿着屏幕向上移动,到达顶部后消失,如图 12-3 所示。 您可以在 settings.py 中更改 bullets 符号的大小、颜色和速度。

image 2023 06 23 20 04 02 675
Figure 1. The ship after firing a series of bullets

删除已消失的子弹

目前,子弹到达顶部时就会消失,但这只是因为 Pygame 无法将它们绘制到屏幕顶部上方。子弹实际上仍然存在;它们的 y 坐标值只会变得越来越负。这是一个问题,因为它们继续消耗内存和处理能力。

我们需要删除这些旧子弹,否则游戏会因为做这么多不必要的工作而减慢速度。为此,我们需要检测子弹矩形的 bottom 值何时为 0,这表示子弹已经从屏幕顶部飞过:

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

        # Get rid of bullets that have disappeared.
        for bullet in self.bullets.copy(): (1)
            if bullet.rect.bottom <= 0: (2)
                self.bullets.remove(bullet) (3)
        print(len(self.bullets)) (4)

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

当您将 for 循环与列表(或 Pygame 中的组)一起使用时,Python 期望只要循环运行,列表长度就不会变化。 这意味着您无法在 for 循环中从列表或组中删除项目,因此我们必须循环访问组的副本。 我们使用 copy() 方法来设置 for 循环❶,这使我们可以自由地修改循环内的原始 bullets 符号组。 我们检查每个 bullet 符号,看看它是否已经从屏幕顶部消失❷。 如果有,我们将其从 bullets 符号中删除❸。 我们插入一个 print() 调用来显示游戏中当前存在多少子弹,并验证它们在到达屏幕顶部时是否被删除❹。

如果这段代码工作正常,我们可以在发射子弹时观察终端输出,并看到在每一系列子弹清除屏幕顶部后,子弹数量减少到零。运行游戏并验证子弹是否已正确删除后,删除 print() 调用。如果您保留它,游戏速度会显着减慢,因为将输出写入终端比将图形绘制到游戏窗口需要更多时间。

限制子弹数量

许多射击游戏都会限制玩家一次在屏幕上可以拥有的子弹数量;这样做可以鼓励玩家准确射击。 我们也会在《异形入侵》中做同样的事情。 首先,在 settings.py 中存储允许的项目符号数量:

settings.py
# Bullet settings
--snip--
self.bullet_color = (60, 60, 60)
self.bullets_allowed = 3

这限制了玩家一次只能发射三颗子弹。 我们将在 AlienInvasion 中使用此设置,在 _fire_bullet() 中创建新子弹之前检查存在多少子弹:

alien_invasion.py
def _fire_bullet(self):
    """Create a new bullet and add it to the bullets group."""
    if len(self.bullets) < self.settings.bullets_allowed:
        new_bullet = Bullet(self)
        self.bullets.add(new_bullet)

当玩家按下空格键时,我们会检查子弹的长度。如果 len(self.bullets) 小于三,我们创建一个新 bullet 符号。 但是,如果三颗子弹已经处于活动状态,则按下空格键时不会发生任何事情。当你现在运行游戏时,你应该只能以三颗为一组发射子弹。

创建 _update_bullets() 方法

我们希望保持 AlienInvasion 类的组织良好,因此现在我们已经编写并检查了子弹管理代码,我们可以将其移至单独的方法中。我们将创建一个名为 _update_bullets() 的新方法,并将其添加到 _update_screen() 之前:

alien_invasion.py
def _update_bullets(self):
    """Update position of bullets and get rid of old bullets."""
    # Update bullet positions.
    self.bullets.update()

    # Get rid of bullets that have disappeared.
    for bullet in self.bullets.copy():
        if bullet.rect.bottom <= 0:
            self.bullets.remove(bullet)

_update_bullets() 的代码是从 run_game() 剪切并粘贴的;我们在这里所做的只是清除注释。

run_game() 中的 while 循环看起来又很简单:

alien_invasion.py
while True:
    self._check_events()
    self.ship.update()
    self._update_bullets() (1)
    self._update_screen()
    self.clock.tick(60)

现在我们的主循环只包含最少的代码,因此我们可以快速读取方法名称并了解游戏中发生的情况。主循环检查玩家输入,然后更新飞船的位置和已发射的所有子弹。然后,我们使用更新的位置绘制一个新屏幕,并在每次循环结束时滴答时钟。

再运行一次 alien_invasion.py ,并确保您仍然可以毫无错误地发射子弹。

自己试试
12-6.Sideways Shooter

编写一个游戏,将一艘船放置在屏幕左侧,并允许玩家上下移动船。 当玩家按下空格键时,让飞船发射一颗子弹,子弹会直接穿过屏幕。 确保项目符号从屏幕上消失后即被删除。