Python编程 从入门到实践(项目一:外星人入侵)

本篇为实践项目一:外星人入侵。
配合文章python编程入门学习,代码附文末。

Python编程 实践 项目一:外星人入侵

  • 1. 武装飞船
    • 1.1 规划项目
    • 1.2 安装Pygame
    • 1.3 开始游戏项目
      • 1.3.1 创建Pygame窗口及响应用户输入
      • 1.3.2 设置背景色
      • 1.3.3 创建设置类
    • 1.4 添加飞船图像
      • 1.4.1 创建Ship类
      • 1.4.2 在屏幕上绘制飞船
    • 1.5 重构:方法_check_events()和__update_screen()
      • 1.5.1 方法_check_events()
      • 1.5.2 方法_update_screen()
    • 1.6 驾驶飞船
      • 1.6.1 响应按键
      • 1.6.2 允许持续移动
      • 1.6.3 左右移动
      • 1.6.4 调整飞船的速度
      • 1.6.5 限制飞船的活动范围
      • 1.6.6 重构_check_events()
      • 1.6.7 按Q键退出
      • 1.6.8 在全屏模式下运行游戏
    • 1.7 简单回顾
      • 1.7.1 alien_invasion.py
      • 1.7.2 settings.py
      • 1.7.3 ship.py
    • 1.8 射击
      • 1.8.1 添加子弹设置
      • 1.8.2 创建Bullet类
      • 1.8.3 将子弹存储到编组中
      • 1.8.4 开火
      • 1.8.5 删除消失的子弹
      • 1.8.6 限制子弹数量
      • 1.8.7 创建方法_update_bullets()
  • 2. 外星人来了
    • 2.1 创建第一个外星人
      • 2.1.1 创建Alien类
      • 2.1.2 创建Alien实例
    • 2.2 创建一群外星人
      • 2.2.1 确定一行可容纳多少个外星人
      • 2.2.2 创建一行外星人
      • 2.2.3 重构_create_fleet()
      • 2.2.4 添加行
    • 2.3 让外星人群移动
      • 2.3.1 向右移动外星人群
      • 2.3.2 创建表示外星人移动方向的设置
      • 2.3.3 检查外星人是否撞到了屏幕边缘
      • 2.3.4 向下移动外星人群并改变移动方向
    • 2.4 射杀外星人
      • 2.4.1 检测子弹与外星人的碰撞
      • 2.4.2 为测试创建大子弹
      • 2.4.3 生成新的外星人群
      • 2.4.4 提高子弹的速度
      • 2.4.5 重构_update_bullets()
    • 2.5 结束游戏
      • 2.5.1 检测外星人和飞船碰撞
      • 2.5.2 响应外星人和飞船碰撞
      • 2.5.3 有外星人到达屏幕底端
      • 2.5.4 游戏结束
    • 2.6 确定应运行游戏的哪些部分
  • 3.计分
    • 3.1添加Play按钮
      • 3.1.1 创建Button类
      • 3.1.2 在屏幕上绘制按钮
      • 3.1.3 开始游戏
      • 3.1.4 重置游戏
      • 3.1.5 将Play按钮切换到非活动状态
      • 3.1.6 隐藏鼠标光标
    • 3.2 提高等级
      • 3.2.1 修改速度设置
      • 3.2.2 重置速度
    • 3.3 记分
      • 3.3.1 显示得分
      • 3.3.2 创建记分牌
      • 3.3.3 在外星人被消灭时更新得分
      • 3.3.4 重置得分
      • 3.3.5 将消灭的每个外星人都计入得分
      • 3.3.6 提高分数
      • 3.3.7 舍入得分
      • 3.3.8 最高得分
      • 3.3.9 显示等级
      • 3.3.10 显示余下的飞船数
  • 4.小结

1. 武装飞船

这里将使用Pygame,这是一组功能强大而有趣的模块,可用于管理图形、动画乃至声音,让你能够更轻松地开发复杂的游戏。通过使用Pygame来处理在屏幕上绘制图像等任务,可将重点放在程序的高级逻辑上。

注意 游戏《外星人入侵》将包含很多不同的文件,因此请在系统中新建一个名为alien_invasion的文件夹,并将该项目的所有文件都存储到该文件夹中,这样相关的import语句才能正确工作。

1.1 规划项目

开发大型项目时,制定好规划后再动手编写代码很重要。规划可确保你不偏离轨道,从而提高项目成功的可能性。

下面来编写有关游戏《外星人入侵》的描述,其中虽然没有涵盖这款游戏的所有细节,但能可以清楚地知道该如何动手开发。

在游戏《外星人入侵》中,玩家控制一艘最初出现在屏幕底部中央的飞船。玩家可以使用箭头键左右移动飞船,还可使用空格键射击。游戏开始时,一群外星人出现在天空中,并向屏幕下方移动。玩家的任务是射杀这些外星人。玩家将所有外星人都消灭干净后,将出现一群新的外星人,其移动速度更快。只要有外星人撞到玩家的飞船或到达屏幕底部,玩家就损失一艘飞船。玩家损失三艘飞船后,游戏结束。
开发的第一个阶段将创建一艘飞船,它可左右移动,并且能在用户按空格键时开火。设置好这种行为后,就可以创建外星人并提高游戏的可玩性。

1.2 安装Pygame

开始编码前,先来安装Pygame。可使用pip模块来帮助下载并安装Python包。要安装Pygame,在终端提示符下执行如下命令:
python -m pip install --user pygame
这个命令让Python运行pip模块,将pygame包添加到当前用户的Python安装中。如果你运行程序或启动终端会话时使用的命令不是python,而是python3,请执行如下命令来安装Pygame:
python3 -m pip install --user pygame

注意 如果该命令在macOS系统中不管用,请尝试在不指定标志–user的情况下再次执行。

如果不知该如何安装,请点击相关文章Python拓展库下载 ,其中有详细的安装方法和基本库的一键安装文件。

1.3 开始游戏项目

开始开发游戏《外星人入侵》吧。首先要创建一个空的Pygame窗口,供之后用来绘制游戏元素,如飞船和外星人。我们还将让这个游戏响应用户输入,设置背景色,以及加载飞船图像。

1.3.1 创建Pygame窗口及响应用户输入

下面创建一个表示游戏的类,以创建空的Pygame窗口。为此,在文本编辑器中新建一个文件,将其保存为alien_invasion.py,再在其中输入如下代码:

import sys

import pygame


# 导入模块sys和pygame。模块pygame包含开发游戏所需的功能。玩家退出时,我们将使用模块sys中的工具来退出游戏


class AlienInvasion:
    """管理游戏资源和行为的类"""

    def __init__(self):
        """初始化游戏并创建游戏资源"""
        pygame.init()  # 初始化背景设置

        self.screen = pygame.display.set_mode((1200, 800))  # 创建一个显示窗口,将这个显示窗口赋给属性self.screen,让这个类中的所有方法都能够使用它
        pygame.display.set_caption("Alien Invasion")

    def run_game(self):
        """开始游戏的主循环"""
        while True:
            # 监视键盘和鼠标事件

            for event in pygame.event.get():
                if event.type == pygame.QUIT:  # 当玩家单击游戏窗口的关闭按钮时,将检测到pygame.QUIT事件,进而调用sys.exit()来退出游戏
                    sys.exit()

            # 让最近绘制的屏幕可见
            pygame.display.flip()  # 每次执行while循环时都绘制一个空屏幕,并擦去旧屏幕,使得只有新屏幕可见


if __name__ == '__main__':
    # 创建游戏实例并运行游戏
    ai = AlienInvasion()
    ai.run_game()

# 在这个文件末尾,创建一个游戏实例并调用run_game()。这些代码放在一个if代码块中,仅当直接运行该文件时,它们才会执行。
# 如果此时运行alien_invasion.py,将看到一个空的Pygame窗口。

1.3.2 设置背景色

Pygame默认创建一个黑色屏幕,这太乏味了。下面来将背景设置为另一种颜色,这是在方法__init__()末尾进行的:

import sys

import pygame


# 导入模块sys和pygame。模块pygame包含开发游戏所需的功能。玩家退出时,我们将使用模块sys中的工具来退出游戏


class AlienInvasion:
    """管理游戏资源和行为的类"""

    def __init__(self):
        """初始化游戏并创建游戏资源"""
        pygame.init()  # 初始化背景设置

        self.screen = pygame.display.set_mode((1200, 800))  # 创建一个显示窗口,将这个显示窗口赋给属性self.screen,让这个类中的所有方法都能够使用它
        pygame.display.set_caption("Alien Invasion")

        # 设置背景色
        self.bg_color = (230, 230, 230)

    def run_game(self):
        """开始游戏的主循环"""
        while True:
            # 监视键盘和鼠标事件

            for event in pygame.event.get():
                if event.type == pygame.QUIT:  # 当玩家单击游戏窗口的关闭按钮时,将检测到pygame.QUIT事件,进而调用sys.exit()来退出游戏
                    sys.exit()

            # 每次循环时都重绘屏幕。
            self.screen.fill(self.bg_color)

            # 让最近绘制的屏幕可见
            pygame.display.flip()  # 每次执行while循环时都绘制一个空屏幕,并擦去旧屏幕,使得只有新屏幕可见


if __name__ == '__main__':
    # 创建游戏实例并运行游戏
    ai = AlienInvasion()
    ai.run_game()

# 在这个文件末尾,创建一个游戏实例并调用run_game()。这些代码放在一个if代码块中,仅当直接运行该文件时,它们才会执行。
# 如果此时运行alien_invasion.py,将看到一个空的Pygame窗口。

在Pygame中,颜色是以RGB值指定的。这种颜色由红色、绿色和蓝色值组成,其中每个值的可能取值范围都是0~255。颜色值(255, 0, 0)表示红色,(0, 255, 0)表示绿色,而(0, 0,255)表示蓝色。通过组合不同的RGB值,可创建1600万种颜色。在颜色值(230, 230, 230)中,红色、绿色和蓝色的量相同,它生成一种浅灰色。我们将这种颜色赋给了self.bg_color。

调用方法fill()用这种背景色填充屏幕。方法fill()用于处理surface,只接受一个实参:一种颜色。

1.3.3 创建设置类

每次给游戏添加新功能时,通常也将引入一些新设置。下面来编写一个名为settings的模块,在其中包含一个名为Settings的类,用于将所有设置都存储在一个地方,以免在代码中到处添加设置。这样,每当需要访问设置时,只需使用一个设置对象。另外,在项目增大时,这使得修改游戏的外观和行为更容易:要修改游戏,只需修改(接下来将创建的)settings.py中的一些值,而无须查找散布在项目中的各种设置。

在文件夹alien_invasion中,新建一个名为settings.py的文件,并在其中添加如下Settings类:

class Settings:
    """存储游戏中所有设置的类"""
    def __init__(self):
        """初始化游戏设置"""
        self.screen_width = 1200
        self.screen_height = 800
        self.bg_color = (230, 230, 230)

为在项目中创建Settings实例并用它来访问设置,需要将alien_invasion.py修改成下面这样:

import sys

import pygame

from settings import Settings
# 导入模块sys和pygame。模块pygame包含开发游戏所需的功能。玩家退出时,我们将使用模块sys中的工具来退出游戏


class AlienInvasion:
    """管理游戏资源和行为的类"""

    def __init__(self):
        """初始化游戏并创建游戏资源"""
        pygame.init()  # 初始化背景设置
        self.settings = Settings()
        self.screen = pygame.display.set_mode((self.settings.screen_width, self.settings.screen_height))  # 创建一个显示窗口,将这个显示窗口赋给属性self.screen,让这个类中的所有方法都能够使用它
        pygame.display.set_caption("Alien Invasion")

        # 设置背景色
        self.bg_color = (230, 230, 230)

    def run_game(self):
        """开始游戏的主循环"""
        while True:
            # 监视键盘和鼠标事件

            for event in pygame.event.get():
                if event.type == pygame.QUIT:  # 当玩家单击游戏窗口的关闭按钮时,将检测到pygame.QUIT事件,进而调用sys.exit()来退出游戏
                    sys.exit()

            # 每次循环时都重绘屏幕。
            self.screen.fill(self.settings.bg_color)

            # 让最近绘制的屏幕可见
            pygame.display.flip()  # 每次执行while循环时都绘制一个空屏幕,并擦去旧屏幕,使得只有新屏幕可见


if __name__ == '__main__':
    # 创建游戏实例并运行游戏
    ai = AlienInvasion()
    ai.run_game()

# 在这个文件末尾,创建一个游戏实例并调用run_game()。这些代码放在一个if代码块中,仅当直接运行该文件时,它们才会执行。
# 如果此时运行alien_invasion.py,将看到一个空的Pygame窗口。

在主程序文件中,导入Settings类,调用pygame.init(),再创建一个Settings实例并将其赋给self.settings。创建屏幕时,使用了self.settings的属性screen_width和screen_height。接下来填充屏幕时,也使用了self.settings来访问背景色。

1.4 添加飞船图像

下面将飞船加入游戏中。为了在屏幕上绘制玩家的飞船,我们将加载一幅图像,再使用Pygame方法blit()绘制它。

为游戏选择素材时,务必要注意许可。最安全、最不费钱的方式是使用Pixabay等网站提供的免费图形,无须授权许可即可使用并修改。

在游戏中几乎可以使用任何类型的图像文件,但使用位图(.bmp)文件最为简单,因为Pygame默认加载位图。虽然可配置Pygame以使用其他文件类型,但有些文件类型要求在计算机上安装相应的图像库。大多数图像为.jpg、.png或.gif格式,但可使用Photoshop、GIMP和Paint等工具将其转换为位图。

选择图像时,要特别注意背景色。请尽可能选择背景为透明或纯色的图像,便于使用图像编辑器将其背景替换为任意颜色。图像的背景色与游戏的背景色匹配时,游戏看起来最漂亮。也可以将游戏的背景色设置成图像的背景色。

在项目文件夹(alien_invasion)中新建一个名为images的文件夹,并将文件ship.bmp保存在其中。(图片文件在项目文件中)

1.4.1 创建Ship类

选择用于表示飞船的图像后,需要将其显示到屏幕上。我们创建一个名为ship的模块,其中包含Ship类,负责管理飞船的大部分行为。

import pygame


class Ship:
    """管理飞船类"""

    def __init__(self, ai_game):
        """初始化飞船并设置其初始设置"""
        self.screen = ai_game.screen
        self.screen_rect = ai_game.screen.get_rect()

        # 加载飞船图像并获取其外接矩形
        self.image = pygame.image.load('images/ship.bmp')
        self.rect = self.image.get_rect()

        # 对于每艘新飞船,都将其放在屏幕底部中央
        self.rect.midbottom = self.screen_rect.midbottom

    def blitme(self):
        """在指定位置绘制飞船"""
        self.screen.blit(self.image, self.rect)

Pygame之所以高效,是因为它能够像处理矩形(rect对象)一样处理所有的游戏元素,即便其形状并非矩形。像处理矩形一样处理游戏元素之所以高效,是因为矩形是简单的几何形状。例如,通过将游戏元素视为矩形,Pygame能够更快地判断出它们是否发生了碰撞。这种做法的效果通常很好,游戏玩家几乎注意不到我们处理的并不是游戏元素的实际形状。在这个类中,我们将把飞船和屏幕作为矩形进行处理。

定义这个类之前,导入了模块pygame。Ship的方法__init__()接受两个参数:引用self和指向当前AlienInvasion实例的引用。这让Ship能够访问AlienInvasion中定义的所有游戏资源。将屏幕赋给了Ship的一个属性,以便在这个类的所有方法中轻松访问。使用方法get_rect()访问屏幕的属性rect,并将其赋给了self.screen_rect,这让我们能够将飞船放到屏幕的正确位置。

调用pygame.image.load()加载图像,并将飞船图像的位置传递给它。该函数返回一个表示飞船的surface,而我们将这个surface赋给了self.image。加载图像后,使用get_rect()获取相应surface的属性rect,以便后面能够使用它来指定飞船的位置。

处理rect对象时,可使用矩形四角和中心的 坐标和 坐标。可通过设置这些值来指定矩形的位置。要让游戏元素居中,可设置相应rect对象的属性center、centerx或centery;要让游戏元素与屏幕边缘对齐,可使用属性top、bottom、left或right。除此之外,还有一些组合属性,如midbottom、midtop、midleft和midright。要调整游戏元素的水平或垂直位置,可使用属性x和y,分别是相应矩形左上角的 坐标和 坐标。这些属性让你无须做游戏开发人员原本需要手工完成的计算,因此会经常用到。

注意 在Pygame中,原点(0, 0)位于屏幕左上角,向右下方移动时,坐标值将增大。在1200 × 800的屏幕上,原点位于左上角,而右下角的坐标为(1200, 800)。这些坐标对应的是游戏窗口,而不是物理屏幕。

我们要将飞船放在屏幕底部的中央。为此,将self.rect.midbottom设置为表示屏幕的矩形的属性midbottom。Pygame使用这些rect属性来放置飞船图像,使其与屏幕下边缘对齐并水平居中。定义方法blitme(),它将图像绘制到self.rect指定的位置。

1.4.2 在屏幕上绘制飞船

下面更新alien_invasion.py,创建一艘飞船并调用其方法blitme():

import sys

import pygame

from settings import Settings
from ship import Ship

# 导入模块sys和pygame。模块pygame包含开发游戏所需的功能。玩家退出时,我们将使用模块sys中的工具来退出游戏


class AlienInvasion:
    """管理游戏资源和行为的类"""

    def __init__(self):
        """初始化游戏并创建游戏资源"""
        pygame.init()  # 初始化背景设置
        self.settings = Settings()
        self.screen = pygame.display.set_mode((self.settings.screen_width, self.settings.screen_height))  # 创建一个显示窗口,将这个显示窗口赋给属性self.screen,让这个类中的所有方法都能够使用它
        pygame.display.set_caption("Alien Invasion")
        # 设置背景色
        self.bg_color = (230, 230, 230)

        self.ship = Ship(self)

    def run_game(self):
        """开始游戏的主循环"""
        while True:
            # 监视键盘和鼠标事件

            for event in pygame.event.get():
                if event.type == pygame.QUIT:  # 当玩家单击游戏窗口的关闭按钮时,将检测到pygame.QUIT事件,进而调用sys.exit()来退出游戏
                    sys.exit()

            # 每次循环时都重绘屏幕。
            self.screen.fill(self.settings.bg_color)
            self.ship.blitme()

            # 让最近绘制的屏幕可见
            pygame.display.flip()  # 每次执行while循环时都绘制一个空屏幕,并擦去旧屏幕,使得只有新屏幕可见


if __name__ == '__main__':
    # 创建游戏实例并运行游戏
    ai = AlienInvasion()
    ai.run_game()

# 在这个文件末尾,创建一个游戏实例并调用run_game()。这些代码放在一个if代码块中,仅当直接运行该文件时,它们才会执行。
# 如果此时运行alien_invasion.py,将看到一个空的Pygame窗口。

导入Ship类,并在创建屏幕后创建一个Ship实例。调用Ship()时,必须提供一个参数:一个AlienInvasion实例。在这里,self指向的是当前AlienInvasion实例。
这个参数让Ship能够访问游戏资源,如对象screen。我们将这个Ship实例赋给了self.ship。

1.5 重构:方法_check_events()和__update_screen()

在大型项目中,经常需要在添加新代码前重构既有代码。重构旨在简化既有代码的结构,使其更容易扩展。本节将把越来越长的方法run_game()拆分成两个辅助方法(helper method)。辅助方法在类中执行任务,但并非是通过实例调用的。在Python中,辅助方法的名称以单个下划线打头。

1.5.1 方法_check_events()

我们将把管理事件的代码移到一个名为_check_events()的方法中,以简化run_game()并隔离事件管理循环。通过隔离事件循环,可将事件管理与游戏的其他方面(如更新屏幕)分离。

下面是新增方法_check_events()后的AlienInvasion类,只有run_game()的代码受到影响:

import sys

import pygame

from settings import Settings
from ship import Ship


# 导入模块sys和pygame。模块pygame包含开发游戏所需的功能。玩家退出时,我们将使用模块sys中的工具来退出游戏


class AlienInvasion:
    """管理游戏资源和行为的类"""

    def __init__(self):
        """初始化游戏并创建游戏资源"""
        pygame.init()  # 初始化背景设置
        self.settings = Settings()
        self.screen = pygame.display.set_mode((self.settings.screen_width,
                                               self.settings.screen_height))  # 创建一个显示窗口,将这个显示窗口赋给属性self.screen,让这个类中的所有方法都能够使用它
        pygame.display.set_caption("Alien Invasion")
        # 设置背景色
        self.bg_color = (230, 230, 230)

        self.ship = Ship(self)

    def run_game(self):
        """开始游戏的主循环"""
        while True:
            # 监视键盘和鼠标事件
            self._check_events()
            
            # 每次循环时都重绘屏幕。
            self.screen.fill(self.settings.bg_color)
            self.ship.blitme()

            # 让最近绘制的屏幕可见
            pygame.display.flip()  # 每次执行while循环时都绘制一个空屏幕,并擦去旧屏幕,使得只有新屏幕可见

    def _check_events(self):
        """相应按键和鼠标事件"""
        for event in pygame.event.get():
            if event.type == pygame.QUIT:  # 当玩家单击游戏窗口的关闭按钮时,将检测到pygame.QUIT事件,进而调用sys.exit()来退出游戏
                sys.exit()


if __name__ == '__main__':
    # 创建游戏实例并运行游戏
    ai = AlienInvasion()
    ai.run_game()

# 在这个文件末尾,创建一个游戏实例并调用run_game()。这些代码放在一个if代码块中,仅当直接运行该文件时,它们才会执行。
# 如果此时运行alien_invasion.py,将看到一个空的Pygame窗口。

新增方法_check_events(),并将检查玩家是否单击了关闭窗口按钮的代码移到该方法中。

要调用当前类的方法,可使用句点表示法,并指定变量名self和要调用的方法的名称。我们在run_game()的while循环中调用这个新增的方法。

1.5.2 方法_update_screen()

为进一步简化run_game(),将更新屏幕的代码移到一个名为_update_screen()的方法中:

import sys

import pygame

from settings import Settings
from ship import Ship


# 导入模块sys和pygame。模块pygame包含开发游戏所需的功能。玩家退出时,我们将使用模块sys中的工具来退出游戏


class AlienInvasion:
    """管理游戏资源和行为的类"""

    def __init__(self):
        """初始化游戏并创建游戏资源"""
        pygame.init()  # 初始化背景设置
        self.settings = Settings()
        self.screen = pygame.display.set_mode((self.settings.screen_width,
                                               self.settings.screen_height))  # 创建一个显示窗口,将这个显示窗口赋给属性self.screen,让这个类中的所有方法都能够使用它
        pygame.display.set_caption("Alien Invasion")
        # 设置背景色
        self.bg_color = (230, 230, 230)

        self.ship = Ship(self)

    def run_game(self):
        """开始游戏的主循环"""
        while True:
            # 监视键盘和鼠标事件
            self._check_events()

            # 每次循环时都重绘屏幕
            self._update_screen()

            # 让最近绘制的屏幕可见
            pygame.display.flip()  # 每次执行while循环时都绘制一个空屏幕,并擦去旧屏幕,使得只有新屏幕可见

    def _check_events(self):
        """相应按键和鼠标事件"""
        for event in pygame.event.get():
            if event.type == pygame.QUIT:  # 当玩家单击游戏窗口的关闭按钮时,将检测到pygame.QUIT事件,进而调用sys.exit()来退出游戏
                sys.exit()

    def _update_screen(self):
        """更新屏幕图像,并切换到屏幕"""
        self.screen.fill(self.settings.bg_color)
        self.ship.blitme()


if __name__ == '__main__':
    # 创建游戏实例并运行游戏
    ai = AlienInvasion()
    ai.run_game()

# 在这个文件末尾,创建一个游戏实例并调用run_game()。这些代码放在一个if代码块中,仅当直接运行该文件时,它们才会执行。
# 如果此时运行alien_invasion.py,将看到一个空的Pygame窗口。

我们将绘制背景和飞船以及切换屏幕的代码移到了方_update_screen()中。现在,run_game()中的主循环简单多了,很容易看出在每次循环中都检测了新发生的事件并更新了屏幕。

如果开发过大量的游戏,可能早就开始像这样将代码放到不同的方法中了。不过如果从未开发过这样的项目,可能不知道如何组织代码。这里采用的做法是,先编写可行的代码,等代码越来越复杂时再进行重构,以展示真正的开发过程:先编写尽可能简单的代码,等项目越来越复杂后对其进行重构。

对代码进行重构使其更容易扩展后,可以开始处理游戏的动态方面了!

1.6 驾驶飞船

下面来让玩家能够左右移动飞船。我们将编写代码,在用户按左或右箭头键时做出响应。
我们将首先专注于向右移动,再使用同样的原理来控制向左移动。

1.6.1 响应按键

每当用户按键时,都将在Pygame中注册一个事件。事件都是通过方法
pygame.event.get()获取的,因此需要在方法_check_events()中指定要检查哪些类型的事件。每次按键都被注册为一个KEYDOWN事件。

Pygame检测到KEYDOWN事件时,需要检查按下的是否是触发行动的键。例如,如果玩家按下的是右箭头键,就增大飞船的rect.centerx值,将飞船向右移动:

def _check_events(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

在方法_check_events()中,为事件循环添加一个elif代码块,以便在Pygame检测到KEYDOWN事件时做出响应。我们检查按下键(event.key)是否是右箭头键(pygame.K_RIGHT)。如果是,就将self.ship.rect.centerx的值加1,从而将飞船向右移动。

如果现在运行alien_invasion.py,则每按右箭头键一次,飞船都将向右移动1像素。这是一个开端,但并非控制飞船的高效方式。下面来改进控制方式,允许持续移动。

1.6.2 允许持续移动

玩家按住右箭头键不放时,我们希望飞船不断向右移动,直到玩家松开为止。我们将让游戏检测pygame.KEYUP事件,以便知道玩家何时松开右箭头键。然后,结合使用KEYDOWN和KEYUP事件以及一个名为moving_right的标志来实现持续移动。

当标志moving_right为False时,飞船不会移动。玩家按下右箭头键时,我们将该标志设置为True,在玩家松开时将该标志重新设置为False。

飞船的属性都由Ship类控制,因此要给这个类添加一个名为moving_right的属性和一个名为update()的方法。方法update()检查标志moving_right的状态。如果该标志为True,就调整飞船的位置。我们将在while循环中调用这个方法,以调整飞船的位置。

下面是对Ship类所做的修改:

import pygame


class Ship:
    """管理飞船类"""

    def __init__(self, ai_game):
        """初始化飞船并设置其初始设置"""
        self.screen = ai_game.screen
        self.screen_rect = ai_game.screen.get_rect()

        # 加载飞船图像并获取其外接矩形
        self.image = pygame.image.load('images/ship.bmp')
        self.rect = self.image.get_rect()

        # 对于每艘新飞船,都将其放在屏幕底部中央
        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):
        """在指定位置绘制飞船"""
        self.screen.blit(self.image, self.rect)

在方法__init__()中,添加属性self.moving_right,并将其初始值设置为False。接下来,添加方法update(),在前述标志为True时向右移动飞船。方法update()将通过Ship实例来调用,因此不是辅助方法。
接下来,需要修改_check_events(),使其在玩家按下右箭头键时将moving_right设置为True,并在玩家松开时将moving_right设置为False。

最后,需要修改run_game()中的while循环,以便每次执行循环时都调用飞船的方法update():

import sys

import pygame

from settings import Settings
from ship import Ship


# 导入模块sys和pygame。模块pygame包含开发游戏所需的功能。玩家退出时,我们将使用模块sys中的工具来退出游戏


class AlienInvasion:
    """管理游戏资源和行为的类"""

    def __init__(self):
        """初始化游戏并创建游戏资源"""
        pygame.init()  # 初始化背景设置
        self.settings = Settings()
        self.screen = pygame.display.set_mode((self.settings.screen_width,
                                               self.settings.screen_height))  # 创建一个显示窗口,将这个显示窗口赋给属性self.screen,让这个类中的所有方法都能够使用它
        pygame.display.set_caption("Alien Invasion")
        # 设置背景色
        self.bg_color = (230, 230, 230)

        self.ship = Ship(self)

    def run_game(self):
        """开始游戏的主循环"""
        while True:
            # 监视键盘和鼠标事件
            self._check_events()
            self.ship.update()
            # 每次循环时都重绘屏幕
            self._update_screen()

            # 让最近绘制的屏幕可见
            pygame.display.flip()  # 每次执行while循环时都绘制一个空屏幕,并擦去旧屏幕,使得只有新屏幕可见

    def _check_events(self):
        """相应按键和鼠标事件"""
        for event in pygame.event.get():
            if event.type == pygame.QUIT:  # 当玩家单击游戏窗口的关闭按钮时,将检测到pygame.QUIT事件,进而调用sys.exit()来退出游戏
                sys.exit()
            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

    def _update_screen(self):
        """更新屏幕图像,并切换到屏幕"""
        self.screen.fill(self.settings.bg_color)
        self.ship.blitme()


if __name__ == '__main__':
    # 创建游戏实例并运行游戏
    ai = AlienInvasion()
    ai.run_game()

# 在这个文件末尾,创建一个游戏实例并调用run_game()。这些代码放在一个if代码块中,仅当直接运行该文件时,它们才会执行。
# 如果此时运行alien_invasion.py,将看到一个空的Pygame窗口。

飞船的位置将在检测到键盘事件后(但在更新屏幕前)更新。这样,玩家输入时,飞船的位置将更新,从而确保使用更新后的位置将飞船绘制到屏幕上。

如果现在运行alien_invasion.py并按住右箭头键,飞船将持续向右移动,直到松开为止。

1.6.3 左右移动

现在飞船能够持续向右移动了,添加向左移动的逻辑也很容易。我们将再次修改Ship类
和方法_check_events()。下面显示了对Ship类的方法__init__()和update()所做的相关修改:

import pygame


class Ship:
    """管理飞船类"""

    def __init__(self, ai_game):
        """初始化飞船并设置其初始设置"""
        self.screen = ai_game.screen
        self.screen_rect = ai_game.screen.get_rect()

        # 加载飞船图像并获取其外接矩形
        self.image = pygame.image.load('images/ship.bmp')
        self.rect = self.image.get_rect()

        # 对于每艘新飞船,都将其放在屏幕底部中央
        self.rect.midbottom = self.screen_rect.midbottom

        # 移动标志
        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
            
    def blitme(self):
        """在指定位置绘制飞船"""
        self.screen.blit(self.image, self.rect)

在方法__init__()中,添加标志self.moving_left。在方法update()中,添加一个if代码块而不是elif代码块,这样如果玩家同时按下了左右箭头键,将先增加再减少飞船的rect.x值,即飞船的位置保持不变。如果使用一个elif代码块来处理向左移动的情况,右箭头键将始终处于优先地位。从向左移动切换到向右移动时,玩家可能同时按住左右箭头键,此时前面的做法让移动更准确。

还需对_check_events()做两方面的调整:

import sys

import pygame

from settings import Settings
from ship import Ship


# 导入模块sys和pygame。模块pygame包含开发游戏所需的功能。玩家退出时,我们将使用模块sys中的工具来退出游戏


class AlienInvasion:
    """管理游戏资源和行为的类"""

    def __init__(self):
        """初始化游戏并创建游戏资源"""
        pygame.init()  # 初始化背景设置
        self.settings = Settings()
        self.screen = pygame.display.set_mode((self.settings.screen_width,
                                               self.settings.screen_height))  # 创建一个显示窗口,将这个显示窗口赋给属性self.screen,让这个类中的所有方法都能够使用它
        pygame.display.set_caption("Alien Invasion")
        # 设置背景色
        self.bg_color = (230, 230, 230)

        self.ship = Ship(self)

    def run_game(self):
        """开始游戏的主循环"""
        while True:
            # 监视键盘和鼠标事件
            self._check_events()
            self.ship.update()
            # 每次循环时都重绘屏幕
            self._update_screen()

            # 让最近绘制的屏幕可见
            pygame.display.flip()  # 每次执行while循环时都绘制一个空屏幕,并擦去旧屏幕,使得只有新屏幕可见

    def _check_events(self):
        """相应按键和鼠标事件"""
        for event in pygame.event.get():
            if event.type == pygame.QUIT:  # 当玩家单击游戏窗口的关闭按钮时,将检测到pygame.QUIT事件,进而调用sys.exit()来退出游戏
                sys.exit()
            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

    def _update_screen(self):
        """更新屏幕图像,并切换到屏幕"""
        self.screen.fill(self.settings.bg_color)
        self.ship.blitme()


if __name__ == '__main__':
    # 创建游戏实例并运行游戏
    ai = AlienInvasion()
    ai.run_game()

# 在这个文件末尾,创建一个游戏实例并调用run_game()。这些代码放在一个if代码块中,仅当直接运行该文件时,它们才会执行。
# 如果此时运行alien_invasion.py,将看到一个空的Pygame窗口。

如果因玩家按下K_LEFT键而触发了KEYDOWN事件,就将moving_left设置为True。如果因玩家松开K_LEFT而触发了KEYUP事件,就将moving_left设置为False。这里之所以可以使用elif代码块,是因为每个事件都只与一个键相关联。如果玩家同时按下左右箭头键,将检测到两个不同的事件。

如果此时运行alien_invasion.py,将能够持续左右移动飞船。如果同时按下左右箭头键,飞船将纹丝不动。

下面来进一步优化飞船的移动方式:调整飞船的速度,以及限制飞船的移动距离,以免其消失在屏幕之外。

1.6.4 调整飞船的速度

当前,每次执行while循环时,飞船最多移动1像素,但可在Settings类中添加属性ship_speed,用于控制飞船的速度。我们将根据这个属性决定飞船在每次循环时最多移动多远。下面演示了如何在settings.py中添加这个新属性:

class Settings:
    """存储游戏中所有设置的类"""
    def __init__(self):
        """初始化游戏设置"""
        self.screen_width = 1200
        self.screen_height = 800
        self.bg_color = (230, 230, 230)

        # 飞船设置
        self.ship_speed = 1.5 # 将ship_speed的初始值设置为1.5。现在需要移动飞船时,每次循环将移动1.5像素而不是1像素。

将ship_speed的初始值设置为1.5。现在需要移动飞船时,每次循环将移动1.5像素而不是1像素。

通过将速度设置指定为小数值,可在后面加快游戏节奏时更细致地控制飞船的速度。然而,rect的x等属性只能存储整数值,因此需要对Ship类做些修改:

import pygame


class Ship:
    """管理飞船类"""

    def __init__(self, ai_game):
        """初始化飞船并设置其初始设置"""
        self.screen = ai_game.screen
        self.settings = ai_game.settings
        self.screen_rect = ai_game.screen.get_rect()

        # 加载飞船图像并获取其外接矩形
        self.image = pygame.image.load('images/ship.bmp')
        self.rect = self.image.get_rect()

        # 对于每艘新飞船,都将其放在屏幕底部中央
        self.rect.midbottom = self.screen_rect.midbottom

        # 在飞船中的属性x中存储小数值
        self.x = float(self.rect.x)
        
        # 移动标志
        self.moving_right = False
        self.moving_left = False

    def update(self):
        """根据移动标志调整飞船位置"""
        # 更新飞船而不是rect对象的值
        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):
        """在指定位置绘制飞船"""
        self.screen.blit(self.image, self.rect)

现在可以修改ship_speed的值了。只要它的值大于1,飞船的移动速度就会比以前更快。
这有助于让飞船的反应速度足够快,以便射杀外星人,还让我们能够随着游戏的进行加快游戏的节奏。

1.6.5 限制飞船的活动范围

当前,如果玩家按住箭头键的时间足够长,飞船将飞到屏幕之外,消失得无影无踪。
下面来修复这种问题,让飞船到达屏幕边缘后停止移动。为此,将修改Ship类的方法update():

import pygame


class Ship:
    """管理飞船类"""

    def __init__(self, ai_game):
        """初始化飞船并设置其初始设置"""
        self.screen = ai_game.screen
        self.settings = ai_game.settings
        self.screen_rect = ai_game.screen.get_rect()

        # 加载飞船图像并获取其外接矩形
        self.image = pygame.image.load('images/ship.bmp')
        self.rect = self.image.get_rect()

        # 对于每艘新飞船,都将其放在屏幕底部中央
        self.rect.midbottom = self.screen_rect.midbottom

        # 在飞船中的属性x中存储小数值
        self.x = float(self.rect.x)

        # 移动标志
        self.moving_right = False
        self.moving_left = False

    def update(self):
        """根据移动标志调整飞船位置"""
        # 更新飞船而不是rect对象的值
        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

    def blitme(self):
        """在指定位置绘制飞船"""
        self.screen.blit(self.image, self.rect)

上述代码在修改self.x的值之前检查飞船的位置。self.rect.right返回飞船外接矩形右边缘的 坐标。如果这个值小于self.screen_rect.right的值,就说明飞船未触及屏幕右边缘。左边缘的情况与此类似:如果rect左边缘的 坐标大于零,就说明飞船未触及屏幕左边缘。这确保仅当飞船在屏幕内时,才调整self.x的值。

1.6.6 重构_check_events()

随着游戏的开发,方法_check_events()将越来越长。因此将其部分代码放在两个方法中,其中一个处理KEYDOWN事件,另一个处理KEYUP事件:

import sys

import pygame

from settings import Settings
from ship import Ship


# 导入模块sys和pygame。模块pygame包含开发游戏所需的功能。玩家退出时,我们将使用模块sys中的工具来退出游戏


class AlienInvasion:
    """管理游戏资源和行为的类"""

    def __init__(self):
        """初始化游戏并创建游戏资源"""
        pygame.init()  # 初始化背景设置
        self.settings = Settings()
        self.screen = pygame.display.set_mode((self.settings.screen_width,
                                               self.settings.screen_height))  # 创建一个显示窗口,将这个显示窗口赋给属性self.screen,让这个类中的所有方法都能够使用它
        pygame.display.set_caption("Alien Invasion")
        # 设置背景色
        self.bg_color = (230, 230, 230)

        self.ship = Ship(self)

    def run_game(self):
        """开始游戏的主循环"""
        while True:
            # 监视键盘和鼠标事件
            self._check_events()
            self.ship.update()
            # 每次循环时都重绘屏幕
            self._update_screen()

            # 让最近绘制的屏幕可见
            pygame.display.flip()  # 每次执行while循环时都绘制一个空屏幕,并擦去旧屏幕,使得只有新屏幕可见

    def _check_events(self):
        """相应按键和鼠标事件"""
        for event in pygame.event.get():
            if event.type == pygame.QUIT:  # 当玩家单击游戏窗口的关闭按钮时,将检测到pygame.QUIT事件,进而调用sys.exit()来退出游戏
                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):
        """响应按键"""
        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):
        """响应松开"""
        if event.key == pygame.K_RIGHT:
            self.ship.moving_right = False
        elif event.key == pygame.K_LEFT:
            self.ship.moving_left = False

    def _update_screen(self):
        """更新屏幕图像,并切换到屏幕"""
        self.screen.fill(self.settings.bg_color)
        self.ship.blitme()


if __name__ == '__main__':
    # 创建游戏实例并运行游戏
    ai = AlienInvasion()
    ai.run_game()

# 在这个文件末尾,创建一个游戏实例并调用run_game()。这些代码放在一个if代码块中,仅当直接运行该文件时,它们才会执行。
# 如果此时运行alien_invasion.py,将看到一个空的Pygame窗口。

我们创建了两个新的辅助方法:_check_keydown_events()和_check_keyup_events()。它们都包含形参self和event。这两个方法的代码是从_check_events()中复制而来的,因此将方法_check_events()中相应的代码替换成了对这两个新方法的调用。现在,方法_check_events()更简单,代码结构也更清晰,在其中响应玩家输入时将更容易。

1.6.7 按Q键退出

能够高效地响应按键后,我们来添加另一种退出游戏的方式。当前,每次测试新功能时,都需要单击游戏窗口顶部的X按钮来结束游戏,实在是太麻烦了。
因此,我们来添加一个结束游戏的键盘快捷键—— Q键:

    def _check_keydown_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
        elif event.key == pygame.K_q:   # 按Q退出游戏
            sys.exit()

在_check_keydown_events()中,添加一个代码块,用于在玩家按Q键时结束游戏。现在测试该游戏时,你可按Q键来结束游戏,而无须使用鼠标将窗口关闭。

1.6.8 在全屏模式下运行游戏

Pygame支持全屏模式,你可能会更喜欢在这种模式下而非常规窗口中运行游戏。有些游戏在全屏模式下看起来更舒服,而在macOS系统中用全屏模式运行会提升性能。

要在全屏模式下运行该游戏,可在__init__()中做如下修改:

        def __init__(self):
        """初始化游戏并创建游戏资源"""
        pygame.init()  # 初始化背景设置
        self.settings = Settings()

        # 固定窗口大小运行游戏
        self.screen = pygame.display.set_mode((self.settings.screen_width,
                                               self.settings.screen_height))  # 创建一个显示窗口,将这个显示窗口赋给属性self.screen,让这个类中的所有方法都能够使用它
        
        # 全屏运行游戏
        # 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")
        # 设置背景色
        self.bg_color = (230, 230, 230)

        self.ship = Ship(self)

创建屏幕时,传入了尺寸(0, 0)以及参数pygame.FULLSCREEN。这让Pygame生成一个覆盖整个显示器的屏幕。由于无法预先知道屏幕的宽度和高度,要在创建屏幕后更新这些设置:使用屏幕的rect的属性width和height来更新对象settings。

如果你喜欢这款游戏在全屏模式下的外观和行为,请保留这些设置。如果你更喜欢这款游戏在独立的窗口中运行,可恢复到原来采用的方法——将屏幕尺寸设置为特定的值。

注意 在全屏模式下运行这款游戏之前,请确认能够按Q键退出,因为Pygame默认不提供在全屏模式下退出游戏的方式。

1.7 简单回顾

1.7.1 alien_invasion.py

主文件alien_invasion.py包含AlienInvasion类。这个类创建一系列贯穿整个游戏都要用到的属性:赋给self.settings的设置,赋给screen中的主显示surface,以及一个飞船实例。这个模块还包含游戏的主循环,即一个调用_check_events()、ship.update()和_update_screen()的while循环。

方法_check_events()检测相关的事件(如按下和松开键盘),并通过调用方法_check_keydown_events()和_check_keyup_events()处理这些事件。当前,这些方法负责管理飞船的移动。AlienInvasion类还包含方法_update_screen(),该方法在每次主循环中重绘屏幕。

要玩游戏《外星人入侵》,只需运行文件alien_invasion.py,其他文件(settings.py和ship.py)包含的代码会被导入这个文件中。

1.7.2 settings.py

文件settings.py包含Settings类,这个类只包含方法__init__(),用于初始化控制游戏外观和飞船速度的属性。

1.7.3 ship.py

文件ship.py包含Ship类,这个类包含方法__init__()、管理飞船位置的方法update()和在屏幕上绘制飞船的方法blitme()。表示飞船的图像存储在文件夹images下的文件ship.bmp中。

1.8 射击

下面来添加射击功能。我们将编写在玩家按空格键时发射子弹(用小矩形表示)的代码。子弹将在屏幕中向上飞行,抵达屏幕上边缘后消失。

1.8.1 添加子弹设置

首先,更新settings.py,在方法__init__()末尾存储新类Bullet所需的值:

lass Settings:
    """存储游戏中所有设置的类"""
    def __init__(self):
        """初始化游戏设置"""
        self.screen_width = 1200
        self.screen_height = 800
        self.bg_color = (230, 230, 230)

        # 飞船设置
        self.ship_speed = 1.5 # 将ship_speed的初始值设置为1.5。现在需要移动飞船时,每次循环将移动1.5像素而不是1像素。

        # 子弹设置
        self.bullet_speed = 1.0
        self.bullet_width = 3
        self.bullet_height = 15
        self.bullet_color = (60, 60, 60)

这些设置创建宽3像素、高15像素的深灰色子弹。子弹的速度比飞船稍低。

1.8.2 创建Bullet类

下面来创建存储Bullet类的文件bullet.py,其前半部分如下:

import pygame
from pygame.sprite import Sprite

class Bullet(Sprite):
    """管理飞船所发射子弹的类"""
    def __init__(self, ai_game):
        """在飞船当前位置创建一个子弹对象"""
        super().__init__()
        self.screen = ai_game.screen
        self.settings = ai_game.settings
        self.color = self.settings.bullet_color

        # 在(0, 0)出创建一个表示子弹的矩形,再设置正确的位置
        self.rect = pygame.Rect(0, 0, self.settings.bullet_width, self.settings.bullet_height)
        self.rect.midtop = ai_game.ship.rect.midtop

        # 存储用小数表示的子弹位置
        self.y = float(self.y)

Bullet类继承了从模块pygame.sprite导入的Sprite类。通过使用精灵(sprite),可将游戏中相关的元素编组,进而同时操作编组中的所有元素。为创建子弹实例,init()需要当前的AlienInvasion实例,我们还调用了super()来继承Sprite。另外,我们还定义了用于存储屏幕以及设置对象和子弹颜色的属性。

创建子弹的属性rect。子弹并非基于图像,因此必须使用pygame.Rect()类从头开始创建一个矩形。创建这个类的实例时,必须提供矩形左上角的 坐标和 坐标,以及矩形的宽度和高度。我们在(0, 0)处创建这个矩形,但下一行代码将其移到了正确的位置,因为子弹的初始位置取决于飞船当前的位置。子弹的宽度和高度是从self.settings中获取的。

将子弹的rect.midtop设置为飞船的rect.midtop。这样子弹将从飞船顶部出发,看起来像是从飞船中射出的。我们将子弹的 坐标存储为小数值,以便能够微调子弹的速度。

下面是bullet.py的第二部分,包括方法update()和draw_bullet():

    def update(self):
        """根据移动标志调整飞船位置"""
        # 更新飞船而不是rect对象的值
        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

    def blitme(self):
        """在指定位置绘制飞船"""
        self.screen.blit(self.image, self.rect)

方法update()管理子弹的位置。发射出去后,子弹向上移动,意味着其 坐标将不断减小。为更新子弹的位置,从self.y中减去settings .bullet_speed的值。接下来,将self.rect.y设置为self.y的值。

属性bullet_speed让我们能够随着游戏的进行或根据需要提高子弹的速度,以调整游戏的行为。子弹发射后,其 坐标始终不变,因此子弹将沿直线垂直向上飞行。

需要绘制子弹时,我们调用draw_bullet()。draw.rect()函数使用存储在self.color中的颜色填充表示子弹的rect占据的屏幕部分。

1.8.3 将子弹存储到编组中

定义Bullet类和必要的设置后,便可编写代码在玩家每次按空格键时都射出一发子弹了。我们将在AlienInvasion中创建一个编组(group),用于存储所有有效的子弹,以便管理发射出去的所有子弹。这个编组是pygame.sprite.Group类的一个实
例。
pygame.sprite.Group类似于列表,但提供了有助于开发游戏的额外功能。在主循环中,将使用这个编组在屏幕上绘制子弹以及更新每颗子弹的位置。

首先,在__init__()中创建用于存储子弹的编组:

# 子弹
        self.ship = Ship(self)
        self.bullets = pygame.sprite.Group()

然后在while循环中更新子弹的位置:

    def run_game(self):
        """开始游戏的主循环"""
        while True:
            # 监视键盘和鼠标事件
            self._check_events()
            self.ship.update()
            # 更新子弹位置
            self.bullets.update()
            self._update_screen()
            # 每次循环时都重绘屏幕
            self._update_screen()

            # 让最近绘制的屏幕可见
            pygame.display.flip()  # 每次执行while循环时都绘制一个空屏幕,并擦去旧屏幕,使得只有新屏幕可见

1.8.4 开火

在AlienInvasion中,需要修改_check_keydown_events(),以便在玩家按空格键时发射一颗子弹。无须修改_check_keyup_events(),因为玩家松开空格键时什么都不会发生。还需要修改_update_screen(),确保在调用flip()前在屏幕上重绘每颗子弹。

为发射子弹,需要做的工作不少,因此编写一个新方法_fire_bullet()来完成这项任务:

    import sys

import pygame

from settings import Settings
from ship import Ship
from bullet import Bullet

# 导入模块sys和pygame。模块pygame包含开发游戏所需的功能。玩家退出时,我们将使用模块sys中的工具来退出游戏


class AlienInvasion:
    """管理游戏资源和行为的类"""

    def __init__(self):
        """初始化游戏并创建游戏资源"""
        pygame.init()  # 初始化背景设置
        self.settings = Settings()

        # 固定窗口大小运行游戏
        self.screen = pygame.display.set_mode((self.settings.screen_width,
                                               self.settings.screen_height))  # 创建一个显示窗口,将这个显示窗口赋给属性self.screen,让这个类中的所有方法都能够使用它

        # 全屏运行游戏
        # 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")
        # 设置背景色
        self.bg_color = (230, 230, 230)

        self.ship = Ship(self)

        # 子弹
        self.ship = Ship(self)
        self.bullets = pygame.sprite.Group()
    def run_game(self):
        """开始游戏的主循环"""
        while True:
            # 监视键盘和鼠标事件
            self._check_events()
            self.ship.update()
            # 更新子弹位置
            self.bullets.update()
            self._update_screen()
            # 每次循环时都重绘屏幕
            self._update_screen()

            # 让最近绘制的屏幕可见
            pygame.display.flip()  # 每次执行while循环时都绘制一个空屏幕,并擦去旧屏幕,使得只有新屏幕可见

    def _check_events(self):
        """相应按键和鼠标事件"""
        for event in pygame.event.get():
            if event.type == pygame.QUIT:  # 当玩家单击游戏窗口的关闭按钮时,将检测到pygame.QUIT事件,进而调用sys.exit()来退出游戏
                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):
        """响应按键"""
        if event.key == pygame.K_RIGHT:
            # 向右移动飞船
            self.ship.moving_right = True
        elif event.key == pygame.K_LEFT:
            # 向左移动飞船
            self.ship.moving_left = True
        elif event.key == pygame.K_q:   # 按Q退出游戏
            sys.exit()
        elif event.key == pygame.K_SPACE:   # 按空格发射子弹
            self._fire_bullet()

    def _check_keyup_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

    def _fire_bullet(self):
        """创建一颗子弹,并将其加入编组bullets中"""
        new_bullet = Bullet(self)
        self.bullets.add(new_bullet)

    def _update_screen(self):
        """更新屏幕图像,并切换到屏幕"""
        self.screen.fill(self.settings.bg_color)
        self.ship.blitme()
        for bullet in self.bullets.sprites():
            bullet.draw_bullet()
        pygame.display.flip()


if __name__ == '__main__':
    # 创建游戏实例并运行游戏
    ai = AlienInvasion()
    ai.run_game()

# 在这个文件末尾,创建一个游戏实例并调用run_game()。这些代码放在一个if代码块中,仅当直接运行该文件时,它们才会执行。
# 如果此时运行alien_invasion.py,将看到一个空的Pygame窗口。

如果此时运行alien_invasion.py,将能够左右移动飞船,并发射任意数量的子弹。子弹在屏幕上向上飞行,抵达屏幕顶部后消失得无影无踪。

1.8.5 删除消失的子弹

当前,子弹在抵达屏幕顶端后消失,但这仅仅是因为Pygame无法在屏幕外面绘制它们。这些子弹实际上依然存在,其 坐标为负数且越来越小。这是个问题,因为它们将继续消耗内存和处理能力。

需要将这些消失的子弹删除,否则游戏所做的无谓工作将越来越多,进而变得越来越慢。为此,需要检测表示子弹的rect的bottom属性是否为零。如果是,则表明子弹已飞过屏幕顶端:

    def run_game(self):
        """开始游戏的主循环"""
        while True:
            # 监视键盘和鼠标事件
            self._check_events()
            self.ship.update()
            # 更新子弹位置
            self.bullets.update()
            # 删除消失的子弹
            for bullet in self.bullets.copy():
                if bullet.rect.bottom <= 0:
                    self.bullets.remove(bullet)
            print(len(self.bullets))
            
            self._update_screen()
            # 每次循环时都重绘屏幕
            self._update_screen()

            # 让最近绘制的屏幕可见
            pygame.display.flip()  # 每次执行while循环时都绘制一个空屏幕,并擦去旧屏幕,使得只有新屏幕可见

使用for循环遍历列表(或Pygame编组)时,Python要求该列表的长度在整个循环中保持不变。因为不能从for循环遍历的列表或编组中删除元素,所以必须遍历编组的副本。我们使用方法copy()来设置for循环,从而能够在循环中修改bullets。检查
每颗子弹,看看它是否从屏幕顶端消失。如果是,就将其从bullets中删除。使用函数调用print()显示当前还有多少颗子弹,以核实确实删除了消失的子弹。

如果这些代码没有问题,我们发射子弹后查看终端窗口时,将发现随着子弹一颗颗地在屏幕顶端消失,子弹数将逐渐降为零。运行该游戏并确认子弹被正确删除后,请将这个函数调用print()删除。如果不删除,游戏的速度将大大降低,因为将输出写入终端花费的时间比将图形绘制到游戏窗口花费的时间还要多。

1.8.6 限制子弹数量

很多射击游戏对可同时出现在屏幕上的子弹数量进行了限制,以鼓励玩家有目标地射击。
下面在游戏《外星人入侵》中做这样的限制。
首先,在settings.py中存储最大子弹数:

class Settings:
    """存储游戏中所有设置的类"""
    def __init__(self):
        """初始化游戏设置"""
        self.screen_width = 1200
        self.screen_height = 800
        self.bg_color = (230, 230, 230)

        # 飞船设置
        self.ship_speed = 1.5 # 将ship_speed的初始值设置为1.5。现在需要移动飞船时,每次循环将移动1.5像素而不是1像素。

        # 子弹设置
        self.bullet_speed = 1.0
        self.bullet_width = 3
        self.bullet_height = 15
        self.bullet_color = (60, 60, 60)
        self.bullet_allowed = 3

这将未消失的子弹数限制为三颗。

    def _fire_bullet(self):
        """创建一颗子弹,并将其加入编组bullets中"""
        if len(self.bullets) < self.settings.bullet_allowed:
            new_bullet = Bullet(self)
            self.bullets.add(new_bullet)

玩家按空格键时,我们检查bullets的长度。如果len(bullets)小于3,就创建一颗新子弹;但如果有三颗未消失的子弹,则玩家按空格键时什么都不会发生。如果现在运行这个游戏,屏幕上最多只能有三颗子弹。

1.8.7 创建方法_update_bullets()

编写并检查子弹管理代码后,可将其移到一个独立的方法中,确保AlienInvasion类组织有序。为此,创建一个名为_update_bullets()的新方法,并将其放在_update_screen()前面:

import sys

import pygame

from settings import Settings
from ship import Ship
from bullet import Bullet

# 导入模块sys和pygame。模块pygame包含开发游戏所需的功能。玩家退出时,我们将使用模块sys中的工具来退出游戏


class AlienInvasion:
    """管理游戏资源和行为的类"""

    def __init__(self):
        """初始化游戏并创建游戏资源"""
        pygame.init()  # 初始化背景设置
        self.settings = Settings()

        # 固定窗口大小运行游戏
        self.screen = pygame.display.set_mode((self.settings.screen_width,
                                               self.settings.screen_height))  # 创建一个显示窗口,将这个显示窗口赋给属性self.screen,让这个类中的所有方法都能够使用它

        # 全屏运行游戏
        # 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")
        # 设置背景色
        self.bg_color = (230, 230, 230)

        self.ship = Ship(self)

        # 子弹
        self.ship = Ship(self)
        self.bullets = pygame.sprite.Group()
    def run_game(self):
        """开始游戏的主循环"""
        while True:
            # 监视键盘和鼠标事件
            self._check_events()
            self.ship.update()
            self._update_bullets()

            # 每次循环时都重绘屏幕
            self._update_screen()

            # 让最近绘制的屏幕可见
            pygame.display.flip()  # 每次执行while循环时都绘制一个空屏幕,并擦去旧屏幕,使得只有新屏幕可见

    def _check_events(self):
        """相应按键和鼠标事件"""
        for event in pygame.event.get():
            if event.type == pygame.QUIT:  # 当玩家单击游戏窗口的关闭按钮时,将检测到pygame.QUIT事件,进而调用sys.exit()来退出游戏
                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):
        """响应按键"""
        if event.key == pygame.K_RIGHT:
            # 向右移动飞船
            self.ship.moving_right = True
        elif event.key == pygame.K_LEFT:
            # 向左移动飞船
            self.ship.moving_left = True
        elif event.key == pygame.K_q:   # 按Q退出游戏
            sys.exit()
        elif event.key == pygame.K_SPACE:   # 按空格发射子弹
            self._fire_bullet()

    def _check_keyup_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

    def _fire_bullet(self):
        """创建一颗子弹,并将其加入编组bullets中"""
        if len(self.bullets) < self.settings.bullet_allowed:
            new_bullet = Bullet(self)
            self.bullets.add(new_bullet)
    def _update_bullets(self):
        """更新子弹位置并删除消失的子弹"""
        # 更新子弹位置
        self.bullets.update()
        # 删除消失的子弹
        for bullet in self.bullets.copy():
            if bullet.rect.bottom <= 0:
                self.bullets.remove(bullet)
        
    def _update_screen(self):
        """更新屏幕图像,并切换到屏幕"""
        self.screen.fill(self.settings.bg_color)
        self.ship.blitme()
        for bullet in self.bullets.sprites():
            bullet.draw_bullet()
        pygame.display.flip()


if __name__ == '__main__':
    # 创建游戏实例并运行游戏
    ai = AlienInvasion()
    ai.run_game()

# 在这个文件末尾,创建一个游戏实例并调用run_game()。这些代码放在一个if代码块中,仅当直接运行该文件时,它们才会执行。
# 如果此时运行alien_invasion.py,将看到一个空的Pygame窗口。

我们让主循环包含尽可能少的代码,这样只要看方法名就能迅速知道游戏中发生的情况。主循环检查玩家的输入,并更新飞船的位置和所有未消失子弹的位置。然后,使用更新后的位置来绘制新屏幕。

2. 外星人来了

这里将在游戏《外星人入侵》中添加外星人。我们将首先在屏幕上边缘附近添加一个外星人,再生成一群外星人。然后让这群外星人向两边和下面移动,并删除被子弹击中的外星人。最后,显示玩家拥有的飞船数量,并在玩家的飞船用完后结束游戏。

2.1 创建第一个外星人

在屏幕上放置外星人与放置飞船类似。每个外星人的行为都由Alien类控制,我们将像创建Ship类那样创建这个类。出于简化考虑,也将使用位图来表示外星人。你可以自己寻找表示外星人图片。请务必将选择的图像文件保存到文件夹images中。

2.1.1 创建Alien类

下面来编写Alien类并将其保存为文件alien.py:

import pygame
from pygame.sprite import Sprite


class Alien(Sprite):
    """表示单个外星人的类"""

    def __init__(self, ai_game):
        """初始化外星人并设置其起始位置"""
        super().__init__()
        self.screen = ai_game.screen

        # 加载外星人图像并设置其rect属性
        self.image = pygame.image.load('images/alien.bmp')
        self.rect = self.image.get_rect()

        # 每个外星人最初都在屏幕左上角附近
        self.rect.x = self.rect.width
        self.rect.y = self.rect.height

        # 存储外星人的精确水平位置
        self.x = float(self.rect.x)
        

除位置不同外,这个类的大部分代码与Ship类相似。每个外星人最初都位于屏幕左上角附近。将每个外星人的左边距都设置为外星人的宽度,并将上边距设置为外星人的高度,这样更容易看清。我们主要关心的是外星人的水平速度,因此精确地记录了每个外星人的水平位置。

Alien类不需要一个在屏幕上绘制外星人的方法,因为我们将使用一个Pygame编组方法,自动在屏幕上绘制编组中的所有元素。

2.1.2 创建Alien实例

要让第一个外星人在屏幕上现身,需要创建一个Alien实例。这属于设置工作,因此将把这些代码放在AlienInvasion类的方法__init__()末尾。我们最终会创建一群外星人,涉及的工作量不少,因此将新建一个名为_create_fleet()的辅助方法。

在类中,方法的排列顺序无关紧要,只要按统一的标准排列就行。我们将把_create_fleet()放在_update_screen()前面,不过放在AlienInvasion类的任何地方其实都可行。
首先,需要导入Alien类。
下面是alien_invasion.py中修改后的import语句:

    def __init__(self):
        """初始化游戏并创建游戏资源"""
        pygame.init()  # 初始化背景设置
        self.settings = Settings()

        # 固定窗口大小运行游戏
        self.screen = pygame.display.set_mode((self.settings.screen_width,
                                               self.settings.screen_height))  # 创建一个显示窗口,将这个显示窗口赋给属性self.screen,让这个类中的所有方法都能够使用它

        # 全屏运行游戏
        # 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")
        # 设置背景色
        self.bg_color = (230, 230, 230)

        self.ship = Ship(self)

        # 子弹
        self.ship = Ship(self)
        self.bullets = pygame.sprite.Group()
        # 外星人
        self.alliens = pygame.sprite.Group()
        
        self._create_fleet()

创建了一个用于存储外星人群的编组,还调用了接下来将编写的方法_create_fleet()。
下面是新编写的方法_create_fleet():

    def _create_fleet(self):
        """创建外星人群"""
        # 创建一个外星人
        alien = Alien(self)
        self.alliens.add(alien)

在这个方法中,创建了一个Alien实例,再将其添加到用于存储外星人群的编组中。外星人默认放在屏幕左上角附近,对第一个外星人来说,这样的位置非常合适。
要让外星人现身,需要在_update_screen()中对外星人编组调用方法draw():

    def _update_screen(self):
        """更新屏幕图像,并切换到屏幕"""
        self.screen.fill(self.settings.bg_color)
        self.ship.blitme()
        for bullet in self.bullets.sprites():
            bullet.draw_bullet()
        self.alliens.draw(self.screen)

        pygame.display.flip()

对编组调用draw()时,Pygame将把编组中的每个元素绘制到属性rect指定的位置。方法draw()接受一个参数,这个参数指定了要将编组中的元素绘制到哪个surface上。

2.2 创建一群外星人

要绘制一群外星人,需要确定一行能容纳多少个外星人以及要绘制多少行。我们将首先计算外星人的水平间距并创建一行外星人,再确定可用的垂直空间并创建整群外星人。

2.2.1 确定一行可容纳多少个外星人

为确定一行可容纳多少个外星人,来看看可用的水平空间有多大。屏幕宽度存储在settings.screen_width中,但需要在屏幕两边都留下一定的边距(将其设置为外星人的宽度)。因为有两个边距,所以可用于放置外星人的水平空间为屏幕宽度减去外星人宽度的两倍。

available_space_x = settings.screen_width – (2 * alien_width)

还需要在外星人之间留出一定的空间,不妨将其定为外星人的宽度。因此,显示一个外星人所需的水平空间为外星人宽度的两倍:一个宽度用于放置外星人,另一个宽度为外星人右边的空白区域。为确定一行可容纳多少个外星人,将可用空间除以外星人宽度的两倍。
我们使用整除(floor division)运算符//,它将两个数相除并丢弃余数,让我们得到一个表示外星人个数的整数。

number_aliens_x = available_space_x // (2 * alien_width)

2.2.2 创建一行外星人

现在可以创建整行外星人了。由于创建单个外星人的代码管用,我们重写_create_fleet()使其创建一行外星人:

    def _create_fleet(self):
        """创建外星人群"""
        # 创建一个外星人并计算一行可容纳多少外星人
        # 外星人的间距为外星人的宽度
        alien = Alien(self)
        alien_width = alien.rect.width
        available_space_x = self.settings.screen_width - (2 * alien_width)
        number_aliens_x = available_space_x // (2 * alien_width)

        # 创建一行外星人
        for alien_number in range(number_aliens_x):
            # 创建一个外星人并将其加入当前行
            alien = Alien(self)
            alien.x = alien_width + 2 * alien_width * alien_number
            alien.rect.x = alien.x
            self.alliens.add(alien)

这行外星人在屏幕上稍微偏向了左边、这实际上是有好处的,因为后面将让外星人群往右移,触及屏幕边缘后稍微往下移,再往左移,依此类推。就像经典游戏《太空入侵者》,相比于只往下移,这种移动方式更为有趣。我们将让外星人群不断这样移动,直到所有外星人都被击落,或者有外星人撞上飞船或抵达屏幕底端。

注意 根据所选择的屏幕宽度,在你的系统中,第一个外星人的位置可能稍有不同。

2.2.3 重构_create_fleet()

倘若只需使用前面的代码就能创建外星人群,也许应该让_create_fleet()保持原样,但鉴于创建外星人群的工作还未完成,我们稍微整理一下这个方法。为此,添加辅助方法_create_alien(),并在_create_fleet()中调用它:

 def _create_fleet(self):
        """创建外星人群"""
        # 创建一个外星人并计算一行可容纳多少外星人
        # 外星人的间距为外星人的宽度
        alien = Alien(self)
        alien_width = alien.rect.width
        available_space_x = self.settings.screen_width - (2 * alien_width)
        number_aliens_x = available_space_x // (2 * alien_width)

        # 创建一行外星人
        for alien_number in range(number_aliens_x):
            self._create_alien(alien_number)

    def _create_alien(self, alien_number):
        """创建一个外星人并将其加入当前行"""
        alien = Alien(self)
        alien_width = alien.rect.width
        alien.x = alien_width + 2 * alien_width * alien_number
        alien.rect.x = alien.x
        self.alliens.add(alien)

除self外,方法_create_alien()还接受另一个参数,即要创建的外星人的编号。该方法的代码与_create_fleet()相同,但在内部获取外星人宽度,而不是将其作为参数传入。这样重构后,添加新行进而创建整群外星人将更容易。

2.2.4 添加行

要创建外星人群,需要计算屏幕可容纳多少行,并将创建一行外星人的循环重复执行相应的次数。为计算可容纳的行数,要先计算可用的垂直空间:用屏幕高度减去第一行外星人的上边距(外星人高度)、飞船的高度以及外星人群最初与飞船之间的距离(外星人高度的两倍):

available_space_y = settings.screen_height – (3 * alien_height) – ship_height

这将在飞船上方留出一定的空白区域,给玩家留出射杀外星人的时间。

每行下方都要留出一定的空白区域,不妨将其设置为外星人的高度。为计算可容纳的行数,将可用的垂直空间除以外星人高度的两倍。我们使用整除,因为行数只能是整数。(同样,如果这样的计算不对,我们马上就能发现,继而将间距调整为合理的值。)

number_rows = available_space_y // (2 * alien_height)

知道可容纳多少行之后,便可重复执行创建一行外星人的代码了:

    def _create_fleet(self):
        """创建外星人群"""
        # 创建一个外星人并计算一行可容纳多少外星人
        # 外星人的间距为外星人的宽度
        alien = Alien(self)
        alien_width, alien_height = alien.rect.size
        available_space_x = self.settings.screen_width - (2 * alien_width)
        number_aliens_x = available_space_x // (2 * alien_width)

        # 计算屏幕可容纳多少行外星人
        ship_height = self.ship.rect.height
        available_space_y = (self.settings.screen_height - (3 * alien_height) - ship_height)
        number_rows = available_space_y // (2 * alien_height)

        # 创建外星人群
        for row_number in range(number_rows):
            for alien_number in range(number_aliens_x):
                self._create_alien(alien_number, row_number)

    def _create_alien(self, alien_number, row_number):
        """创建一个外星人并将其加入当前行"""
        alien = Alien(self)
        alien_width, alien_height = alien.rect.size
        alien.x = alien_width + 2 * alien_width * alien_number
        alien.rect.x = alien.x
        alien.rect.y = alien.rect.height + 2 * alien.rect.height * row_number
        self.alliens.add(alien)

需要知道外星人的宽度和高度,因此使用了属性size。该属性是一个元组,包含rect对象的宽度和高度。为计算屏幕可容纳多少行外星人,在计算available_space_x的代码后面添加了计算available_space_y的代码。将计算公式用圆括号括起来,以便将代码分成两行,遵循每行不超过79字符的建议。

为创建多行外星人,使用了两个嵌套在一起的循环:一个外部循环和一个内部循环。内部循环创建一行外星人,而外部循环从零数到要创建的外星人行数:Python将重复执行创建单行外星人的代码,重复次数为number_rows。

为嵌套循环,编写了一个新的for循环,并缩进了要重复执行的代码。(在大多数文本编辑器中,缩进代码块和取消缩进都很容易,详情请参阅附录B)。现在调用_create_alien()时,传递了一个表示行号的实参,将每行都沿屏幕依次向下放置。

在_create_alien()的定义中,需要一个用于存储行号的形参。在_create_alien()中,修改外星人的 坐标并在第一行外星人上方留出与外星人等高的空白区域。相邻外星人行的 坐标相差外星人高度的两倍,因此将外星人高度乘以2,再乘以行号。第一行的行号为0,因此第一行的垂直位置不变,而其他行都沿屏幕依次向下放置。

2.3 让外星人群移动

下面来让外星人群在屏幕上向右移动,撞到屏幕边缘后下移一定的量,再沿相反的方向移动。我们将不断移动所有的外星人,直到外星人被全部消灭,或者有外星人撞上飞船或抵达屏幕底端。下面先让外星人向右移动。

2.3.1 向右移动外星人群

为移动外星人群,将使用alien.py中的方法update()。对于外星人群中的每个外星人,都要调用它。首先,添加一个控制外星人速度的设置:

class Settings:
    """存储游戏中所有设置的类"""
    def __init__(self):
        """初始化游戏设置"""
        self.screen_width = 1200
        self.screen_height = 800
        self.bg_color = (230, 230, 230)

        # 飞船设置
        self.ship_speed = 1.5 # 将ship_speed的初始值设置为1.5。现在需要移动飞船时,每次循环将移动1.5像素而不是1像素。

        # 子弹设置
        self.bullet_speed = 1.0
        self.bullet_width = 3
        self.bullet_height = 15
        self.bullet_color = (60, 60, 60)
        self.bullet_allowed = 3
        
        # 外星人设置
        self.alien_speed = 1.0
        

再使用这个设置来实现update():

import pygame
from pygame.sprite import Sprite


class Alien(Sprite):
    """表示单个外星人的类"""

    def __init__(self, ai_game):
        """初始化外星人并设置其起始位置"""
        super().__init__()
        self.screen = ai_game.screen
        self.settings = ai_game.settings

        # 加载外星人图像并设置其rect属性
        self.image = pygame.image.load('images/alien.bmp')
        self.rect = self.image.get_rect()

        # 每个外星人最初都在屏幕左上角附近
        self.rect.x = self.rect.width
        self.rect.y = self.rect.height

        # 存储外星人的精确水平位置
        self.x = float(self.rect.x)

    def update(self):
        """向右移动外星人"""
        self.x += self.settings.alien_speed
        self.rect.x = self.x

在__init__()中添加了属性settings,以便能够在update()中访问外星人的速度。每次更新外星人时,都将它向右移动,移动量为alien_speed的值。我们使用属性self.x跟踪每个外星人的准确位置,该属性可存储小数值。然后,使用self.x的值来更新外星人的rect的位置。

主while循环中已调用了更新飞船和子弹的方法,现在还需更调用更新每个外星人位置的方法:

    def run_game(self):
        """开始游戏的主循环"""
        while True:
            # 监视键盘和鼠标事件
            self._check_events()
            self.ship.update()
            self._update_bullets()
            self._update_aliens()
            # 每次循环时都重绘屏幕
            self._update_screen()

            # 让最近绘制的屏幕可见
            pygame.display.flip()  # 每次执行while循环时都绘制一个空屏幕,并擦去旧屏幕,使得只有新屏幕可见
            
    def _update_aliens(self):
        """更新外星人群所有外星人位置"""
        self.alliens.update()

对编组调用方法update(),这将自动对每个外星人调用方法update()。如果现在运行这
个游戏,你将看到外星人群向右移动,并在屏幕右边缘消失。

2.3.2 创建表示外星人移动方向的设置

下面来创建让外星人撞到屏幕右边缘后向下移动、再向左移动的设置。实现这种行为的代
码如下:

class Settings:
    """存储游戏中所有设置的类"""
    def __init__(self):
        """初始化游戏设置"""
        self.screen_width = 1200
        self.screen_height = 800
        self.bg_color = (230, 230, 230)

        # 飞船设置
        self.ship_speed = 1.5 # 将ship_speed的初始值设置为1.5。现在需要移动飞船时,每次循环将移动1.5像素而不是1像素。

        # 子弹设置
        self.bullet_speed = 1.0
        self.bullet_width = 3
        self.bullet_height = 15
        self.bullet_color = (60, 60, 60)
        self.bullet_allowed = 3

        # 外星人设置
        self.alien_speed = 1.0
        self.fleet_drop_speed = 10
        # fleet_direction为1表示右移,为-1表示左移
        self.fleet_direction = 1
        

设置fleet_drop_speed指定有外星人撞到屏幕边缘时,外星人群向下移动的速度。将这个速度与水平速度分开是有好处的,便于分别调整这两个速度。

要实现设置fleet_direction,可将其设置为文本值,如’left’或’right’,但这样就必须编写if-elif语句来检查外星人群的移动方向。鉴于只有两个可能的方向,我们使用值1和-1来表示,并在外星人群改变方向时在这两个值之间切换。(向右移时需要增大每个外星人的 坐标,而向左移时需要减小每个外星人的x坐标,因此使用数字来表示方向十分合理。)

2.3.3 检查外星人是否撞到了屏幕边缘

现在需要编写一个方法来检查外星人是否撞到了屏幕边缘,还需修改update()让每个外星人都沿正确的方向移动。这些代码位于Alien类中:

import pygame
from pygame.sprite import Sprite


class Alien(Sprite):
    """表示单个外星人的类"""

    def __init__(self, ai_game):
        """初始化外星人并设置其起始位置"""
        super().__init__()
        self.screen = ai_game.screen
        self.settings = ai_game.settings

        # 加载外星人图像并设置其rect属性
        self.image = pygame.image.load('images/alien.bmp')
        self.rect = self.image.get_rect()

        # 每个外星人最初都在屏幕左上角附近
        self.rect.x = self.rect.width
        self.rect.y = self.rect.height

        # 存储外星人的精确水平位置
        self.x = float(self.rect.x)

    def check_edges(self):
        """如果外星人位于屏幕边缘,返回True"""
        screen_rect = self.screen.get_rect()
        if self.rect.right >= screen_rect.right or self.rect.left <= 0:
            return True

    def update(self):
        """向左或向右移动外星人"""
        self.x += (self.settings.alien_speed * self.settings.fleet_direction)
        self.rect.x = self.x

可对任意外星人调用新方法check_edges(),看其是否位于屏幕左边缘或右边缘。如果外星人的rect的属性right大于或等于屏幕的rect的right属性,就说明外星人位于屏幕右边缘;如果外星人的rect的left属性小于或等于0,就说明外星人位于屏幕左边缘。

我们修改方法update(),将移动量设置为外星人速度和fleet_direction的乘积,让外星人向左或向右移动。如果fleet_direction为1,就将外星人的当前 坐标增大alien_speed,从而将外星人向右移;如果fleet_direction为-1,就将外星人的当前 坐标减去alien_speed,从而将外星人向左移。

2.3.4 向下移动外星人群并改变移动方向

有外星人到达屏幕边缘时,需要将整群外行星下移,并改变它们的移动方向。为此,需要在AlienInvasion中添加一些代码,因为要在这里检查是否有外星人到达了左边缘或右边缘。我们编写方法_check_fleet_edges()和_change_fleet_direction(),并且修改_update_aliens()。这些新方法将放在_create_alien()后面,但其实放在AlienInvasion类中的什么位置都无关紧要:

    def _check_fleet_edges(self):
        """有外星人到达边缘时采取相应的措施"""
        for alien in self.alliens.sprites():
            if alien.check_edges():
                self._change_fleet_direction()
                break
    
    def _change_fleet_direction(self):
        """将整群外星人下移,并改变它们的方向"""
        for alien in self.alliens.sprites():
            alien.rect.y += self.settings.fleet_drop_speed
        self.settings.fleet_direction *= -1

下面显示了对_update_aliens()所做的修改:

 def _update_aliens(self):
        """检查是否有外星人位于屏幕边缘,并更新所有外星人位置"""
        self._check_fleet_edges()
        self.alliens.update()

我们将方法_update_aliens()修改成了先调用_check_fleet_edges(),再更新每个外星人的位置。
如果现在运行这个游戏,外星人群将在屏幕上来回移动,并在抵达屏幕边缘后向下移动。
现在可以开始射杀外星人,并检查是否有外星人撞到飞船或抵达了屏幕底端。

2.4 射杀外星人

我们创建了飞船和外星人群,但子弹击中外星人时将穿过外星人,因为还没有检查碰撞。
在游戏编程中,碰撞指的是游戏元素重叠在一起。要让子弹能够击落外星人,我们将使
用sprite.groupcollide()检测两个编组的成员之间的碰撞。

2.4.1 检测子弹与外星人的碰撞

子弹击中外星人时,我们需要马上知道,以便碰撞发生后让子弹立即消失。为此,我们将在更新子弹的位置后立即检测碰撞。

函数sprite.groupcollide()将一个编组中每个元素的rect同另一个编组中每个元素的rect进行比较。在这里,是将每颗子弹的rect同每个外星人的rect进行比较,并返回一个字典,其中包含发生了碰撞的子弹和外星人。在这个字典中,每个键都是一颗子弹,而关联的值是被该子弹击中的外星人。

在方法_update_bullets()末尾,添加如下检查子弹和外星人碰撞的代码:

    def _update_bullets(self):
        """更新子弹位置并删除消失的子弹"""
        # 更新子弹位置
        self.bullets.update()
        # 删除消失的子弹
        for bullet in self.bullets.copy():
            if bullet.rect.bottom <= 0:
                self.bullets.remove(bullet)

        # 检查子弹是否击中了外星人,如果是,就删除相应的子弹和外星人
        collisions = pygame.sprite.groupcollide(self.bullets, self.alliens, True, True)
        

这些新增的代码将self.bullets中所有的子弹都与self.aliens中所有的外星人进行比较,看它们是否重叠在一起。每当有子弹和外星人的rect重叠时,groupcollide()就在它返回的字典中添加一个键值对。两个实参True让Pygame删除发生碰撞的子弹和外星人。(要模拟能够飞行到屏幕顶端、消灭击中的每个外星人的高能子弹,可将第一个布尔实参设置为False,并保留第二个布尔参数为True。这样被击中的外星人将消失,但所有的子弹都始终有效,直到抵达屏幕顶端后消失。)

如果此时运行这个游戏,被击中的外星人将消失。

2.4.2 为测试创建大子弹

只需运行这个游戏就可测试很多功能,但有些功能在正常情况下测试起来比较烦琐。例如,要测试代码能否正确处理外星人编组为空的情形,需要花很长时间将屏幕上的外星人全部射杀。

测试有些功能时,可以修改游戏的某些设置,以便能够专注于游戏的特定方面。例如,可以缩小屏幕以减少需要射杀的外星人数量,也可以提高子弹的速度,以便能够在单位时间内发射大量子弹。

测试这个游戏时,增大子弹的尺寸并使其在击中外星人后依然有效。请尝试将bullet_width设置为300乃至3000,看看将所有外星人全部射杀有多快!

2.4.3 生成新的外星人群

这个游戏的一个重要特点是,外星人无穷无尽:一群外星人被消灭后,又会出现另一群外星人。

要在一群外星人被消灭后再显示一群外星人,首先需要检查编组aliens是否为空。如果是,就调用_create_fleet()。我们将在_update_bullets()末尾执行这项任务,因为外星人都是在这里被消灭的:

    def _update_bullets(self):
        """更新子弹位置并删除消失的子弹"""
        # 更新子弹位置
        self.bullets.update()
        # 删除消失的子弹
        for bullet in self.bullets.copy():
            if bullet.rect.bottom <= 0:
                self.bullets.remove(bullet)

        # 检查子弹是否击中了外星人,如果是,就删除相应的子弹和外星人
        collisions = pygame.sprite.groupcollide(self.bullets, self.alliens, True, True)

        if not self.alliens:
            # 删除现有的子弹并新建一群外星人
            self.bullets.empty()
            self._create_fleet()

检查编组aliens是否为空。空编组相当于False,因此这是一种检查编组是否为空的简单方式。如果编组aliens为空,就使用方法empty()删除编组中余下的所有精灵,从而删除现有的所有子弹。我们还调用了_create_fleet(),在屏幕上重新显示一群外星人。

现在,当前这群外星人被消灭干净后,将立刻出现一群新的外星人。

2.4.4 提高子弹的速度

如果现在尝试在游戏中射杀外星人,可能会发现子弹的速度不太合适(有点快或有点慢),游戏感不好。当前,可通过修改设置让这款游戏更有意思、更好玩。
要修改子弹的速度,可调整settings.py中bullet_speed的值。把bullet_speed的值调整到1.5,让子弹的速度快些:

class Settings:
    """存储游戏中所有设置的类"""
    def __init__(self):
        """初始化游戏设置"""
        self.screen_width = 1200
        self.screen_height = 800
        self.bg_color = (230, 230, 230)

        # 飞船设置
        self.ship_speed = 1.5 # 将ship_speed的初始值设置为1.5。现在需要移动飞船时,每次循环将移动1.5像素而不是1像素。

        # 子弹设置
        self.bullet_speed = 1.5
        self.bullet_width = 3
        self.bullet_height = 15
        self.bullet_color = (60, 60, 60)
        self.bullet_allowed = 3

        # 外星人设置
        self.alien_speed = 1.0
        self.fleet_drop_speed = 10
        # fleet_direction为1表示右移,为-1表示左移
        self.fleet_direction = 1

2.4.5 重构_update_bullets()

下面来重构_update_bullets(),使其不再执行那么多任务。为此,将处理子弹和外星人碰撞的代码移到一个独立的方法中:

    def _update_bullets(self):
        """更新子弹位置并删除消失的子弹"""
        # 更新子弹位置
        self.bullets.update()
        # 删除消失的子弹
        for bullet in self.bullets.copy():
            if bullet.rect.bottom <= 0:
                self.bullets.remove(bullet)

        self._check_bullet_alien_collisions()

    def _check_bullet_alien_collisions(self):
        """检查子弹是否击中了外星人,如果是,就删除相应的子弹和外星人"""
        collisions = pygame.sprite.groupcollide(self.bullets, self.alliens, True, True)
        if not self.alliens:
            # 删除现有的子弹并新建一群外星人
            self.bullets.empty()
            self._create_fleet()

我们创建了一个新方法_check_bullet_alien_collisions(),用于检测子弹和外星人之间的碰撞,并在整群外星人被消灭干净时采取相应的措施。这能避免_update_bullets()过长,简化了后续开发工作。

2.5 结束游戏

如果玩家根本不会输,游戏还有什么趣味和挑战性可言?如果玩家没能在足够短的时间内将整群外星人消灭干净,导致有外星人撞到了飞船或抵达屏幕底端,飞船将被摧毁。与此同时,限制玩家可使用的飞船数,在玩家用光所有的飞船后,游戏将结束。

2.5.1 检测外星人和飞船碰撞

首先检查外星人和飞船之间的碰撞,以便在外星人撞上飞船时做出合适的响应。

为此,在AlienInvasion中更新每个外星人的位置后,立即检测外星人和飞船之间的碰撞:

    def _update_aliens(self):
        """检查是否有外星人位于屏幕边缘,并更新所有外星人位置"""
        self._check_fleet_edges()
        self.alliens.update()
        
        # 检测外星人和飞船之间的碰撞
        if pygame.sprite.spritecollideany(self.ship, self.alliens):
            print("Ship hit!!!")
            

函数spritecollideany()接受两个实参:一个精灵和一个编组。它检查编组是否有成员与精灵发生了碰撞,并在找到与精灵发生碰撞的成员后停止遍历编组。在这里,它遍历编组aliens,并返回找到的第一个与飞船发生碰撞的外星人。
如果没有发生碰撞,spritecollideany()将返回None。

2.5.2 响应外星人和飞船碰撞

现在需要确定当外星人与飞船发生碰撞时该做些什么。我们不销毁Ship实例并创建新的,而是通过跟踪游戏的统计信息来记录飞船被撞了多少次(跟踪统计信息还有助于记分)。

下面来编写一个用于跟踪游戏统计信息的新类GameStats,并将其保存为文件game_stats.py:

class GameStats:
    """跟踪游戏的统计信息"""
    def __init__(self, ai_game):
        """初始化统计信息"""
        self.settings = ai_game.settings
        self.reset_stats()

    def reset_stats(self):
        """初始化在游戏运行期间可能变化的统计信息"""
        self.ships_left = self.settings.ship_limit
        

在游戏运行期间,只创建一个GameStats实例,但每当玩家开始新游戏时,需要重置一些统计信息。为此,在方法reset_stats()中初始化大部分统计信息,而不是在__init__()中直接初始化。我们在__init__()中调用这个方法,这样创建GameStats实例时将妥善地设置这些统计信息,在玩家开始新游戏时也能调用reset_stats()。

当前,只有一项统计信息ships_left,其值在游戏运行期间不断变化。一开始玩家拥有的飞船数存储在settings.py的ship_limit中:

class Settings:
    """存储游戏中所有设置的类"""
    def __init__(self):
        """初始化游戏设置"""
        self.screen_width = 1200
        self.screen_height = 800
        self.bg_color = (230, 230, 230)

        # 飞船设置
        self.ship_speed = 1.5   # 将ship_speed的初始值设置为1.5。现在需要移动飞船时,每次循环将移动1.5像素而不是1像素。
        self.ship_limit = 3
        
        # 子弹设置
        self.bullet_speed = 1.5
        self.bullet_width = 3
        self.bullet_height = 15
        self.bullet_color = (60, 60, 60)
        self.bullet_allowed = 3

        # 外星人设置
        self.alien_speed = 1.0
        self.fleet_drop_speed = 10
        # fleet_direction为1表示右移,为-1表示左移
        self.fleet_direction = 1

还需对alien_invasion.py做些修改,以创建一个GameStats实例。首先,更新这个文件开头的import语句:

from game_stats import GameStats

从Python标准库的模块time中导入函数sleep(),以便在飞船被外星人撞到后让游戏暂停片刻。我们还导入了GameStats。
接下来,在__init__()中创建一个GameStats实例:

# 创建一个用于存储游戏统计信息的实例
        self.stats = GameStats(self)

在创建游戏窗口后、定义诸如飞船等其他游戏元素前,创建一个GameStats实例。
有外星人撞到飞船时,将余下的飞船数减1,创建一群新的外星人,并将飞船重新放到屏幕底端的中央。另外,让游戏暂停片刻,让玩家在新外星人群出现前注意到发生了碰撞并将重新创建外星人群。

下面将实现这些功能的大部分代码放到新方法_ship_hit()中。在_update_aliens()中,将在有外星人撞到飞船时调用这个方法:

    def _ship_hit(self):
        """响应飞船被外星人撞到"""
        # 将ships_left减1
        self.stats.ships_left -= 1
        
        # 清空余下的外星人和子弹
        self.alliens.empty()
        self.bullets.empty()
        
        # 创建一群新的外星人,并将飞船放到屏幕底端的中央
        self._create_fleet()
        self.ship.center_ship()
        
        # 暂停
        sleep(0.5)

新方法_ship_hit()在飞船被外星人撞到时做出响应。在这个方法中,将余下的飞船数减1,再清空编组aliens和bullets。

接下来,创建一群新的外星人,并将飞船居中。(稍后将在Ship类中添加方法center_ship()。)最后,在更新所有元素后(但在将修改显示到屏幕前)暂停,让玩家知道飞船被撞到了。这里的函数调用sleep()让游戏暂停半秒钟,让玩家能够看到外星人撞到了飞船。函数sleep()执行完毕后,将接着执行方法_update_screen(),将新的外星人群绘制到屏幕上。

在_update_aliens()中,当有外星人撞到飞船时,不调用函数print(),而调用_ship_hit():

    def _update_aliens(self):
        """检查是否有外星人位于屏幕边缘,并更新所有外星人位置"""
        self._check_fleet_edges()
        self.alliens.update()

        # 检测外星人和飞船之间的碰撞
        if pygame.sprite.spritecollideany(self.ship, self.alliens):
            self._ship_hit()

下面是新方法center_ship(),请将其添加到ship.py的末尾:

import pygame


class Ship:
    """管理飞船类"""

    def __init__(self, ai_game):
        """初始化飞船并设置其初始设置"""
        self.screen = ai_game.screen
        self.settings = ai_game.settings
        self.screen_rect = ai_game.screen.get_rect()

        # 加载飞船图像并获取其外接矩形
        self.image = pygame.image.load('images/ship.bmp')
        self.rect = self.image.get_rect()

        # 对于每艘新飞船,都将其放在屏幕底部中央
        self.rect.midbottom = self.screen_rect.midbottom

        # 在飞船中的属性x中存储小数值
        self.x = float(self.rect.x)

        # 移动标志
        self.moving_right = False
        self.moving_left = False

    def update(self):
        """根据移动标志调整飞船位置"""
        # 更新飞船而不是rect对象的值
        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

    def blitme(self):
        """在指定位置绘制飞船"""
        self.screen.blit(self.image, self.rect)
    
    def center_ship(self):
        """让飞船在屏幕底端居中"""
        self.rect.midbottom = self.screen_rect.midbottom
        self.x = float(self.rect.x)
        

这里像__init__()中那样让飞船在屏幕底端居中。让飞船在屏幕底端居中后,重置用于跟踪飞船确切位置的属性self.x。

注意 我们根本没有创建多艘飞船。在整个游戏运行期间,只创建了一个飞船实例,并在该飞船被撞到时将其居中。统计信息ships_left指出玩家是否用完了所有的飞船。

请运行这个游戏,射杀几个外星人,并让一个外星人撞到飞船。游戏暂停片刻后,将出现一群新的外星人,而飞船将在屏幕底端居中。

2.5.3 有外星人到达屏幕底端

如果有外星人到达屏幕底端,我们将像有外星人撞到飞船那样做出响应。为检测这种情况,在alien_invasion.py中添加一个新方法:

    def _check_aliens_bottom(self):
        """检查是否有外星人到达了屏幕底端"""
        screen_rect = self.screen.get_rect()
        for alien in self.alliens.sprites():
            if alien.rect.bottom >= screen_rect.bottom:
                # 和飞船被撞到一样处理
                self._ship_hit()
                break

方法_check_aliens_bottom()检查是否有外星人到达了屏幕底端。到达屏幕底端后,外星人的属性rect.bottom大于或等于屏幕的属性rect.bottom。如果有外星人到达屏幕底端,就调用_ship_hit()。只要检测到一个外星人到达屏幕底端,就无须检查其他外星人了,因此在调用_ship_hit()后退出循环。
我们在_update_aliens()中调用_check_aliens_bottom():

  def _update_aliens(self):
        """检查是否有外星人位于屏幕边缘,并更新所有外星人位置"""
        self._check_fleet_edges()
        self.alliens.update()

        # 检测外星人和飞船之间的碰撞
        if pygame.sprite.spritecollideany(self.ship, self.alliens):
            self._ship_hit()
        
        # 检查是否有外星人到达了屏幕底端
        self._check_aliens_bottom()

在更新所有外星人的位置并检测是否有外星人和飞船发生碰撞后调用_check_aliens_bottom()。现在,每当有外星人撞到飞船或抵达屏幕底端时,都将出现一群新的外星人。

2.5.4 游戏结束

现在这个游戏看起来更完整了,但它永远都不会结束,只是ships_left不断变成越来越小的负数。下面在GameStats中添加一个作为标志的属性game_active,以便在玩家的飞船用完后结束游戏。

首先,在GameStats类的方法__init__()末尾设置这个标志:

class GameStats:
    """跟踪游戏的统计信息"""
    def __init__(self, ai_game):
        """初始化统计信息"""
        self.settings = ai_game.settings
        self.reset_stats()
        
        # 游戏刚启动时处于活动状态
        self.game_active = True
        
    def reset_stats(self):
        """初始化在游戏运行期间可能变化的统计信息"""
        self.ships_left = self.settings.ship_limit

接下来在_ship_hit()中添加代码,在玩家的飞船用完后将game_active设置为False:

    def _ship_hit(self):
        """响应飞船被外星人撞到"""
        # 将ships_left减1
        if self.stats.ships_left > 0:
            # 将ship_left减1
            self.stats.ships_left -= 1

            # 清空余下的外星人和子弹
            self.alliens.empty()
            self.bullets.empty()

            # 创建一群新的外星人,并将飞船放到屏幕底端的中央
            self._create_fleet()
            self.ship.center_ship()

            # 暂停
            sleep(0.5)
        else:
            self.stats.game_active = False

_ship_hit()的大部分代码没有变。我们将原来的代码都移到了一个if语句块中,它检查玩家是否至少还有一艘飞船。如果是,就创建一群新的外星人,暂停片刻,再接着往下执行。如果玩家没有了飞船,就将game_active设置为False。

2.6 确定应运行游戏的哪些部分

我们需要确定游戏的哪些部分在任何情况下都应运行,哪些部分仅在游戏处于活动状态时才运行:

    def run_game(self):
        """开始游戏的主循环"""
        while True:
            # 监视键盘和鼠标事件
            self._check_events()
            if self.stats.game_active:
                self.ship.update()
                self._update_bullets()
                self._update_aliens()
            # 每次循环时都重绘屏幕
            self._update_screen()

            # 让最近绘制的屏幕可见
            pygame.display.flip()  # 每次执行while循环时都绘制一个空屏幕,并擦去旧屏幕,使得只有新屏幕可见


在主循环中,在任何情况下都需要调用_check_events(),即便游戏处于非活动状态。
例如,我们需要知道玩家是否按了Q键以退出游戏,或者是否单击了关闭窗口的按钮。我们还需要不断更新屏幕,以便在等待玩家是否选择开始新游戏时修改屏幕。其他的函数仅在游戏处于活动状态时才需要调用,因为游戏处于非活动状态时,不用更新游戏元素的位置。

现在运行这个游戏,它将在飞船用完后停止不动。

3.计分

我们会添加一个Play按钮,用于根据需要启动游戏以及在游戏结束后重启游戏,还会修改这个游戏,使其随玩家等级提高而加快节奏,并实现一个记分系统。

3.1添加Play按钮

本节将添加一个Play按钮,它在游戏开始前出现,并在游戏结束后再次出现,让玩家能够开始新游戏。

当前,这个游戏在玩家运行alien_invasion.py时就开始了。下面让游戏一开始处于非活动状态,并提示玩家单击Play按钮来开始游戏。为此,像下面这样修改GameStats类的方法__init__():

class GameStats:
    """跟踪游戏的统计信息"""
    def __init__(self, ai_game):
        """初始化统计信息"""
        self.settings = ai_game.settings
        self.reset_stats()

        # 游戏刚启动时处于非活动状态
        self.game_active = False

    def reset_stats(self):
        """初始化在游戏运行期间可能变化的统计信息"""
        self.ships_left = self.settings.ship_limit

现在,游戏一开始将处于非活动状态,待创建Play按钮后,玩家才能开始游戏。

3.1.1 创建Button类

由于Pygame没有内置创建按钮的方法,我们将编写一个Button类,用于创建带标签的实心矩形。你可在游戏中使用这些代码来创建任何按钮。下面是Button类的第一部分,请将这个类保存为文件button.py:

import pygame.font


class Button:
    def __init__(self, ai_game, msg):
        """初始化按钮的属性"""
        self.screen = ai_game.screen
        self.screen_rect = self.screen.get_rect()
        
        # 设置按钮的尺寸和其他属性
        self.width, self.height = 200, 50
        self.button_color = (0, 255, 0)
        self.font = pygame.font.SysFont(None, 48)
        
        # 创建按钮的rect对象,并使其居中
        self.rect = pygame.Rect(0, 0, self.width, self.height)
        self.rect.center = self.screen_rect.center
        
        # 按钮的标签只需创建一次
        self._prep_msg(msg)
        

首先,导入模块pygame.font,它让Pygame能够将文本渲染到屏幕上。方法__init__()接受参数self、对象ai_game和msg,其中msg是要在按钮中显示的文本。设置按钮的尺寸,再通过设置button_color,让按钮的rect对象为亮绿色,并通过设置text_color让文本为白色。

指定使用什么字体来渲染文本。实参None让Pygame使用默认字体,而48指定了文本的字号。为让按钮在屏幕上居中,创建一个表示按钮的rect对象,并将其center属性设置为屏幕的center属性。

Pygame处理文本的方式是,将要显示的字符串渲染为图像。调用_prep_msg()来处理这样的渲染。
_prep_msg()的代码如下:

    def _prep_msg(self, msg):
        """将msg渲染为图像,并使其在按钮上居中"""
        self.msg_image = self.font.render(msg, True, self.text_color, self.button_color)
        self.msg_image_rect = self.msg_image.get_rect()
        self.msg_image_rect.center = self.rect.center
        

方法_prep_msg()接受实参self以及要渲染为图像的文本(msg)。调用font.render()将存储在msg中的文本转换为图像,再将该图像存储在self.msg_image中。方法font.render()还接受一个布尔实参,该实参指定开启还是关闭反锯齿功能(反锯齿让文本的边缘更平滑)。余下的两个实参分别是文本颜色和背景色。我们启用了反锯齿功能,并将文本的背景色设置为按钮的颜色。(如果没有
指定背景色,Pygame渲染文本时将使用透明背景。)

让文本图像在按钮上居中:根据文本图像创建一个rect,并将其center属性设置为按钮的center属性。

最后,创建方法draw_button(),用于将这个按钮显示到屏幕上

    def draw_button(self):
        # 绘制一个用颜色填充的按钮,再绘制文本
        self.screen.fill(self.button_color, self.rect)
        self.screen.blit(self.msg_image, self.msg_image_rect)
        

我们调用screen.fill()来绘制表示按钮的矩形,再调用screen.blit()并向它传递一幅图像以及与该图像相关联的rect,从而在屏幕上绘制文本图像。至此,Button类便创建好了。

3.1.2 在屏幕上绘制按钮

我们将在AlienInvasion中使用Button类来创建一个Play按钮。首先,更新import语句。只需要一个Play按钮,因此在AlienInvasion类的方法__init__()中创建它。可将这些代码放在方法__init__()的末尾:

    def __init__(self):
        """初始化游戏并创建游戏资源"""
        pygame.init()  # 初始化背景设置
        self.settings = Settings()

        # 固定窗口大小运行游戏
        self.screen = pygame.display.set_mode((self.settings.screen_width,
                                               self.settings.screen_height))  # 创建一个显示窗口,将这个显示窗口赋给属性self.screen,让这个类中的所有方法都能够使用它

        # 全屏运行游戏
        # 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")

        # 创建一个用于存储游戏统计信息的实例
        self.stats = GameStats(self)

        # 设置背景色
        self.bg_color = (230, 230, 230)

        self.ship = Ship(self)

        # 子弹
        self.ship = Ship(self)
        self.bullets = pygame.sprite.Group()
        # 外星人
        self.alliens = pygame.sprite.Group()

        self._create_fleet()
        
        # 创建play按钮
        self.play_button = Button(self, "Play")

这些代码创建一个标签为Play的Button实例,但没有将它显示到屏幕上。为显示该按钮,在_update_screen()对其调用方法draw_button():

   def _update_screen(self):
        """更新屏幕图像,并切换到屏幕"""
        self.screen.fill(self.settings.bg_color)
        self.ship.blitme()
        for bullet in self.bullets.sprites():
            bullet.draw_bullet()
        self.alliens.draw(self.screen)

        # 如果游戏处于非活跃状态,绘制play按钮
        if not self.stats.game_active:
            self.play_button.draw_button()

        pygame.display.flip()

为让Play按钮位于其他所有屏幕元素上面,在绘制其他所有游戏元素后再绘制这个按钮,然后切换到新屏幕。将这些代码放在一个if代码块中,让按钮仅在游戏出于非活动状态时才出现。

3.1.3 开始游戏

为在玩家单击Play按钮时开始新游戏,在_check_events()末尾添加如下elif代码块,以监视与该按钮相关的鼠标事件:

    def _check_events(self):
        """响应按键和鼠标事件"""
        for event in pygame.event.get():
            if event.type == pygame.QUIT:  # 当玩家单击游戏窗口的关闭按钮时,将检测到pygame.QUIT事件,进而调用sys.exit()来退出游戏
                sys.exit()
            elif event.type == pygame.KEYDOWN:
                self._check_keydown_events(event)
            elif event.type == pygame.KEYUP:
                self._check_keyup_events(event)
            elif event.type == pygame.MOUSEBUTTONDOWN:
                mouse_pos = pygame.mouse.get_pos()
                self._check_play_button(mouse_pos)

无论玩家单击屏幕的什么地方,Pygame都将检测到一个MOUSEBUTTONDOWN事件,但我们只想让这个游戏在玩家用鼠标单击Play按钮时做出响应。为此,使用了pygame.mouse.get_pos(),它返回一个元组,其中包含玩家单击时鼠标的x坐标和y坐标。我们将这些值传递给新方_check_play_button()。

方法_check_play_button()的代码如下,将它放在_check_events()后面:

    def _check_play_button(self, mouse_pos):
        """在玩家单击Play按钮开始游戏"""
        if self.play_button.rect.collidepoint(mouse_pos):
            self.stats.game_active = True

这里使用了rect的方法collidepoint()检查鼠标单击位置是否在Play按钮的rect内。如果是,就将game_active设置为True,让游戏开始!

至此,现在应该能够开始这个游戏了。游戏结束时,应将game_active设置为False,并重新显示Play按钮。

3.1.4 重置游戏

前面编写的代码只处理了玩家第一次单击Play按钮的情况,而没有处理游戏结束的情况,因为没有重置导致游戏结束的条件。

为在玩家每次单击Play按钮时都重置游戏,需要重置统计信息、删除现有的外星人和子弹、创建一群新的外星人并让飞船居中,如下所示:

    def _check_play_button(self, mouse_pos):
        """在玩家单击Play按钮开始游戏"""
        if self.play_button.rect.collidepoint(mouse_pos):
            # 重置游戏统计信息
            self.stats.reset_stats()
            self.stats.game_active = True
            
            # 清空余下外星人和子弹
            self.alliens.empty()
            self.bullets.empty()
            
            # 创建一群新的外星人并让飞船居中
            self._create_fleet()
            self.ship.center_ship()

现在,每当玩家单击Play按钮时,这个游戏都将正确地重置,让玩家想玩多少次就玩多少次!

3.1.5 将Play按钮切换到非活动状态

当前存在一个问题:即便Play按钮不可见,玩家单击其所在的区域时,游戏依然会做出响应。游戏开始后,如果玩家不小心单击了Play按钮所处的区域,游戏将重新开始!

为修复这个问题,可让游戏仅在game_active为False时才开始:

    def _check_play_button(self, mouse_pos):
        """在玩家单击Play按钮开始游戏"""
        button_clicked = self.play_button.rect.collidepoint(mouse_pos)
        if button_clicked and not self.stats.game_active:
            # 重置游戏统计信息
            self.stats.reset_stats()
            self.stats.game_active = True

            # 清空余下外星人和子弹
            self.alliens.empty()
            self.bullets.empty()

            # 创建一群新的外星人并让飞船居中
            self._create_fleet()
            self.ship.center_ship()

标志button_clicked的值为True或False。仅当玩家单击了Play按钮且游戏当前处于非活动状态时,游戏才重新开始。要测试这种行为,可开始新游戏,并不断单击Play按钮所在的区域。如果一切都像预期的那样工作,单击Play按钮所处的区域应该没有任何影响。

3.1.6 隐藏鼠标光标

为让玩家能够开始游戏,要让鼠标光标可见,但游戏开始后,光标只会添乱。为修复这种问题,需要在游戏处于活动状态时让光标不可见。可在方法_check_play_button()末尾的if代码块中完成这项任务:

    def _check_play_button(self, mouse_pos):
        """在玩家单击Play按钮开始游戏"""
        button_clicked = self.play_button.rect.collidepoint(mouse_pos)
        if button_clicked and not self.stats.game_active:
            # 重置游戏统计信息
            self.stats.reset_stats()
            self.stats.game_active = True

            # 清空余下外星人和子弹
            self.alliens.empty()
            self.bullets.empty()

            # 创建一群新的外星人并让飞船居中
            self._create_fleet()
            self.ship.center_ship()

            # 隐藏鼠标光标
            pygame.mouse.set_visible(False)

通过向set_visible()传递False,让Pygame在光标位于游戏窗口内时将其隐藏起来。

游戏结束后,将重新显示光标,让玩家能够单击Play按钮来开始新游戏。相关的代码如下:

    def _ship_hit(self):
        """响应飞船被外星人撞到"""
        # 将ships_left减1
        if self.stats.ships_left > 0:
            # 将ship_left减1
            self.stats.ships_left -= 1

            # 清空余下的外星人和子弹
            self.alliens.empty()
            self.bullets.empty()

            # 创建一群新的外星人,并将飞船放到屏幕底端的中央
            self._create_fleet()
            self.ship.center_ship()

            # 暂停
            sleep(0.5)
        else:
            self.stats.game_active = False
            pygame.mouse.set_visible(True)

在_ship_hit()中,在游戏进入非活动状态后,立即让光标可见。关注这样的细节让游戏显得更专业,也让玩家能够专注于玩游戏而不是去费力理解用户界面。

3.2 提高等级

当前,将整群外星人消灭干净后,玩家将提高一个等级,但游戏的难度没变。下面来增加一点趣味性:每当玩家将屏幕上的外星人消灭干净后,都加快游戏的节奏,让游戏玩起来更难。

3.2.1 修改速度设置

首先重新组织Settings类,将游戏设置划分成静态和动态两组。对于随着游戏进行而变化的设置,还要确保在开始新游戏时进行重置。settings.py的方法__init__()如下:

class Settings:
    """存储游戏中所有设置的类"""
    def __init__(self):
        """初始化游戏设置"""
        self.screen_width = 1200
        self.screen_height = 800
        self.bg_color = (230, 230, 230)

        # 飞船设置
        self.ship_limit = 3

        # 子弹设置
        self.bullet_width = 3
        self.bullet_height = 15
        self.bullet_color = (60, 60, 60)
        self.bullet_allowed = 3

        # 外星人设置
        self.fleet_drop_speed = 10

        # 加快游戏节奏
        self.speed_scale = 1.1

        self.initialize_dynamic_settings()

    def initialize_dynamic_settings(self):
        """初始化随游戏进行而变化设置"""
        self.ship_speed = 1.5  # 将ship_speed的初始值设置为1.5。现在需要移动飞船时,每次循环将移动1.5像素而不是1像素。
        self.bullet_speed = 3.0
        self.alien_speed = 1.0

        # fleet_direction为1表示右移,为-1表示左移
        self.fleet_direction = 1

这个方法设置飞船、子弹和外星人的初始速度。随着游戏的进行,将提高这些速度。每当玩家开始新游戏时,都将重置这些速度。在这个方法中,还设置了fleet_direction,使得游戏刚开始时,外星人总是向右移动。不需要增大fleet_drop_speed的值,因为外星人移动的速度越快,到达屏幕底端所需的时间越短。

为在玩家的等级提高时提高飞船、子弹和外星人的速度,编写一个名为increase_speed()的新方法:

   def increase_speed(self):
        """提高速度设置"""
        self.ship_speed *= self.speedup_scale
        self.bullet_speed *= self.speedup_scale
        self.alien_speed *= self.speedup_scale
``
为提高这些游戏元素的速度,将每个速度设置都乘以speedup_scale的值。

在_check_bullet_alien_collisions()中,在整群外星人都被消灭后调用increase_speed()来加快游戏的节奏:
```py
 def _check_bullet_alien_collisions(self):
        """检查子弹是否击中了外星人,如果是,就删除相应的子弹和外星人"""
        collisions = pygame.sprite.groupcollide(self.bullets, self.alliens, True, True)
        if not self.alliens:
            # 删除现有的子弹并新建一群外星人
            self.bullets.empty()
            self._create_fleet()
            self.settings.increase_speed()

通过修改速度设置ship_speed、alien_speed和bullet_speed的值,足以加快整个游
戏的节奏!

3.2.2 重置速度

每当玩家开始新游戏时,都需要将发生了变化的设置重置为初始值,否则新游戏开始时,速度设置将为前一次提高后的值:

 def _check_play_button(self, mouse_pos):
        """在玩家单击Play按钮开始游戏"""
        button_clicked = self.play_button.rect.collidepoint(mouse_pos)
        if button_clicked and not self.stats.game_active:
            # 重置游戏设置
            self.settings.initialize_dynamic_settings()
            
            # 重置游戏统计信息
            self.stats.reset_stats()
            self.stats.game_active = True

            # 清空余下外星人和子弹
            self.alliens.empty()
            self.bullets.empty()

            # 创建一群新的外星人并让飞船居中
            self._create_fleet()
            self.ship.center_ship()

            # 隐藏鼠标光标
            pygame.mouse.set_visible(False)

现在,游戏《外星人入侵》玩起来更有趣,也更有挑战性了。每当玩家将屏幕上的外星人消灭干净后,游戏都将加快节奏,因此难度更大。如果游戏的难度提高得太快,可降低settings.speedup_scale的值;如果游戏的挑战性不足,可稍微提高这个设置的值。找出这个设置的最佳值,让难度的提高速度相对合理:一开始的几群外星人很容易消灭干净,接下来的几群消灭起来有一定难度,但也不是不可能,而要将之后的外星人群消灭干净几乎不可能。

3.3 记分

下面来实现一个记分系统,以实时跟踪玩家的得分,并显示最高得分、等级和余下的飞船数。

得分是游戏的一项统计信息,因此在GameStats中添加一个score属性:

class GameStats:
    """跟踪游戏的统计信息"""
    def __init__(self, ai_game):
        """初始化统计信息"""
        self.settings = ai_game.settings
        self.reset_stats()

        # 游戏刚启动时处于非活动状态
        self.game_active = False

    def reset_stats(self):
        """初始化在游戏运行期间可能变化的统计信息"""
        self.ships_left = self.settings.ship_limit
        self.score = 0

3.3.1 显示得分

为在屏幕上显示得分,首先创建一个新类Scoreboard。当前,这个类只显示当前得分,但后面也将使用它来显示最高得分、等级和余下的飞船数。下面是这个类的前半部分,被保存为文件scoreboard.py:

import pygame.font


class Scoreboard:
    """显示得分信息的类"""

    def __init__(self, ai_game):
        """初始化显示得分涉及的属性"""
        self.screen = ai_game.screen
        self.screen_rect = self.screen.get_rect()
        self.settings = ai_game.settings
        self.stats = ai_game.stats

        # 显示得分信息时使用的字体设置
        self.text_color = (30, 30, 30)
        self.font = pygame.font.SysFont(None, 48)

        # 准备初始得分图像
        self.prep_score()

由于Scoreboard在屏幕上显示文本,首先导入模块pygame.font。接下来,在__init__()中包含形参ai_game,以便访问报告跟踪的值所需的对象settings、screen和stats。然后,设置文本颜色并实例化一个字体对象。

为将要显示的文本转换为图像,调用prep_score(),其定义如下:

 def prep_score(self):
        """将得分转换为一副渲染的图像"""
        score_str = str(self.stats.score)
        self.score_image = self.font.render(score_str, True, self.text_color, self.settings.bg_color)

        # 在屏幕右上角显示得分
        self.score_rect = self.score_image.get_rect()
        self.score_rect.right = self.screen_rect.right - 20
        self.score_rect.top = 20
        

在prep_score()中,将数值stats.score转换为字符串,再将这个字符串传递给创建图像的render()。为在屏幕上清晰地显示得分,向render()传递屏幕背景色和文本颜色。

将得分放在屏幕右上角,并在得分增大导致数变宽时让其向左延伸。为确保得分始终锚定
在屏幕右边,创建一个名为score_rect的rect,让其右边缘与屏幕右边缘相距20像素,并让其上边缘与屏幕上边缘也相距20像素。

接下来,创建方法show_score(),用于显示渲染好的得分图像:

    def show_score(self):
        """在屏幕上显示得分"""
        self.screen.blit(self.score_image, self.score_rect)

这个方法在屏幕上显示得分图像,并将其放在score_rect指定的位置。

3.3.2 创建记分牌

为显示得分,在AlienInvasion中创建一个Scoreboard实例。先来更新import语句,接下来,在方法__init__()中创建一个Scoreboard实例:

    def __init__(self):
        """初始化游戏并创建游戏资源"""
        pygame.init()  # 初始化背景设置
        self.settings = Settings()

        # 固定窗口大小运行游戏
        self.screen = pygame.display.set_mode((self.settings.screen_width,
                                               self.settings.screen_height))  # 创建一个显示窗口,将这个显示窗口赋给属性self.screen,让这个类中的所有方法都能够使用它

        # 全屏运行游戏
        # 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")
        
        # 创建一个用于存储游戏统计信息的实例
        # 创建记分牌
        self.stats = GameStats(self)
        self.sb = Scoreboard(self)
        
        # 设置背景色
        self.bg_color = (230, 230, 230)

        self.ship = Ship(self)

        # 子弹
        self.ship = Ship(self)
        self.bullets = pygame.sprite.Group()
        # 外星人
        self.alliens = pygame.sprite.Group()

        self._create_fleet()

        # 创建play按钮
        self.play_button = Button(self, "Play")

然后,在_update_screen()中将记分牌绘制到屏幕上:

 def _update_screen(self):
        """更新屏幕图像,并切换到屏幕"""
        self.screen.fill(self.settings.bg_color)
        self.ship.blitme()
        for bullet in self.bullets.sprites():
            bullet.draw_bullet()
        self.alliens.draw(self.screen)

        # 显示得分
        self.sb.show_score()

        # 如果游戏处于非活跃状态,绘制play按钮
        if not self.stats.game_active:
            self.play_button.draw_button()

        pygame.display.flip()

3.3.3 在外星人被消灭时更新得分

为在屏幕上实时显示得分,每当有外星人被击中时,都更新stats.score的值,再调用prep_score()更新得分图像。但在此之前,需要指定玩家每击落一个外星人将得到多少分:

     def initialize_dynamic_settings(self):
        """初始化随游戏进行而变化设置"""
        self.ship_speed = 1.5  # 将ship_speed的初始值设置为1.5。现在需要移动飞船时,每次循环将移动1.5像素而不是1像素。
        self.bullet_speed = 3.0
        self.alien_speed = 1.0

        # fleet_direction为1表示右移,为-1表示左移
        self.fleet_direction = 1

        # 计分
        self.alien_points = 50

随着游戏的进行,将提高每个外星人的分数。为确保每次开始新游戏时这个值都会被重
置,我们在initialize_dynamic_settings()中设置它。
在_check_bullet_alien_collisions()中,每当有外星人被击落时,都更新得分:

        def _check_bullet_alien_collisions(self):
        """检查子弹是否击中了外星人,如果是,就删除相应的子弹和外星人"""
        collisions = pygame.sprite.groupcollide(self.bullets, self.alliens, True, True)
        if collisions:
            self.stats.score += self.settings.alien_points
            self.sb.prep_score()

        if not self.alliens:
            # 删除现有的子弹并新建一群外星人
            self.bullets.empty()
            self._create_fleet()
            self.settings.increase_speed()

有子弹击中外星人时,Pygame返回一个字典(collisions)。我们检查这个字典是否存在,如果存在,就将得分加上一个外星人的分数\。接下来,调用prep_score()来创建一幅包含最新得分的新图像。

3.3.4 重置得分

当前,仅在有外星人被射杀之后生成得分。这在大多数情况下可行,但从开始新游戏到有外星人被射杀之间,显示的是上一次的得分。

为修复这个问题,可在开始新游戏时生成得分:

    def _check_play_button(self, mouse_pos):
        """在玩家单击Play按钮开始游戏"""
        button_clicked = self.play_button.rect.collidepoint(mouse_pos)
        if button_clicked and not self.stats.game_active:
            # 重置游戏设置
            self.settings.initialize_dynamic_settings()

            # 重置游戏统计信息
            self.stats.reset_stats()
            self.stats.game_active = True
            self.sb.prep_score()

            # 清空余下外星人和子弹
            self.alliens.empty()
            self.bullets.empty()

            # 创建一群新的外星人并让飞船居中
            self._create_fleet()
            self.ship.center_ship()

            # 隐藏鼠标光标
            pygame.mouse.set_visible(False)

开始新游戏时,我们重置游戏统计信息再调用prep_score()。此时生成的记分牌上显示的得分为零。

3.3.5 将消灭的每个外星人都计入得分

当前的代码可能会遗漏一些被消灭的外星人。例如,如果在一次循环中,有两颗子弹击中了外星人,或者因子弹较宽而同时击中了多个外星人,玩家将只能得到一个外星人的分数。为修复这种问题,我们来调整检测子弹和外星人碰撞的方式。

在_check_bullet_alien_collisions()中,与外星人碰撞的子弹都是字典collisions中的一个键,而与每颗子弹相关的值都是一个列表,其中包含该子弹击中的外星人。我们遍历字典collisions,确保将消灭的每个外星人都计入得分:

    def _check_bullet_alien_collisions(self):
        """检查子弹是否击中了外星人,如果是,就删除相应的子弹和外星人"""
        collisions = pygame.sprite.groupcollide(self.bullets, self.alliens, True, True)
        if collisions:
            for aliens in collisions.values():
                self.stats.score += self.settings.alien_points * len(aliens)
            self.sb.prep_score()

如果字典collisions存在,就遍历其中的所有值。别忘了,每个值都是一个列表,包含被同一颗子弹击中的所有外星人。对于每个列表,都将其包含的外星人数量乘以一个外星人的分数,并将结果加入当前得分。为测试这一点,请将子弹宽度改为300像素,并核实得到了其击中的每个外星人的分数,再将子弹宽度恢复正常值。

3.3.6 提高分数

鉴于玩家每提高一个等级,游戏都变得更难,因此处于较高的等级时,外星人的分数应更高。为实现这种功能,需要编写在游戏节奏加快时提高分数的代码:

class Settings:
    """存储游戏中所有设置的类"""
    def __init__(self):
        """初始化游戏设置"""
        self.screen_width = 1200
        self.screen_height = 800
        self.bg_color = (230, 230, 230)

        # 飞船设置
        self.ship_limit = 3

        # 子弹设置
        self.bullet_width = 3
        self.bullet_height = 15
        self.bullet_color = (60, 60, 60)
        self.bullet_allowed = 3

        # 外星人设置
        self.fleet_drop_speed = 10

        # 加快游戏节奏
        self.speedup_scale = 1.1

        # 外星人分数提高的速度
        self.score_scale = 1.5

        self.initialize_dynamic_settings()

    def initialize_dynamic_settings(self):
        """初始化随游戏进行而变化设置"""
        self.ship_speed = 1.5  # 将ship_speed的初始值设置为1.5。现在需要移动飞船时,每次循环将移动1.5像素而不是1像素。
        self.bullet_speed = 3.0
        self.alien_speed = 1.0

        # fleet_direction为1表示右移,为-1表示左移
        self.fleet_direction = 1

        # 计分
        self.alien_points = 50

    def increase_speed(self):
        """提高速度设置和外星人分数"""
        self.ship_speed *= self.speedup_scale
        self.bullet_speed *= self.speedup_scale
        self.alien_speed *= self.speedup_scale

        self.alien_points = int(self.alien_points * self.score_scale)
        

我们定义了分数的提高速度,并称之为score_scale。较低的节奏加快速度(1.1)让游戏很快变得极具挑战性,但为了让记分发生显著的变化,需要将分数的提高速度设置为更大的值(1.5)。现在,在加快游戏节奏的同时,提高了每个外星人的分数。为让分数为整数,使用了函数int()。

为显示外星人的分数,在Settings的方法increase_speed()中调用函数print():

    def increase_speed(self):
        """提高速度设置和外星人分数"""
        self.ship_speed *= self.speedup_scale
        self.bullet_speed *= self.speedup_scale
        self.alien_speed *= self.speedup_scale

        self.alien_points = int(self.alien_points * self.score_scale)
        print(self.alien_points)

现在每当提高一个等级时,你都将在终端窗口看到新的分数值。
注意 确认分数在不断增加后,一定要删除调用函数print()的代码,否则可能影响游戏的性能,分散玩家的注意力。

3.3.7 舍入得分

大多数街机风格的射击游戏将得分显示为10的整数倍,下面让记分系统遵循这个原则。我们还将设置得分的格式,在大数中添加用逗号表示的千位分隔符。在Scoreboard中执行这种修改:

    def prep_score(self):
        """将得分转换为一副渲染的图像"""
        rounded_score = round(self.stats.score, -1)
        score_str = "{:,}".format(rounded_score)
        self.score_image = self.font.render(score_str, True, self.text_color, self.settings.bg_color)

        # 在屏幕右上角显示得分
        self.score_rect = self.score_image.get_rect()
        self.score_rect.right = self.screen_rect.right - 20
        self.score_rect.top = 20

函数round()通常让小数精确到小数点后某一位,其中小数位数是由第二个实参指定的。然而,如果将第二个实参指定为负数,round()将舍入到最近的10的整数倍,如10、100、1000等。Python将stats.score的值舍入到最近的10的整数倍,并将结果存储到rounded_score中。
使用一个字符串格式设置指令,让Python将数值转换为字符串时在其中插入逗号。例如,输出为1,000,000而不是1000000。如果现在运行这个游戏,看到的得分将是10的整数倍,即便得分很高亦如此。

3.3.8 最高得分

每个玩家都想超过游戏的最高得分纪录。下面来跟踪并显示最高得分,给玩家提供要超越
的目标。我们将最高得分存储在GameStats中:

  def __init__(self, ai_game):
        """初始化统计信息"""
        self.settings = ai_game.settings
        self.reset_stats()

        # 游戏刚启动时处于非活动状态
        self.game_active = False

        # 任何情况下都不重置最高得分
        self.high_score = 0

因为在任何情况下都不会重置最高得分,所以在__init__()而不是reset_stats()中初始化high_score。
下面来修改Scoreboard以显示最高得分。先来修改方法__init__():

    def __init__(self, ai_game):
        """初始化显示得分涉及的属性"""
        self.ai_game = ai_game
        self.screen = ai_game.screen
        self.screen_rect = self.screen.get_rect()
        self.settings = ai_game.settings
        self.stats = ai_game.stats

        # 显示得分信息时使用的字体设置
        self.text_color = (30, 30, 30)
        self.font = pygame.font.SysFont(None, 48)

        # 准备包含最高得分和当前得分图像
        self.prep_score()
        self.prep_high_score()
        self.pre_level()
        self.prep_ships()

最高得分将与当前得分分开显示,因此需要编写一个新方法prep_high_score(),用于准备包含最高得分的图像。方法prep_high_score()的代码如下

    def prep_high_score(self):
        """将最高得分转换为渲染的图像"""
        high_score = round(self.stats.high_score, -1)
        high_score_str = "{:,}".format(high_score)
        self.high_score_image = self.font.render(high_score_str, True, self.text_color, self.settings.bg_color)
        
        # 将最高得分放在屏幕顶部中央
        self.high_score_rect = self.high_score_image.get_rect()
        self.high_score_rect.centerx = self.settings.bg_color
        self.high_score_rect.top = self.score_rect.top
        

将最高得分舍入到最近的10的整数倍,并添加用逗号表示的千分位分隔符。然后,根据最高得分生成一幅图像,使其水平居中,并将其top属性设置为当前得分图像的top属性。

现在,方法show_score()需要在屏幕右上角显示当前得分,并在屏幕顶部中央显示最高得分:

    def show_score(self):
        """在屏幕上显示得分"""
        self.screen.blit(self.score_image, self.score_rect)
        self.screen.blit(self.high_score_image, self.high_score_rect)
        

为检查是否诞生了新的最高得分,在Scoreboard中添加一个新方法check_high_score():

   def check_high_score(self):
        """检查是否诞生了新的最高分"""
        if self.stats.score > self.stats.high_score:
            self.stats.high_score = self.stats.score
            self.prep_high_score()

方法check_high_score()比较当前得分和最高得分。如果当前得分更高,就更新high_score的值,并调用prep_high_score()来更新包含最高得分的图像。

在_check_bullet_alien_collisions()中,每当有外星人被消灭时,都需要在更新得分后调用check_high_score():

 def _check_bullet_alien_collisions(self):
        """检查子弹是否击中了外星人,如果是,就删除相应的子弹和外星人"""
        collisions = pygame.sprite.groupcollide(self.bullets, self.alliens, True, True)
        if collisions:
            for aliens in collisions.values():
                self.stats.score += self.settings.alien_points * len(aliens)
            self.sb.prep_score()
            self.sb.check_high_score()
            
        if not self.alliens:
            # 删除现有的子弹并新建一群外星人
            self.bullets.empty()
            self._create_fleet()
            self.settings.increase_speed()

如果字典collisions存在,就根据消灭了多少外星人更新得分,再调用check_high_score()。

3.3.9 显示等级

为在游戏中显示玩家的等级,首先需要在GameStats中添加一个表示当前等级的属性。为确保每次开始新游戏时都重置等级,在reset_stats()中初始化它:

 def reset_stats(self):
        """初始化在游戏运行期间可能变化的统计信息"""
        self.ships_left = self.settings.ship_limit
        self.score = 0
        self.level = 1

为了让Scoreboard显示当前等级,在__init__()中调用一个新方法prep_level():

  def __init__(self, ai_game):
        """初始化显示得分涉及的属性"""
        self.ai_game = ai_game
        self.screen = ai_game.screen
        self.screen_rect = self.screen.get_rect()
        self.settings = ai_game.settings
        self.stats = ai_game.stats

        # 显示得分信息时使用的字体设置
        self.text_color = (30, 30, 30)
        self.font = pygame.font.SysFont(None, 48)

        # 准备包含最高得分和当前得分图像
        self.prep_score()
        self.prep_high_score()
        self.pre_level()
        self.prep_ships()

prep_level()的代码如下:

    def prep_level(self):
        """将等级转换为渲染的图像"""
        leve_str = str(self.stats.level)
        self.level_image = self.render(leve_str, True, self.text_color, self.settings.bg_color)

        # 将等级放在得分下方
        self.level_rect = self.level_image.get_rect()
        self.level_rect.right = self.score_rect.right
        self.level_rect.top = self.score_rect.bottom + 10

方法prep_level()根据存储在stats.level中的值创建一幅图像,并将其right属性设置为得分的right属性。然后,将top属性设置为比得分图像的bottom属性大10像素,以便在得分和等级之间留出一定的空间。

还需要更新show_score():

    def show_score(self):
        """在屏幕上显示得分"""
        self.screen.blit(self.score_image, self.score_rect)
        self.screen.blit(self.high_score_image, self.high_score_rect)
        self.screen.blit(self.level_image, self.level_rect)

新增的代码行在屏幕上显示等级图像。我们在_check_bullet_alien_collisions()中提高等级并更新等级图像:
``py
def _check_bullet_alien_collisions(self):
“”“检查子弹是否击中了外星人,如果是,就删除相应的子弹和外星人”“”
collisions = pygame.sprite.groupcollide(self.bullets, self.alliens, True, True)
if collisions:
for aliens in collisions.values():
self.stats.score += self.settings.alien_points * len(aliens)
self.sb.prep_score()
self.sb.check_high_score()

    if not self.alliens:
        # 删除现有的子弹并新建一群外星人
        self.bullets.empty()
        self._create_fleet()
        self.settings.increase_speed()
        
        # 提高等级
        self.stats.level += 1
        self.sb.prep_level()
如果整群外星人都被消灭,就将stats.level的值加1,并调用prep_level()确保正确地显示了新等级。
为确保在开始新游戏时更新等级图像,还需在玩家单击按钮Play时调用prep_level():
```py
 def _check_play_button(self, mouse_pos):
        """在玩家单击Play按钮开始游戏"""
        button_clicked = self.play_button.rect.collidepoint(mouse_pos)
        if button_clicked and not self.stats.game_active:
            # 重置游戏设置
            self.settings.initialize_dynamic_settings()

            # 重置游戏统计信息
            self.stats.reset_stats()
            self.stats.game_active = True
            self.sb.prep_score()
            self.sb.prep_level()
            
            # 清空余下外星人和子弹
            self.alliens.empty()
            self.bullets.empty()

            # 创建一群新的外星人并让飞船居中
            self._create_fleet()
            self.ship.center_ship()

            # 隐藏鼠标光标
            pygame.mouse.set_visible(False)

这里在调用prep_score()后立即调用prep_level()。

注意 在一些经典游戏中,得分带有标签,如Score、High Score和Level。这里没有显示这些标签,游戏开始后,每个数的含义将一目了然。要包含这些标签,只需在Scoreboard中调用font.render()前,将它们添加到得分字符串中。

3.3.10 显示余下的飞船数

最后来显示玩家还有多少艘飞船,但使用图形而不是数字。为此,在屏幕左上角绘制飞船图像来指出还余下多少艘飞船,就像众多经典的街机游戏中那样。首先,需要让Ship继承Sprite,以便创建飞船编组:

import pygame
from pygame.sprite import Sprite

class Ship(Sprite):
    """管理飞船类"""

    def __init__(self, ai_game):
        """初始化飞船并设置其初始设置"""
        super().__init__()
        self.screen = ai_game.screen
        self.settings = ai_game.settings
        self.screen_rect = ai_game.screen.get_rect()

        # 加载飞船图像并获取其外接矩形
        self.image = pygame.image.load('images/ship.bmp')
        self.rect = self.image.get_rect()

        # 对于每艘新飞船,都将其放在屏幕底部中央
        self.rect.midbottom = self.screen_rect.midbottom

        # 在飞船中的属性x中存储小数值
        self.x = float(self.rect.x)

        # 移动标志
        self.moving_right = False
        self.moving_left = False

    def update(self):
        """根据移动标志调整飞船位置"""
        # 更新飞船而不是rect对象的值
        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

    def blitme(self):
        """在指定位置绘制飞船"""
        self.screen.blit(self.image, self.rect)

    def center_ship(self):
        """让飞船在屏幕底端居中"""
        self.rect.midbottom = self.screen_rect.midbottom
        self.x = float(self.rect.x)


接下来,需要修改Scoreboard,以创建可供显示的飞船编组。下面是其中的import语句:

import pygame.font
from pygame.sprite import Group

from ship import Ship

class Scoreboard:
    """显示得分信息的类"""

    def __init__(self, ai_game):
        """初始化显示得分涉及的属性"""
        self.ai_game = ai_game
        self.screen = ai_game.screen
        self.screen_rect = self.screen.get_rect()
        self.settings = ai_game.settings
        self.stats = ai_game.stats

        # 显示得分信息时使用的字体设置
        self.text_color = (30, 30, 30)
        self.font = pygame.font.SysFont(None, 48)

        # 准备包含最高得分和当前得分图像
        self.prep_score()
        self.prep_high_score()
        self.prep_level()
        self.prep_ships()

    def prep_score(self):
        """将得分转换为一副渲染的图像"""
        rounded_score = round(self.stats.score, -1)
        score_str = "{:,}".format(rounded_score)
        self.score_image = self.font.render(score_str, True, self.text_color, self.settings.bg_color)

        # 在屏幕右上角显示得分
        self.score_rect = self.score_image.get_rect()
        self.score_rect.right = self.screen_rect.right - 20
        self.score_rect.top = 20

    def show_score(self):
        """在屏幕上显示得分"""
        self.screen.blit(self.score_image, self.score_rect)
        self.screen.blit(self.high_score_image, self.high_score_rect)
        self.screen.blit(self.level_image, self.level_rect)

    def prep_high_score(self):
        """将最高得分转换为渲染的图像"""
        high_score = round(self.stats.high_score, -1)
        high_score_str = "{:,}".format(high_score)
        self.high_score_image = self.font.render(high_score_str, True, self.text_color, self.settings.bg_color)

        # 将最高得分放在屏幕顶部中央
        self.high_score_rect = self.high_score_image.get_rect()
        self.high_score_rect.centerx = self.screen_rect.centerx
        self.high_score_rect.top = self.score_rect.top

    def check_high_score(self):
        """检查是否诞生了新的最高分"""
        if self.stats.score > self.stats.high_score:
            self.stats.high_score = self.stats.score
            self.prep_high_score()

    def prep_level(self):
        """将等级转换为渲染的图像"""
        leve_str = str(self.stats.level)
        self.level_image = self.font.render(leve_str, True, self.text_color, self.settings.bg_color)

        # 将等级放在得分下方
        self.level_rect = self.level_image.get_rect()
        self.level_rect.right = self.score_rect.right
        self.level_rect.top = self.score_rect.bottom + 10

我们将游戏实例赋给一个属性,因为创建飞船时需要用到它。在调用prep_level()后调用了prep_ships()。
prep_ships()的代码如下:

 def prep_ships(self):
        """显示还余下多少艘飞船"""
        self.ships = Group()
        for ship_number in range(self.stats.ships_left):
            ship = Ship(self.ai_game)
            ship.rect.x = 10 + ship_number * ship.rect.width
            seip.rect.y = 10
            self.ships.add(ship)

方法prep_ships()创建一个空编组self.ships,用于存储飞船实例。为填充这个编组,根据玩家还有多少艘飞船以相应的次数运行一个循环。在这个循环中,创建新飞船并设置其 坐标,让整个飞船编组都位于屏幕左边,且每艘飞船的左边距都为10像素。还将 坐标设置为离屏幕上边缘10像素,让所有飞船都出现在屏幕左上角。最后,将每艘新飞船都添加到编组ships中。

  def show_score(self):
        """在屏幕上显示得分"""
        self.screen.blit(self.score_image, self.score_rect)
        self.screen.blit(self.high_score_image, self.high_score_rect)
        self.screen.blit(self.level_image, self.level_rect)
        self.ships.draw(self.screen)

为在屏幕上显示飞船,对编组调用draw()。Pygame将绘制每艘飞船。为在游戏开始时让玩家知道自己有多少艘飞船,在开始新游戏时调用prep_ships()。
这是在AlienInvasion的_check_play_button()中进行的:

 def _check_play_button(self, mouse_pos):
        """在玩家单击Play按钮开始游戏"""
        button_clicked = self.play_button.rect.collidepoint(mouse_pos)
        if button_clicked and not self.stats.game_active:
            # 重置游戏设置
            self.settings.initialize_dynamic_settings()

            # 重置游戏统计信息
            self.stats.reset_stats()
            self.stats.game_active = True
            self.sb.prep_score()
            self.sb.prep_level()
            self.sb.prep_ships()
            
            # 清空余下外星人和子弹
            self.alliens.empty()
            self.bullets.empty()

            # 创建一群新的外星人并让飞船居中
            self._create_fleet()
            self.ship.center_ship()

            # 隐藏鼠标光标
            pygame.mouse.set_visible(False)

还要在飞船被外星人撞到时调用prep_ships(),从而在玩家损失飞船时更新飞船图像:

    def _ship_hit(self):
        """响应飞船被外星人撞到"""
        # 将ships_left减1
        if self.stats.ships_left > 0:
            # 将ship_left减1并更新记分牌
            self.stats.ships_left -= 1
            self.sb.prep_ships()

            # 清空余下的外星人和子弹
            self.alliens.empty()
            self.bullets.empty()

            # 创建一群新的外星人,并将飞船放到屏幕底端的中央
            self._create_fleet()
            self.ship.center_ship()

            # 暂停
            sleep(0.5)
        else:
            self.stats.game_active = False
            pygame.mouse.set_visible(True)

这里在将ships_left的值减1后调用prep_ships()。这样每次损失飞船后,显示的飞船数都是正确的。

4.小结

至此,我们的外星人入侵项目就算成了,其中有很多细节需要注意和学习,稍有疏漏便会溃不成塔。这只是Python编程从入门到实践的第一个项目,之后还有多个项目可供学习,基础知识可参考python编程入门,项目源代码下载请点击此链接项目一:外星人入侵

你可能感兴趣的:(Python,python,pygame,开发语言,游戏程序)