文章目录


项目描述

首先描述一下要实验的这个《飞机大战》游戏。

在游戏中,玩家控制这一艘最初出现在屏幕最底部中央的飞船。玩家可以使用 箭头键 左右移动非常,还可以使用 空格键 进行射击。游戏开始时,一群外星人出现在天空中,他们在屏幕中向下移动。玩家的任务就是射杀这些外星人。玩家将所有外星人都消灭干净后,将出现一批新的外星人,他们移动速度更快。这个过程中,只要外星人撞到玩家的飞船或者到达了屏幕底部,玩家就会损失一艘飞船。玩家在损失三艘飞船后,游戏结束。

Pygame 安装

Windows 平台下安装

首先下载安装包,下载地址,下载 whl 文件,使用 pip 安装。

PS F:\Pythone\实例代码\项目(飞机大战)> pip install pygame-1.9.6-cp39-cp39-win_amd64.whl
ERROR: pygame-1.9.6-cp39-cp39-win_amd64.whl is not a supported wheel on this platform.
WARNING: You are using pip version 19.2.3, however version 20.1.1 is available.
You should consider upgrading via the 'python -m pip install --upgrade pip' command.
PS F:\Pythone\实例代码\项目(飞机大战)> pip install pygame-1.9.6-cp39-cp39-win_amd64.whl
ERROR: pygame-1.9.6-cp39-cp39-win_amd64.whl is not a supported wheel on this platform.
WARNING: You are using pip version 19.2.3, however version 20.1.1 is available.
You should consider upgrading via the 'python -m pip install --upgrade pip' command.
PS F:\Pythone\实例代码\项目(飞机大战)> python -m pip install --upgrade pip
Collecting pip
  Downloading https://files.pythonhosted.org/packages/43/84/23ed6a1796480a6f1a2d38f2802901d078266bda38388954d01d3f2e821d/pip-20.1.1-py2.py3-none-any.whl (1.5MB)
     |████████████████████████████████| 1.5MB 21kB/s
Installing collected packages: pip
  Found existing installation: pip 19.2.3
    Uninstalling pip-19.2.3:
      Successfully uninstalled pip-19.2.3
Successfully installed pip-20.1.1
PS F:\Pythone\实例代码\项目(飞机大战)> pip install pygame-1.9.6-cp39-cp39-win_amd64.whl
ERROR: pygame-1.9.6-cp39-cp39-win_amd64.whl is not a supported wheel on this platform.
PS F:\Pythone\实例代码\项目(飞机大战)> pip3 install pygame-1.9.6-cp39-cp39-win_amd64.whl
ERROR: pygame-1.9.6-cp39-cp39-win_amd64.whl is not a supported wheel on this platform.
PS F:\Pythone\实例代码\项目(飞机大战)>

首先遇到 pip 工具更新问题,就行了安装更新。然后又报出来安装的这个库版本不匹配,我安装的 Python 版本为 3.8 ,这里发现库上写的 cp39 表示支持 Python 版本为 3.9 , 所以又下载了新的 3.8 的库,继续尝试安装。

在这里插入图片描述终端运行 Python 查看是否可以正常导入,显示如下结果表示正常。

PS F:\Pythone\实例代码\项目(飞机大战)> python
Python 3.8.3 (tags/v3.8.3:6f8c832, May 13 2020, 22:37:02) [MSC v.1924 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import pygame
pygame 1.9.6
Hello from the pygame community. https://www.pygame.org/contribute.html
>>>

Linux 平台安装

如果使用的是 Python2.7 ,则可以使用包管理器来安装 pygame。打开终端,执行下面命令:

$ sudo apt-get install python-pygame

如果使用的是 Python3 ,就需要执行两个步骤:

  • 安装 pygame 依赖的库
  • 下载并安装 pygame
$ sudo apt-get install python3.8-dev mercurial
$ sudo apt-get install libsdl-image1.2-dev libsdl2-dev libsd1-ttf2.0-dev
$ sudo apt-get install libsdl-mixer1.2-dev libportmidi-dev
$ sudo apt-get install libswscale-dev libsmpeg-dev libavformat-dev libavcode-dev
$ sudo apt-get install python-numpy

$ pip install --user hg+http://bitbucket.org/pygame/pygame

开始编写项目

开始编写项目,首先创建一个空的 pygame 窗口,供后面用来绘制游戏元素,如飞船和外星人。此外还需要使得这个游戏可以响应用户输入、设置背景以及加载飞船图像。

创建 pygame 窗口

首先,创建一个空的 pygame 窗口,使用 pygame 编写的游戏的基本结构如下:

import sys
import pygame

def run_game():
    # 初始化游戏并创建一个屏幕对象
    pygame.init()
    screen = pygame.display.set_mode((1200, 800))
    pygame.display.set_caption("Alien Invasion")
    
    # 开始游戏的主循环
    while True:
        # 监视键盘和鼠标事件
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                sys.exit()
        
        # 让最近绘制的屏幕可见
        pygame.display.flip()
        
run_game()

执行结果如下:

在这里插入图片描述
上述代码中:

  • pygame.init() 初始化背景设置,调用 pygame.display.set_mode() 来创建一个名为 screen 的显示窗口。
  • 对象 screen 是一个 surface 。在 pygame 中,surface 是屏幕的一部分,用于显示游戏元素。在这个项目中,每个元素(外星人或者飞船)都是一个 surface 。每次循环后都会重新绘制 surface 。
  • 在 while 循环中,编写一个侦听事件,并根据发送的事件执行对应的任务。为访问 pygame 检测到的事件,使用方法 pygame.event.get() 。所有的键盘和鼠标事件都将促使这个 for 循环运行。
  • pygame.display.flip() 调用是命令 pygame 使得最近绘制的屏幕可见。每次循环到最后都会绘制一个空屏幕,并擦去旧屏幕,使得新画面可见。
  • 最后一行的 run_game() 函数的调用,将初始化游戏并开始主循环。

设置背景色

pygame 默认创建一个黑色屏幕。下面将背景设置为另一种颜色:

import sys
import pygame

def run_game():
    # 初始化游戏并创建一个屏幕对象
    pygame.init()
    screen = pygame.display.set_mode((1200, 800))
    pygame.display.set_caption("Alien Invasion")
    
    # 设置背景色
    bg_color = (230, 0, 0)
    
    # 开始游戏的主循环
    while True:
        # 监视键盘和鼠标事件
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                sys.exit()
        
        # 每次循环都重新绘制屏幕
        screen.fill(bg_color)
        
        # 让最近绘制的屏幕可见
        pygame.display.flip()
        
run_game()

执行结果如下:

在这里插入图片描述在 pygame 中,颜色是以 RGB值指定的。上面代码中 bg_color = (230, 0, 0) 指定了一个红色。在主循环调用 screen.fill() 将使用背景色填充正个屏幕(这个方法只接受一个实参,一种颜色)。

创建设置类

编写一个名为 settings 的模块作为设置功能模块,其中包含一个名为 Settings 类,用于将所有设置存储在一个地方,以方便后续添加设置或者修改参数方便。

具体模块代码如下:

class Settings():
    '''存储《外星人大战》所有设置的类'''
    
    def __init__(self):
        # 初始化游戏的设置
        self.screen_width = 1200
        self.screen_height = 800
        self.bg_color = (230, 0, 0)

修改之前游戏框架代码,使用 Settings 类,如下:

import sys
import pygame
from settings import Settings

def run_game():
    # 初始化游戏并创建一个屏幕对象
    pygame.init()
    ai_settings = Settings()
    screen = pygame.display.set_mode((ai_settings.screen_width, ai_settings.screen_height))
    pygame.display.set_caption("Alien Invasion")
    
    # 设置背景色
    #bg_color = (230, 0, 0)
    
    # 开始游戏的主循环
    while True:
        # 监视键盘和鼠标事件
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                sys.exit()
        
        # 每次循环都重新绘制屏幕
        screen.fill(ai_settings.bg_color)
        
        # 让最近绘制的屏幕可见
        pygame.display.flip()
        
run_game()

执行结果如下:

在这里插入图片描述

添加飞船图像

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

再添加图片素材时需要注意版权问题,这里可以使用安全免费的网站,例如:飞船图片
在主目录下新建一个名为 images 文件夹,将飞船图片素材存放到文件夹内。如下图:

在这里插入图片描述

创建 Ship 类

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

import pygame

class Ship():
    # 管理飞船部分行为
    def __init__(self, screen):
        # 初始化飞船并设置其初始位置
        self.screen = screen
        
        # 加载飞船图像并获取其外接矩形
        self.image = pygame.image.load("images/ship.png")
        self.rect = self.image.get_rect()
        self.screen_rect = screen.get_rect()
        
        # 将每艘新产生的飞船放在屏幕底部正中央
        self.rect.centerx = self.screen_rect.centerx
        self.rect.bottom = self.screen_rect.bottom
        
    def blitme(self):
        # 在指定位置绘制飞船
        self.screen.blit(self.image, self.rect)
  • 使用 pygame.image.load() 加载图像,这个函数返回一个表示飞船的 surface, 这里将这个 surface 存储到 self.image
  • 加载图像后,使用 get_rect() 获取相应的 surface 的属性 rect 。pygame 的效率之所以如此高,一个原因是它让你能够像处理矩形(rect 对象)一样处理游戏元素,即便它们的形状并非矩形。像处理矩形一样处理游戏元素之所以高效,是因为矩形是简单的几何形状。这样做法的效果通常很好,游戏玩家几乎注意不到我们处理的不是游戏元素的实际形状。
  • 处理 rect 对象时,可使用矩形四角和中心的 x 和 y 坐标。可以通过设置这些值来指定矩形的位置。
  • 要将游戏元素居中,可设置相应的 rect 对象的属性 center 、 centerx 或 centery 。要使得游戏元素与屏幕边缘对齐,可使用属性 topbottomleftright
  • 将飞船放置到屏幕底部中央。首先将表示屏幕的矩形存储在 self.screen_rect 中,再将 self.rect.centerx 设置为表示屏幕的矩形的属性 centerx,并将 self.rect.bottom 设置为表示屏幕的矩形的属性 bottom 。pygame将使用这些 rect 属性来放置飞船图像,使其与屏幕下边缘对齐并水平居中。
  • 定义 blitme() ,根据 self.rect 指定的位置将图像绘制到屏幕上。

在屏幕上绘制飞船

更新前面编写的 alien_invasion.py ,使其创建一艘飞船,并调用其方法 blitme() 。具体代码修改如下:

import sys
import pygame
from settings import Settings
from ship import Ship

def run_game():
    # 初始化游戏并创建一个屏幕对象
    pygame.init()
    ai_settings = Settings()
    screen = pygame.display.set_mode((ai_settings.screen_width, ai_settings.screen_height))
    pygame.display.set_caption("Alien Invasion")
    
    # 设置背景色
    #bg_color = (230, 0, 0)
    
    # 创建一艘飞船
    ship = Ship(screen)
    
    # 开始游戏的主循环
    while True:
        # 监视键盘和鼠标事件
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                sys.exit()
        
        # 每次循环都重新绘制屏幕
        screen.fill(ai_settings.bg_color)
        ship.blitme()
        
        # 让最近绘制的屏幕可见
        pygame.display.flip()
        
run_game()

执行结果如下:

在这里插入图片描述

重构:模块 game_func

在大型的项目中,经常需要在添加新代码前重构已有的代码。重构的目的主要是为了简化已有的代码结构,使得其更容易扩展。这里,我们将创建一个名为 game_func 的新模块,存储大量让游戏《飞机大战》运行的函数,通过创建模块 game_func,可避免 alien_invasion.py 内容过多,并使得代码逻辑更加容易理解。

函数 check_events()

首先将管理事件的代码转移到名为 check_events() 的函数中,以简化 run_game() 并隔离事件管理循环。通过隔离事件循环,可以将事件管理与游戏的其他方面(如更新屏幕)分离。

创建一个 game_func 的新模块,具体内容如下:

import sys
import pygame

def check_events():
    # 响应按键和鼠标事件
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit()

下面修改之前的 alien_invasion.py ,使得其导入模块 game_func ,并将事件循环修改为对 check_events() 的调用。

import sys
import pygame
from settings import Settings
from ship import Ship
import game_func as gf

def run_game():
    # 初始化游戏并创建一个屏幕对象
    pygame.init()
    ai_settings = Settings()
    screen = pygame.display.set_mode((ai_settings.screen_width, ai_settings.screen_height))
    pygame.display.set_caption("Alien Invasion")
    
    # 设置背景色
    #bg_color = (230, 0, 0)
    
    # 创建一艘飞船
    ship = Ship(screen)
    
    # 开始游戏的主循环
    while True:
        # 监视键盘和鼠标事件
        #for event in pygame.event.get():
        #    if event.type == pygame.QUIT:
        #        sys.exit()
        gf.check_events()
        
        # 每次循环都重新绘制屏幕
        screen.fill(ai_settings.bg_color)
        ship.blitme()
        
        # 让最近绘制的屏幕可见
        pygame.display.flip()
        
run_game()

函数 update_screen()

进一步简化 run_game() ,下面将更新屏幕的代码转移到 update_screen() 的函数中,并将这个函数放入 game_func 模块中。添加代码如下:

import sys
import pygame

def check_events():
    # 响应按键和鼠标事件
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit()
            
def update_screen(ai_settings, screen, ship):
    # 更新屏幕上的图像,并切换到新屏幕
    # 每次循环时都重新绘制屏幕
    screen.fill(ai_settings.bg_color)
    ship.blitme()
    
    # 让最近绘制的屏幕可见
    pygame.display.flip()

修改 alien_invasion.py代码内容,具体内容如下:

import sys
import pygame
from settings import Settings
from ship import Ship
import game_func as gf

def run_game():
    # 初始化游戏并创建一个屏幕对象
    pygame.init()
    ai_settings = Settings()
    screen = pygame.display.set_mode((ai_settings.screen_width, ai_settings.screen_height))
    pygame.display.set_caption("Alien Invasion")
    
    # 创建一艘飞船
    ship = Ship(screen)
    
    # 开始游戏的主循环
    while True:
        # 监视键盘和鼠标事件
        gf.check_events()
        # 调用 update_screen() 更新屏幕
        gf.update_screen(ai_settings, screen, ship)
        

run_game()

操作飞船

下面来让玩家能够左右移动飞船。为此,将编写代码在用户操作按键时能够作出响应。这里,首先专注于处理向右移动,并用同样的方法来处理向左移动。

响应按键

每当用户按键时,都将在 pygame 中注册一个事件。事件都是通过方法 pygame.event.get() 获取的,因此在函数 check_evets() 中,这里我们需要制定要检查哪些类型的事件。每次按键都被注册为一个 KEYDOWN 事件。
检测到 KEYDOWN 事件时,我们需要检查按下的是否是特定的键。例如,如果按下的是右箭头,那么就增大飞船的 rect.centerx 值,将飞船向右移动。具体如下:(game_func.py中修改)

import sys
import pygame

def check_events(settings, ship):
    # 响应按键和鼠标事件
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit()
        
        elif event.type == pygame.KEYDOWN:
            if event.key == pygame.K_RIGHT:
                ship.rect.centerx += 50
                if ship.rect.centerx > settings.screen_width:
                    ship.rect.centerx -= 50
            if event.key == pygame.K_LEFT:
                ship.rect.centerx -= 50
                if ship.rect.centerx < 0:
                    ship.rect.centerx += 50
            
def update_screen(ai_settings, screen, ship):
    # 更新屏幕上的图像,并切换到新屏幕
    # 每次循环时都重新绘制屏幕
    screen.fill(ai_settings.bg_color)
    ship.blitme()
    
    # 让最近绘制的屏幕可见
    pygame.display.flip()

并且修改在 alien_invasion.py 中的 check_events() 函数调用,如下:

import sys
import pygame
from settings import Settings
from ship import Ship
import game_func as gf

def run_game():
    # 初始化游戏并创建一个屏幕对象
    pygame.init()
    ai_settings = Settings()
    screen = pygame.display.set_mode((ai_settings.screen_width, ai_settings.screen_height))
    pygame.display.set_caption("Alien Invasion")
    
    # 创建一艘飞船
    ship = Ship(screen)
    
    # 开始游戏的主循环
    while True:
        # 监视键盘和鼠标事件
        gf.check_events(ai_settings, ship)
        # 调用 update_screen() 更新屏幕
        gf.update_screen(ai_settings, screen, ship)
        

run_game()

执行如下:

移动到最右侧:
在这里插入图片描述移动到最左侧:

在这里插入图片描述

允许不断移动

前面修改过后,按键一次飞船响应一次,但是长按不动却不会响应,这样很不方便。可以添加检测 pygame.KEYUP 事件,以方便玩家在玩家在松开按键时能够检测到事件,结合使用 KEYDOWNKEYUP 事件以及一个名为 moving_right 的标志来实现持续移动。

飞船不动时,标志 moving_right 设为 False 。当按下按键时设置为 True ,当松开时再次设置为 False

ship.py 模块修改如下:

import pygame

class Ship():
    # 管理飞船部分行为
    def __init__(self, screen):
        # 初始化飞船并设置其初始位置
        self.screen = screen
        
        # 加载飞船图像并获取其外接矩形
        self.image = pygame.image.load("images/ship.png")
        self.rect = self.image.get_rect()
        self.screen_rect = screen.get_rect()
        
        # 将每艘新产生的飞船放在屏幕底部正中央
        self.rect.centerx = self.screen_rect.centerx
        self.rect.bottom = self.screen_rect.bottom
        
        # 移动标志
        self.moving_right = False
        self.moving_left = False
        
    def update(self):
        # 根据移动标志调整位置
        if self.moving_right:
            self.rect.centerx += 1
            if self.rect.centerx > self.screen_rect.width:
                self.rect.centerx -= 1
        if self.moving_left:
            self.rect.centerx -= 1
            if self.rect.centerx < 0:
                self.rect.centerx += 1
        
    def blitme(self):
        # 在指定位置绘制飞船
        self.screen.blit(self.image, self.rect)

修改 game_func 模块,具体代码如下:

import sys
import pygame

def check_events(settings, ship):
    # 响应按键和鼠标事件
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit()
        
        elif event.type == pygame.KEYDOWN:
            if event.key == pygame.K_RIGHT:
                ship.moving_right = True
                #ship.rect.centerx += 50
                #if ship.rect.centerx > settings.screen_width:
                #    ship.rect.centerx -= 50
            if event.key == pygame.K_LEFT:
                ship.moving_left = True
                #ship.rect.centerx -= 50
                #if ship.rect.centerx < 0:
                    #ship.rect.centerx += 50
        elif event.type == pygame.KEYUP:
            if event.key == pygame.K_RIGHT:
                ship.moving_right = False
            if event.key == pygame.K_LEFT:
                ship.moving_left = False
            
def update_screen(ai_settings, screen, ship):
    # 更新屏幕上的图像,并切换到新屏幕
    # 每次循环时都重新绘制屏幕
    screen.fill(ai_settings.bg_color)
    ship.blitme()
    
    # 让最近绘制的屏幕可见
    pygame.display.flip()

执行如下:

在这里插入图片描述

调整飞船的速度

当前,每次执行 while 循环时,飞船最多移动 1 个像素的位置,这个移动步长其实可以在 Settings 类中添加属性 ship_speed_factor 用于控制飞船的速度。这样就可以根据这个属性决定飞船每次循环时最多移动多少距离。具体代码如下:

class Settings():
    '''存储《外星人大战》所有设置的类'''
    
    def __init__(self):
        # 初始化游戏的设置
        self.screen_width = 1200
        self.screen_height = 800
        self.bg_color = (230, 0, 0)
        
        # 飞船的设置
        self.ship_speed_factor = 1.5

ship_speed_factor 的初始值设置成了 1.5 。需要移动飞船的时候,就会根据这个参数移动 1.5 而不是 1 。由于之前在飞船类中的 rect 的 centerx 等属性只能存储整数值,因此需要对 Ship 类做如下修改:

import pygame

class Ship():
    # 管理飞船部分行为
    def __init__(self, ai_settings, screen):
        # 初始化飞船并设置其初始位置
        self.screen = screen
        self.ai_settings = ai_settings
        
        # 加载飞船图像并获取其外接矩形
        self.image = pygame.image.load("images/ship.png")
        self.rect = self.image.get_rect()
        self.screen_rect = screen.get_rect()
        
        # 将每艘新产生的飞船放在屏幕底部正中央
        self.rect.centerx = self.screen_rect.centerx
        self.rect.bottom = self.screen_rect.bottom
        
        # 在飞船的属性center中存储小数值
        self.center = float(self.rect.centerx)
        
        # 移动标志
        self.moving_right = False
        self.moving_left = False
        
    def update(self):
        # 根据移动标志调整位置
        # 更新飞船的 center值而不是rect值
        if self.moving_right:
            #self.rect.centerx += 1
            self.center += self.ai_settings.ship_speed_factor
            if self.center > self.screen_rect.width:
                self.center -= self.ai_settings.ship_speed_factor
        if self.moving_left:
            #self.rect.centerx -= 1
            self.center -= self.ai_settings.ship_speed_factor
            if self.center < 0:
                self.center += self.ai_settings.ship_speed_factor
        self.rect.centerx = self.center
        
    def blitme(self):
        # 在指定位置绘制飞船
        self.screen.blit(self.image, self.rect)

修改前面 alien_invasion.py 文件中对于 Ship() 构造函数的调用为:ship = Ship(ai_settings, screen)

限制飞船活动范围

前面虽然也限制了飞船活动范围,不过前面是简单判断了一下,如果当前增加或者减少后飞船会离开屏幕,那么就放弃这次操作,代码如下:

class Ship():
	def update(self):
		if self.moving_right:
			self.center += self.ai_settings.ship_speed_factor
			if self.center > self.screen_rect.width:
				self.center -= self.ai_settings.ship_speed_factor
		if self.moving_left:
			self.center -= self.ai_settings.ship_speed_factor
			if self.center < 0:
				self.center += self.ai_settings.ship_speed_factor
		self.rect.centerx = self.center

优化后的代码如下:

class Ship():
    def update(self):
        if self.moving_right and self.rect.right < self.screen_rect.right:
            self.center += self.ai_settings.ship_speed_factor
        if self.moving_left and self.rect.left > 0:
            self.center -= self.ai_settings.ship_speed_factor
        self.rect.centerx = self.center

这样修改后,在这里的无用操作更少,并且飞船距离边界更清楚。此时运行程序后,飞船在触及屏幕边界时会停止继续向外移动。

修改后,执行程序如下图所示:

在这里插入图片描述

重构 check_events()

随着开发工作的进行,函数 check_events() 将越来越长,所以可以考虑将其部分代码放在两个函数中:

  • 一个处理 KEYDOWN 事件;
  • 一个处理 KEYUP 事件;

如下:

def check_events(settings, ship):
    # 响应按键和鼠标事件
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit()
        
        elif event.type == pygame.KEYDOWN:
            if event.key == pygame.K_RIGHT:
                ship.moving_right = True
                #ship.rect.centerx += 50
                #if ship.rect.centerx > settings.screen_width:
                #    ship.rect.centerx -= 50
            if event.key == pygame.K_LEFT:
                ship.moving_left = True
                #ship.rect.centerx -= 50
                #if ship.rect.centerx < 0:
                    #ship.rect.centerx += 50
        elif event.type == pygame.KEYUP:
            if event.key == pygame.K_RIGHT:
                ship.moving_right = False
            if event.key == pygame.K_LEFT:
                ship.moving_left = False

修改后,将其中按键按下处理和按键松开处理封装成对应函数,如下:

def check_keydown_events(event, ship):
    # 响应按键按下
    if event.key == pygame.K_RIGHT:
        ship.moving_right = True
    elif event.key == pygame.K_LEFT:
        ship.moving_left = True

def check_keyup_events(event, ship):
    # 响应按键松开
    if event.key == pygame.K_RIGHT:
        ship.moving_right = False
    elif event.key == pygame.K_LEFT:
        ship.moving_left = False
        
def check_events(ship):
    # 响应按键和鼠标事件 
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit()
        elif event.type == pygame.KEYDOWN:
            check_keydown_events(event, ship)
        elif event.type == pygame.KEYUP:
            check_keyup_events(event, ship)

射击

下面就开始添加射击功能。将编写玩家按下空格键时发射子弹(小矩形)的响应处理,子弹将在屏幕中向上穿行,抵达屏幕上边缘后消失。

添加子弹设置

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

class Settings():
    '''存储《外星人大战》所有设置的类'''
    
    def __init__(self):
        # 初始化游戏的设置
        self.screen_width = 1200
        self.screen_height = 800
        self.bg_color = (230, 0, 0)
        
        # 飞船的设置
        self.ship_speed_factor = 1.5
        
        # 子弹设置
        self.bullet_speed_factor = 1
        self.bullet_width = 3
        self.bullet_height = 15
        self.bullet_color = 60, 60, 60

创建 Bullet 类

下面创建 bullet 模块,编写子弹运动的 Bullet 类,具体如下:

import pygame
from pygame.sprite import Sprite

class Bullet(Sprite):
    # 一个对飞船发射的子弹进行管理的类
    
    def __init__(self, ai_settings, screen, ship):
        # 在飞船所处的位置创建一个子弹对象
        super(Bullet, self).__init__()
        self.screen = screen
        
        # 在 (0,0)处创建一个表示子弹的矩形,在设置正确的位置
        self.rect = pygame.Rect(0, 0, ai_settings.bullet_width, ai_settings.bullet_height)
        self.rect.centerx = ship.rect.centerx
        self.rect.top = ship.rect.top
        
        # 存储用小数表示的子弹位置
        self.y = float(self.rect.y)
        self.color = ai_settings.bullet_color
        self.speed_factor = ai_settings.bullet_speed_factor
        
    def update(self):
        # 向上移动子弹
        # 更新表示子弹的小数值
        self.y -= self.speed_factor
        # 更新表示子弹的rect位置
        self.rect.y = self.y
        
    def draw_bullet(self):
        # 在屏幕上绘制子弹
        pygame.draw.rect(self.screen, self.color, self.rect)

  • Bullet 类继承了从模块 pygame.sprite 模块中导入的 Sprite 类,通过使用精灵,可以将游戏中相关的元素编组,进而同时操作编组中的所有元素。为创建子弹实例,需要想 __init__() 传递 ai_settingsscreenship 实例。
  • 方法 update() 管理子弹的位置,放射子弹后,子弹在屏幕中向上移动,这意味着 y 的值不断减小,因此更新子弹的位置就可以看到子弹在移动。
  • 方法 draw_bullet() 绘制子弹,使用存储在 self.color 中的颜色填充子弹的 rect 占据屏幕部分。

将子弹存储到编组

定义 Bullet 类后,就可以编写代码。首先,将 alien_invasion.py 中创建一个编组(group),用于存储所有高效的子弹,以便能够管理发射出去的所有子弹。

Group 类其实类似于一个列表,方便存储精灵。具体修改如下(alien_invasion.py):

import sys
import pygame
from settings import Settings
from ship import Ship
import game_func as gf
from pygame.sprite import Group

def run_game():
    # 初始化游戏并创建一个屏幕对象
    pygame.init()
    ai_settings = Settings()
    screen = pygame.display.set_mode((ai_settings.screen_width, ai_settings.screen_height))
    pygame.display.set_caption("Alien Invasion")
    
    # 创建一艘飞船
    ship = Ship(ai_settings, screen)
    # 创建一个用于存储子弹的编组
    bullets = Group()
    
    # 开始游戏的主循环
    while True:
        # 监视键盘和鼠标事件
        gf.check_events(ai_settings, screen, ship, bullets)
        ship.update()
        bullets.update()
        # 调用 update_screen() 更新屏幕
        gf.update_screen(ai_settings, screen, ship, bullets)
        

run_game()

开火

game_func.py 中,需要修改 check_keydown_events() 代码,添加对空格键的响应操作,并且还需要修改 update_screen() ,以确保在调用 flip() 前在屏幕上已经完成了子弹重绘。具体修改如下:

def check_keydown_events(event, ai_settings, screen, ship, bullets):
    # 响应按键按下
    if event.key == pygame.K_RIGHT:
        ship.moving_right = True
    elif event.key == pygame.K_LEFT:
        ship.moving_left = True
    elif event.key == pygame.K_SPACE:
        # 创建一颗子弹,并将其加入编组bullets中
        new_bullet = Bullet(ai_settings, screen, ship)
        bullets.add(new_bullet)

          
def update_screen(ai_settings, screen, ship, bullets):
    # 更新屏幕上的图像,并切换到新屏幕
    # 每次循环时都重新绘制屏幕
    screen.fill(ai_settings.bg_color)
    
    # 在飞船和外星人后面重新绘制子弹
    for bullet in bullets.sprites():
        bullet.draw_bullet()
    ship.blitme()
    
    # 让最近绘制的屏幕可见
    pygame.display.flip()

执行效果如下图所示:

在这里插入图片描述

删除已消失的子弹

上面的操作完成后,子弹到达顶端后看上去是消失了,实际上是因为超出边界没有显示,其实子弹依然存在,只不过它们的 y 坐标值为负数,这样积攒过多对程序消耗和处理都是负担。所以需要将这些已经消失的子弹进行删除,其实就是检测子弹坐标条件,在超出上边界后将其删除即可。具体代码如下( alien_invasion.py ):

import sys
import pygame
from settings import Settings
from ship import Ship
import game_func as gf
from pygame.sprite import Group

def run_game():
    # 初始化游戏并创建一个屏幕对象
    pygame.init()
    ai_settings = Settings()
    screen = pygame.display.set_mode((ai_settings.screen_width, ai_settings.screen_height))
    pygame.display.set_caption("Alien Invasion")
    
    # 创建一艘飞船
    ship = Ship(ai_settings, screen)
    # 创建一个用于存储子弹的编组
    bullets = Group()
    
    # 开始游戏的主循环
    while True:
        # 监视键盘和鼠标事件
        gf.check_events(ai_settings, screen, ship, bullets)
        ship.update()
        bullets.update()
        # 删除已消失的子弹
        for bullet in bullets.copy():
            if bullet.rect.bottom <= 0:
                bullets.remove(bullet)
                print(len(bullets))
        # 调用 update_screen() 更新屏幕
        gf.update_screen(ai_settings, screen, ship, bullets)
        

run_game()

执行结果如下,从图中可以看出在子弹到达上边界后,列表中的数量就减少了。

在这里插入图片描述

限制子弹的数量

很多射击游戏都对可同时出现在屏幕上的子弹数量进行了限制,以鼓励玩家有目标地设计,这里也添加上这部分的限制。

首先,在 settings.py 中存储子弹最大限制数量参数,如下:

class Settings():
    '''存储《外星人大战》所有设置的类'''
    
    def __init__(self):
        # 初始化游戏的设置
        self.screen_width = 1200
        self.screen_height = 800
        self.bg_color = (230, 0, 0)
        
        # 飞船的设置
        self.ship_speed_factor = 1.5
        
        # 子弹设置
        self.bullet_speed_factor = 1
        self.bullet_width = 3
        self.bullet_height = 15
        self.bullet_color = 60, 60, 60
        self.bullets_allowed = 5

这里将同时存在的子弹数量设置为 5 ,如何在 game_func.py 中的 check_keydown_events() 对空格的响应增加子弹个数前进行条件判断,具体如下:

def check_keydown_events(event, ai_settings, screen, ship, bullets):
    # 响应按键按下
    if event.key == pygame.K_RIGHT:
        ship.moving_right = True
    elif event.key == pygame.K_LEFT:
        ship.moving_left = True
    elif event.key == pygame.K_SPACE:
        # 创建一颗子弹,并将其加入编组bullets中
        # 判断当前子弹数量
        if len(bullets) < ai_settings.bullets_allowed:
            new_bullet = Bullet(ai_settings, screen, ship)
            bullets.add(new_bullet)

创建函数 update_bullets()

编写并检查子弹管理代码后,可以将这部分代码封装到 game_func模块中,使得主程序尽可能简单,这里创建一个名为 update_bullets() 的函数,添加到 game_func模块中,具体如下:

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

如何修改 alien_invasion.py 模块中的 while 循环,如下:

import sys
import pygame
from settings import Settings
from ship import Ship
import game_func as gf
from pygame.sprite import Group

def run_game():
    # 初始化游戏并创建一个屏幕对象
    pygame.init()
    ai_settings = Settings()
    screen = pygame.display.set_mode((ai_settings.screen_width, ai_settings.screen_height))
    pygame.display.set_caption("Alien Invasion")
    
    # 创建一艘飞船
    ship = Ship(ai_settings, screen)
    # 创建一个用于存储子弹的编组
    bullets = Group()
    
    # 开始游戏的主循环
    while True:
        # 监视键盘和鼠标事件
        gf.check_events(ai_settings, screen, ship, bullets)
        ship.update()
        # 调用 update_bullets() 更新子弹
        gf.update_bullets(bullets)
        # 调用 update_screen() 更新屏幕
        gf.update_screen(ai_settings, screen, ship, bullets)
        

run_game()

创建函数 fire_bullet()

下面将发射子弹的代码转移到单独的函数中,这样在 check_keydown_events() 中只需要使用一行函数调用即可完成发射子弹操作。

具体如下( game_func.py ):

def fire_bullet(ai_settings, screen, ship, bullets):
    # 创建一个新子弹,并加入编组bullets
    # 判断当前子弹数量
    if len(bullets) < ai_settings.bullets_allowed:
        new_bullet = Bullet(ai_settings, screen, ship)
        bullets.add(new_bullet)

def check_keydown_events(event, ai_settings, screen, ship, bullets):
    # 响应按键按下
    if event.key == pygame.K_RIGHT:
        ship.moving_right = True
    elif event.key == pygame.K_LEFT:
        ship.moving_left = True
    elif event.key == pygame.K_SPACE:
        # 创建一颗子弹,并将其加入编组bullets中
        fire_bullet(ai_settings, screen, ship, bullets)
        

添加程序退出快捷键响应

game_func.py 模块中添加对 Q 按键的退出响应,具体如下:

def check_keydown_events(event, ai_settings, screen, ship, bullets):
    # 响应按键按下
    if event.key == pygame.K_RIGHT:
        ship.moving_right = True
    elif event.key == pygame.K_LEFT:
        ship.moving_left = True
    elif event.key == pygame.K_SPACE:
        # 创建一颗子弹,并将其加入编组bullets中
        fire_bullet(ai_settings, screen, ship, bullets)
    elif event.key == pygame.K_q:
        # 退出程序
        sys.exit()

创建第一个外星人

在屏幕上放置敌方外星人和放置飞船类似,每个外星人的行为都由 Alien 类控制,我们将像创建 Ship 类那样创建这个类,简化考虑,使用位图表示外星人,下载外星人的图片文件,下载链接:外星人图片下载

将下载好的图片放置到 images 目录下,命名为 alien.png 如下图所示:

在这里插入图片描述

创建 alien.py 模块,具体代码如下:

import pygame
from pygame.sprite import Sprite

class Alien(Sprite):
    # 表示外星人的类
    
    def __init__(self, ai_settings, screen):
        # 初始化外星人并设置其初始位置
        super(Alien, self).__init__()
        self.screen = screen
        self.ai_settings = ai_settings
        
        # 加载外星人图像,并设置rect属性
        self.image = pygame.image.load('images/alien.png')
        self.rect = self.image.get_rect()
        
        # 每个外星人最初在屏幕左上角出现
        self.rect.x = self.rect.width
        self.rect.y = self.rect.height
        
    def blitme(self):
        # 在指定位置绘制外星人
        self.screen.blit(self.image, self.rect)

创建 Alien 实例

下面在 alien_invasion.py 中创建一个 Alien 实例:

import sys
import pygame
from settings import Settings
from ship import Ship
from alien import Alien
import game_func as gf
from pygame.sprite import Group


def run_game():
    # 初始化游戏并创建一个屏幕对象
    pygame.init()
    ai_settings = Settings()
    screen = pygame.display.set_mode((ai_settings.screen_width, ai_settings.screen_height))
    pygame.display.set_caption("Alien Invasion")
    
    # 创建一艘飞船
    ship = Ship(ai_settings, screen)
    # 创建一个用于存储子弹的编组
    bullets = Group()
    # 创建一个外星人
    alien = Alien(ai_settings, screen)
    
    # 开始游戏的主循环
    while True:
        # 监视键盘和鼠标事件
        gf.check_events(ai_settings, screen, ship, bullets)
        ship.update()
        # 调用 update_bullets() 更新子弹
        gf.update_bullets(bullets)
        # 调用 update_screen() 更新屏幕
        gf.update_screen(ai_settings, screen, ship, bullets)
        

run_game()

并且修改其中调用函数 updaget_screen(),如下:

def update_screen(ai_settings, screen, ship, alien, bullets):
    # 更新屏幕上的图像,并切换到新屏幕
    # 每次循环时都重新绘制屏幕
    screen.fill(ai_settings.bg_color)
    
    # 在飞船和外星人后面重新绘制子弹
    for bullet in bullets.sprites():
        bullet.draw_bullet()
    ship.blitme()
    alien.blitme()
    
    # 让最近绘制的屏幕可见
    pygame.display.flip()

运行之后,如下图:

在这里插入图片描述

创建一群外星人

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

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

为了确定一行可容纳多少个外星人,了解可用的水平空间大小。屏幕宽度存储在 ai_settings.screen_width 中,但是两边还是需要留下一定的空袭,因此可以用放置外星人的水平控件定为屏幕宽度减去外星人宽度的两倍。

# 可用空间宽度
available_space_x = ai_settings.screen_width - (2 * alien_width)
# 单行可容纳外星人个数
number_aliens_x = available_space_x / (2 * alien_width)

创建多行外星人

为创建一行外星人,首先在 alien_ivasion.py 中创建一个名为 aliens 的空编组,用于存储全部外星人。再调用 game_func.py 模块中创建外星人群的函数。

game_func.py 模块中创建外星人群函数如下:


def create_fleet(ai_settings, screen, aliens):
    # 创建外星人群
    # 创建一个外星人,并计算一行可容纳多少个外星人
    # 外星人间距为外星人宽度
    alien = Alien(ai_settings, screen)
    alien_width = alien.rect.width
    available_space_x = ai_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(ai_settings, screen)
        alien.x = alien_width + 2 * alien_width * alien_number
        alien.rect.x = alien.x
        aliens.add(alien)

            
def update_screen(ai_settings, screen, ship, aliens, bullets):
    # 更新屏幕上的图像,并切换到新屏幕
    # 每次循环时都重新绘制屏幕
    screen.fill(ai_settings.bg_color)
    
    # 在飞船和外星人后面重新绘制子弹
    for bullet in bullets.sprites():
        bullet.draw_bullet()
    ship.blitme()
    aliens.draw(screen)
    
    # 让最近绘制的屏幕可见
    pygame.display.flip()

修改 alien_invasion.py 中的调用,具体如下:

import sys
import pygame
from settings import Settings
from ship import Ship
from alien import Alien
import game_func as gf
from pygame.sprite import Group


def run_game():
    # 初始化游戏并创建一个屏幕对象
    pygame.init()
    ai_settings = Settings()
    screen = pygame.display.set_mode((ai_settings.screen_width, ai_settings.screen_height))
    pygame.display.set_caption("Alien Invasion")
    
    # 创建一艘飞船
    ship = Ship(ai_settings, screen)
    # 创建一个用于存储子弹的编组
    bullets = Group()
    # 创建外星人群
    aliens = Group()
    gf.create_fleet(ai_settings, screen, aliens)
    
    
    # 开始游戏的主循环
    while True:
        # 监视键盘和鼠标事件
        gf.check_events(ai_settings, screen, ship, bullets)
        ship.update()
        # 调用 update_bullets() 更新子弹
        gf.update_bullets(bullets)
        # 调用 update_screen() 更新屏幕
        gf.update_screen(ai_settings, screen, ship, aliens, bullets)
        

run_game()

执行如下图所示:

在这里插入图片描述

重构 create_fleet()

这时候能看出外星人都在屏幕上靠左边,这样就可以向右移动,触及屏幕边缘后下移再向左移动,依次类推。

下面是 create_fleet() 和新创建的两个函数 aliens_x()create_alien()

def get_number_aliens_x(ai_settings, alien_width):
    # 计算每行可容纳的外星人个数
    available_space_x = ai_settings.screen_width - 2 * alien_width
    number_aliens_x = int(available_space_x / (2 * alien_width))
    return number_aliens_x
    
def create_alien(ai_settings, screen, aliens, alien_number):
    # 创建一个外星人并将其放在当前行
    alien = Alien(ai_settings, screen)
    alien_width = alien.rect.width
    alien.x = alien_width + 2 * alien_width * alien_number
    alien.rect.x = alien.x
    alien.add(alien)
    
def create_fleet(ai_settings, screen, aliens):
    # 创建外星人群
    # 创建一个外星人,并计算一行可容纳多少个外星人
    # 外星人间距为外星人宽度
    alien = Alien(ai_settings, screen)
    number_aliens_x = get_number_aliens_x(ai_settings, alien_width)
    
    # 创建第一行外星人
    for alien_number in range(number_aliens_x):
        # 创建第一个外星人并加入当前行
        create_alien(ai_settings, screen, aliens, alien_number)
            

添加行

要创建外星人群,首先就需要计算屏幕可容纳多少行,并对创建一行外星人的循环重复相应的次数。为计算可容纳的行数,需要计算当前屏幕可用垂直空间:

  • 将屏幕高度减去第一行外星人的高度,飞船的高度以及最初外星人群与飞船的距离(外星人高度的两倍)
available_space_y = ai_settings.screen_height - 3 * alien_height - ship_height

这样计算将会在飞船上方留出一定的空白区域,给玩家留出射杀外星人的时间。所以行数计算如下:

number_rows = available_height_y / (2 * alien_height)

知道可容纳多少行后,便可以重复执行创建一行外星人的代码( game_func.py ):

 
def get_number_rows(ai_settings, ship_height, alien_height):
    # 计算屏幕可容纳多少行外星人
    available_space_y = (ai_settings.screen_height - (3 * alien_height) - ship_height)
    number_rows = int(available_space_y / (2 * alien_height))
    return number_rows
    
def create_alien(ai_settings, screen, aliens, alien_number, row_number):
    # 创建一个外星人并将其放在当前行
    alien = Alien(ai_settings, screen)
    alien_width = alien.rect.width
    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
    aliens.add(alien)
    
def create_fleet(ai_settings, screen, ship, aliens):
    # 创建外星人群
    # 创建一个外星人,并计算一行可容纳多少个外星人
    # 外星人间距为外星人宽度
    alien = Alien(ai_settings, screen)
    number_aliens_x = get_number_aliens_x(ai_settings, alien.rect.width)
    number_rows = get_number_rows(ai_settings, ship.rect.height, alien.rect.height)
    
    # 创建外星人群
    for row_number in range(number_rows):
    # 创建第一行外星人
        for alien_number in range(number_aliens_x):
            # 创建第一个外星人并加入当前行
            create_alien(ai_settings, screen, aliens, alien_number, row_number)
            

执行后可以看到添加了一群外星人,如下图所示:

在这里插入图片描述

让外星人移动

接下来就是让外星人群在屏幕上向右移动,在接触到屏幕边缘后下移一定距离,再向相反方向移动,不断移动所有的外星人,知道都被消灭;有外星人撞到飞船,或者有外星人抵达屏幕最低端则游戏结束。

向右移动外星人

为移动外星人,可以调用 alien.py 中的 update() 方法,需要先在 settings.py 模块中添加外星人移动速度参数,如下:

class Settings():
    '''存储《外星人大战》所有设置的类'''
    
    def __init__(self):
        # 初始化游戏的设置
        self.screen_width = 1200
        self.screen_height = 800
        self.bg_color = (230, 0, 0)
        
        # 飞船的设置
        self.ship_speed_factor = 1.5
        
        # 子弹设置
        self.bullet_speed_factor = 1
        self.bullet_width = 3
        self.bullet_height = 15
        self.bullet_color = 60, 60, 60
        self.bullets_allowed = 5
        
        # 外星人设置
        self.alien_speed_factor = 1

然后设置 alien.py 中的 update() 实现,如下:

    def update(self):
        # 向右移动外星人
        self.x += self.ai_settings.alien_speed_factor
        self.rect.x = self.x

game_func.py 模块中添加 update_aliens() 方法用于更新外星人组的位置,如下:


def update_aliens(aliens):
    # 更新外星人群中所有的外星人的位置
    # 对编组调用方法update(),会自动对每个组内成员调用update()
    aliens.update()

在主循环中添加使用更新外星人位置的方法,具体如下:

import sys
import pygame
from settings import Settings
from ship import Ship
from alien import Alien
import game_func as gf
from pygame.sprite import Group


def run_game():
    # 初始化游戏并创建一个屏幕对象
    pygame.init()
    ai_settings = Settings()
    screen = pygame.display.set_mode((ai_settings.screen_width, ai_settings.screen_height))
    pygame.display.set_caption("Alien Invasion")
    
    # 创建一艘飞船
    ship = Ship(ai_settings, screen)
    # 创建一个用于存储子弹的编组
    bullets = Group()
    # 创建外星人群
    aliens = Group()
    gf.create_fleet(ai_settings, screen, ship, aliens)
    
    
    # 开始游戏的主循环
    while True:
        # 监视键盘和鼠标事件
        gf.check_events(ai_settings, screen, ship, bullets)
        ship.update()
        # 调用 update_bullets() 更新子弹
        gf.update_bullets(bullets)
        # 调用 update_aliens() 更新外星人位置
        gf.update_aliens(aliens)
        # 调用 update_screen() 更新屏幕
        gf.update_screen(ai_settings, screen, ship, aliens, bullets)
        

run_game()

此时还没有添加移动到右端限制,所以外星人会始终右移,即使离开屏幕后还是会不断增加 x 的大小。

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

下面创建使外星人在接触右边缘后向下移动、再向左移动的设置。在 settings.py 模块中添加参数:

class Settings():
    '''存储《外星人大战》所有设置的类'''
    
    def __init__(self):
        # 初始化游戏的设置
        self.screen_width = 1200
        self.screen_height = 800
        self.bg_color = (230, 0, 0)
        
        # 飞船的设置
        self.ship_speed_factor = 1.5
        
        # 子弹设置
        self.bullet_speed_factor = 1
        self.bullet_width = 3
        self.bullet_height = 15
        self.bullet_color = 60, 60, 60
        self.bullets_allowed = 5
        
        # 外星人设置
        self.alien_speed_factor = 1
        # 遇到边沿后向下移动的速度
        self.fleet_drop_speed = 10
        # fleet_direction :1-表示向右移动,-1 表示向左移动
        self.fleet_direction = 1

添加外星人是否撞到边缘的判断

现在向 alien.py 中添加检测边缘的代码,如下:

    def update(self):
        # 向右移动外星人
        self.x += (self.ai_settings.alien_speed_factor * self.ai_settings.fleet_direction)
        self.rect.x = self.x
        
    def check_edges(self):
        # 如果接触边缘,则返回 True
        screen_rect = self.screen.get_rect()
        if self.rect.right >= screen_rect.right:
            return True
        elif self.rect.left <= 0:
            return True
        else
            return False

实现向下移动并改变方向

有外星人到达屏幕边沿时,需要将其下移并改变移动方向。为此,可以向 game_func() 模块中添加判断边缘函数 check_fleet_edges()change_fleet_direction() 。具体如下:

def check_fleet_edges(ai_settings, aliens):
    # 有外星人到达边缘时采取相应措施
    for alien in aliens:
        if alien.check_edges():
            change_fleet_direction(ai_settings, aliens)
            break

def change_fleet_direction(ai_settings, aliens):
    # 整群外星人下移,并改变移动方向
    for alien in aliens:
        alien.rect.y += ai_settings.fleet_drop_speed
    ai_settings.fleet_direction *= -1
    
def update_aliens(ai_settings, aliens):
    # 更新外星人群中所有的外星人的位置
    check_fleet_edges(ai_settings, aliens)
    aliens.update()


修改 ai_invasion.py 中的相关调用,之前调用 gf.update_aliens() 的传递参数部分,gf.update_aliens(ai_settings, aliens) 。修改完成后执行效果如下:

在这里插入图片描述

射杀外星人

在创建完成飞船及操作响应和外星人移动后,就需要添加当子弹击中外星人后的响应操作。在游戏编程中,子弹碰撞到外星人其实就是两个游戏元素产生了重叠,可以采用 sprite.groupcollide() 方法来检测两个编组成员之间的碰撞。

检测子弹与外星人的碰撞

首先添加坚持子弹与外星人直接的碰撞,如果发生碰撞立刻使被碰撞的外星人消失,为此,可以在更新子弹位置后立刻检测碰撞并做出响应。

方法 sprite.groupcollide() 将每颗子弹的 rect 同每个外星人的 rect 进行比较,并返回一个字典,其中包含了碰撞的子弹和外星人。在这个字典中,每个键都是一颗子弹,而对应的值就是发生碰撞的外星人。

具体在 game_func.py 模块中的 update_bullets() 中添加如下代码:

def update_bullets(aliens, bullets):
    # 更新子弹的位置
    bullets.update()
    
    # 删除已消失的子弹
    for bullet in bullets.copy():
        if bullet.rect.bottom <= 0:
            bullets.remove(bullet)
            
    # 检查是否有子弹与外星人发送碰撞
    #如果发生碰撞,删除发送碰撞的子弹和外星人
    collisions = pygame.sprite.groupcollide(bullets, aliens, True, True)

其中,collisions = pygame.sprite.groupcollide(bullets, aliens, True, True)groupcollide 会返回字典,后面的两个实参 True 表示当检测到发生碰撞时会自动移除组内发送碰撞的元素。修改相应的参数调用并执行,如下:

在这里插入图片描述

为测试方便添加大招子弹

只需要通过运行这个游戏就可以测试其中的很多功能,但是有些时候需要花很长的时间才能击落外星人进行测试,这很浪费时间,为了方便起见,可以将子弹的宽度设置很大,这样就能一次操作后将所有的外星人都击落了。

settings.py 模块中将子弹宽度设置与屏幕同宽,如下:

        # 子弹设置
        self.bullet_speed_factor = 1
        self.bullet_width = 1200
        self.bullet_height = 15
        self.bullet_color = 60, 60, 60
        self.bullets_allowed = 5
        

game_func.py 中的碰撞检测的调用修改为 collisions = pygame.sprite.groupcollide(bullets, aliens, False, True) ,这样子弹在击落外星人后会继续存在,效果如下:

在这里插入图片描述

生成新的外星人群

当一个外星人群被消灭之后,又会出现新的一群外星人。所以需要在外星人群被消灭后再次添加一群外星人,这就需要检查编组 aliens 是否为空,如果为空则再次调用 create_fleet() ,这一步操作添加到 update_bullets() 中进行,因为外星人是在这里被消灭的,如下:

def update_bullets(ai_settings, screen, ship, aliens, bullets):
    # 更新子弹的位置
    bullets.update()
    
    # 删除已消失的子弹
    for bullet in bullets.copy():
        if bullet.rect.bottom <= 0:
            bullets.remove(bullet)
            
    # 检查是否有子弹与外星人发送碰撞
    #如果发生碰撞,删除发送碰撞的子弹和外星人
    collisions = pygame.sprite.groupcollide(bullets, aliens, False, True)
    
    # 检查是否需要再次添加外星人
    if len(aliens) == 0:
        # 删除现有子弹并新建新的外星人群
        bullets.empty()
        create_fleet(ai_settings, screen, ship, aliens)
      

改动后,效果如下:

在这里插入图片描述

提高子弹速度

当外星人数量多了之后,可能会发现子弹速度比之前更慢了,这是因为在每次循环后,pygame 需要做更多工作了,可以修改 settings.py 模块中的子弹速度参数来提高子弹的速度。

        # 子弹设置
        self.bullet_speed_factor = 1
        self.bullet_width = 1200
        self.bullet_height = 15
        self.bullet_color = 60, 60, 60
        self.bullets_allowed = 5

重构 update_bullets()

接下来重新构造 update_bullets() 函数,使其不再需要完成那么多任务,将处理子弹和外星人碰撞的代码单独封装,如下:

def update_bullets(ai_settings, screen, ship, aliens, bullets):
    # 更新子弹的位置
    bullets.update()
    
    # 删除已消失的子弹
    for bullet in bullets.copy():
        if bullet.rect.bottom <= 0:
            bullets.remove(bullet)
            
    # 检查是否有子弹与外星人发送碰撞
    check_bullet_alien_collision(ai_settings, screen, ship, aliens, bullets)
    
            
check_bullet_alien_collision(ai_settings, screen, ship, aliens, bullets):
    # 删除发生碰撞的子弹和外星人
    collisions = pygame.sprite.groupcollide(bullets, aliens, False, True)
    # 检查是否需要再次添加外星人
    if len(aliens) == 0:
        # 删除现有子弹并新建新的外星人群
        bullets.empty()
        create_fleet(ai_settings, screen, ship, aliens)
  

结束游戏

如果玩家没能够在足够短的时间内将全部外星人消灭,并且有外星人撞到飞船或者到达底端时,飞船将被摧毁。当玩家用光飞船后,游戏也就结束了。

检测外星人和飞船碰撞

我们首先检查外星人与飞船之间的碰撞,以便对此作出合适的响应。在每次更新外星人位置后立即检测外星人与飞船之间的碰撞。

def update_aliens(ai_settings, ship, aliens):
    # 更新外星人群中所有的外星人的位置
    check_fleet_edges(ai_settings, aliens)
    aliens.update()
    
    # 检测外星人和飞船之间的碰撞
    if pygame.sprite.spritecollideany(ship, aliens):
        print("Ship hit!!!")

修改后,当外星人和飞船发生碰撞后就会产生打印消息的处理,如下:

在这里插入图片描述

响应外星人和飞船碰撞

前面只是对外星人和飞船的碰撞进行检测,当检测到时也只是单纯作出打印处理,并没有做其他处理。在真实游戏中,发生碰撞后,并不销毁当前 ship 实例再创建一个新的实例,而是对撞击次数进行统计。

接下来就需要编写一个用于游戏统计信息的新类—— GameStats ,存放在模块 game_stats.py 中,具体如下:

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

    def reset_stats(self):
        # 初始化在游戏运行中可能变化的统计信息
        # 飞船限制次数
        self.ships_left = self.ai_settings.ship_limit

并且在 settings.py 模块中添加飞船次数限制参数 ship_limit,如下:

        # 飞船的设置
        self.ship_speed_factor = 1.5
        self.ship_limit = 3

此外,还需要对 alien_invasion.py 做一些修改,以创建 GameStats 实例:

import sys
import pygame
from settings import Settings
from ship import Ship
from alien import Alien
from game_stats import GameStats
import game_func as gf
from pygame.sprite import Group


def run_game():
    # 初始化游戏并创建一个屏幕对象
    pygame.init()
    ai_settings = Settings()
    screen = pygame.display.set_mode((ai_settings.screen_width, ai_settings.screen_height))
    pygame.display.set_caption("Alien Invasion")
    
    # 创建用于存储游戏统计信息的实例
    stats = GameStats(ai_settings)
    # 创建一艘飞船
    ship = Ship(ai_settings, screen)
    # 创建一个用于存储子弹的编组
    bullets = Group()
    # 创建外星人群
    aliens = Group()
    gf.create_fleet(ai_settings, screen, ship, aliens)
    
    
    # 开始游戏的主循环
    while True:
        # 监视键盘和鼠标事件
        gf.check_events(ai_settings, screen, ship, bullets)
        ship.update()
        # 调用 update_bullets() 更新子弹
        gf.update_bullets(ai_settings, screen, ship, aliens, bullets)
        # 调用 update_aliens() 更新外星人位置
        gf.update_aliens(ai_settings, stats, screen, ship, aliens, bullets)
        # 调用 update_screen() 更新屏幕
        gf.update_screen(ai_settings, screen, ship, aliens, bullets)
        

run_game()

可以看到已经对 gf.update_aliens(ai_settings, stats, screen, ship, aliens, bullets) 调用进行了修改,所以还需要修改其具体实现:

def ship_hit(ai_settings, stats, screen, ship, aliens, bullets):
    # 响应外星人撞到飞船
    # 将 ships_left 减 1
    stats.ships_left -= 1
    
    # 清空外星人列表和子弹列表,相当于清屏
    aliens.empty()
    bullets.empty()
    
    # 创建一群新的外星人,并将飞船放置到屏幕底部中央
    create_fleet(ai_settings, screen, ship, aliens)
    ship.center_ship()
    
    # 暂停
    sleep(0.5)

def update_aliens(ai_settings, stats, screen, ship, aliens, bullets):
    # 更新外星人群中所有的外星人的位置
    check_fleet_edges(ai_settings, aliens)
    aliens.update()
    
    # 检测外星人和飞船之间的碰撞
    if pygame.sprite.spritecollideany(ship, aliens):
        ship_hit(ai_settings, stats, screen, ship, aliens, bullets)

并且在 ship.py 模块中添加 center_ship() 方法,如下:

class Ship():
    def center_ship(self):
        # 居中飞船位置
        self.center = self.screen_rect.centerx

运行后如下效果:

在这里插入图片描述

确定运行游戏的哪些部分

alien_invasion.py 中,需要确定游戏的哪些部分在任何情况下都运行,哪些部分仅在游戏处于活动状态才运行:

修改 alien_invasion.py ,如下:

import sys
import pygame
from settings import Settings
from ship import Ship
from alien import Alien
from game_stats import GameStats
import game_func as gf
from pygame.sprite import Group


def run_game():
    # 初始化游戏并创建一个屏幕对象
    pygame.init()
    ai_settings = Settings()
    screen = pygame.display.set_mode((ai_settings.screen_width, ai_settings.screen_height))
    pygame.display.set_caption("Alien Invasion")
    
    # 创建用于存储游戏统计信息的实例
    stats = GameStats(ai_settings)
    # 创建一艘飞船
    ship = Ship(ai_settings, screen)
    # 创建一个用于存储子弹的编组
    bullets = Group()
    # 创建外星人群
    aliens = Group()
    gf.create_fleet(ai_settings, screen, ship, aliens)
    
    
    # 开始游戏的主循环
    while True:
        # 监视键盘和鼠标事件
        gf.check_events(ai_settings, screen, stats, ship, bullets)
        
        if stats.game_active:
            ship.update()
            # 调用 update_bullets() 更新子弹
            gf.update_bullets(ai_settings, screen, ship, aliens, bullets)
            # 调用 update_aliens() 更新外星人位置
            gf.update_aliens(ai_settings, stats, screen, ship, aliens, bullets)
        # 调用 update_screen() 更新屏幕
        gf.update_screen(ai_settings, screen, ship, aliens, bullets)
        

run_game()

并且修改 game_func.py 模块中的 check_events() 方式的处理,添加对 t 按键的暂停和继续的响应。


def check_keydown_events(event, ai_settings, screen, stats, ship, bullets):
    # 响应按键按下
    if event.key == pygame.K_RIGHT:
        ship.moving_right = True
    elif event.key == pygame.K_LEFT:
        ship.moving_left = True
    elif event.key == pygame.K_SPACE:
        # 创建一颗子弹,并将其加入编组bullets中
        fire_bullet(ai_settings, screen, ship, bullets)
    elif event.key == pygame.K_q:
        # 退出程序
        sys.exit()
    elif event.key == pygame.K_t:
        # 暂停游戏或者继续游戏
        if stats.game_active:
            stats.game_active = False
        else:
            stats.game_active = True
        

def check_keyup_events(event, ship):
    # 响应按键松开
    if event.key == pygame.K_RIGHT:
        ship.moving_right = False
    elif event.key == pygame.K_LEFT:
        ship.moving_left = False
        
def check_events(ai_settings, screen, stats, ship, bullets):
    # 响应按键和鼠标事件 
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit()
        elif event.type == pygame.KEYDOWN:
            check_keydown_events(event, ai_settings, screen, stats, ship, bullets)
        elif event.type == pygame.KEYUP:
            check_keyup_events(event, ship)
        
          

添加 Play 按钮

接下来,我们将添加一个 Play 按钮,在游戏开始前出现,并在结束后再次出现,这样可以使得玩家能够再次开始新游戏。

当前,玩家在运行 alien_invasion.py 时就开始了。下面让游戏一开始处于非活动状态,并提示玩家单击 Play 按钮来开始游戏。为此,需要在 game_stats.py 模块中添加一下代码:

class GameStats():
    # 跟踪游戏的统计信息
    
    def __init__(self, ai_settings):
        # 初始化统计信息
        self.ai_settings = ai_settings
        self.reset_stats()
        # 让游戏一开始处于非活动状态
        self.game_active = False

    def reset_stats(self):
        # 初始化在游戏运行中可能变化的统计信息
        # 飞船限制次数
        self.ships_left = self.ai_settings.ship_limit

创建 Button 类

由于 pygame 没用内置创建按钮的方法,所以需要构建一个 Button 类,用于创建带标签的实心矩形,可以在游戏中使用这些代码来创建任何按钮。

创建文件 button.py ,在其中添加如下代码:

import pygame.font

class Button():
    # 创建带有标签的实心矩形
    
    def __init__(self, ai_settings, screen, msg):
        # 初始化按钮属性
        self.screen = screen
        self.screen_rect = screen.get_rect()
        
        # 设置按钮的尺寸和其他属性
        self.width, self.height = 200, 500
        self.button_color = (0, 255, 0)
        self.text_color = (255, 255, 255)
        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)
        
    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
        
    def draw_button(self):
        # 绘制一个用颜色填充的按钮,在绘制其中文本
        self.screen.fill(self.button_color, self.rect)
        self.screen.blit(self.msg_image, self.msg_image_rect)

在屏幕上绘制按钮

接下来就是使用 Button 类在屏幕上创建一个 Play 按钮,直接在 alien_invasion.py 文件中创建,如下所示:

import sys
import pygame
from settings import Settings
from ship import Ship
from alien import Alien
from button import Button
from game_stats import GameStats
import game_func as gf
from pygame.sprite import Group


def run_game():
    # 初始化游戏并创建一个屏幕对象
    pygame.init()
    ai_settings = Settings()
    screen = pygame.display.set_mode((ai_settings.screen_width, ai_settings.screen_height))
    pygame.display.set_caption("Alien Invasion")
    
    # 创建play按钮
    play_button = Button(ai_settings, screen, "Play")
    
    # 创建用于存储游戏统计信息的实例
    stats = GameStats(ai_settings)
    # 创建一艘飞船
    ship = Ship(ai_settings, screen)
    # 创建一个用于存储子弹的编组
    bullets = Group()
    # 创建外星人群
    aliens = Group()
    gf.create_fleet(ai_settings, screen, ship, aliens)
    
    
    # 开始游戏的主循环
    while True:
        # 监视键盘和鼠标事件
        gf.check_events(ai_settings, screen, stats, ship, bullets)
        
        if stats.game_active:
            ship.update()
            # 调用 update_bullets() 更新子弹
            gf.update_bullets(ai_settings, screen, ship, aliens, bullets)
            # 调用 update_aliens() 更新外星人位置
            gf.update_aliens(ai_settings, stats, screen, ship, aliens, bullets)
        # 调用 update_screen() 更新屏幕
        gf.update_screen(ai_settings, screen, stats, ship, aliens, bullets, play_button)
        

run_game()

修改 update_screen(ai_settings, screen, ship, aliens, bullets, play_button) 实现,如下:

def update_screen(ai_settings, screen, stats, ship, aliens, bullets, play_button):
    # 更新屏幕上的图像,并切换到新屏幕
    # 每次循环时都重新绘制屏幕
    screen.fill(ai_settings.bg_color)
    
    # 在飞船和外星人后面重新绘制子弹
    for bullet in bullets.sprites():
        bullet.draw_bullet()
    ship.blitme()
    aliens.draw(screen)
    
    # 如果游戏处于非活动状态,则绘制Play按钮
    if not stats.game_active:
        play_button.draw_button()
    
    # 让最近绘制的屏幕可见
    pygame.display.flip()

此时运行,开始画面如下:

在这里插入图片描述

开始游戏

为了在玩家点击 Play 按钮时开始新游戏,需要在 game_func.py 模块中添加一下代码,以实现与这个按钮相关的鼠标事件(game_func.py):

def check_play_button(stats, play_button, mouse_x, mouse_y):
    # 在玩家单击play按钮时开始游戏
    if play_button.rect.collidepoint(mouse_x, mouse_y):
        stats.game_active = True
    
def check_events(ai_settings, screen, stats, ship, bullets):
    # 响应按键和鼠标事件 
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit()
        elif event.type == pygame.KEYDOWN:
            check_keydown_events(event, ai_settings, screen, stats, ship, bullets)
        elif event.type == pygame.KEYUP:
            check_keyup_events(event, ship)
        elif event.type == pygame.MOUSEBUTTONDOWN:
            # 鼠标按下事件
            mouse_x, mouse_y = pygame.mouse.get_pos()
            check_play_button(stats, play_button, mouse_x, mouse_y)
        

并修改调用部分的参数传递,gf.check_events(ai_settings, screen, stats, play_button, ship, bullets) ,修改后运行如下:

在这里插入图片描述

重置游戏

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

为了在玩家每次单击 Play 按钮时都重置游戏,需要重置统计信息、删除现有的外星人和子弹,创建一群新的外星人,并使得飞船居中,修改如下(game_func.py):

def check_play_button(ai_settings, screen, stats, play_button, ship, aliens, bullets, mouse_x, mouse_y):
    # 在玩家单击play按钮时开始游戏
    if play_button.rect.collidepoint(mouse_x, mouse_y):
        # 重置游戏统计信息
        stats.reset_stats()
        stats.game_active = True
        
        # 清空外星人列表和子弹列表
        aliens.empty()
        bullets.empty()
        
        # 创建一群新外星人并将飞船居中
        create_fleet(ai_settings, screen, ship, aliens)
        ship.center_ship()

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

当前,Play 按钮存在一个问题,即便按钮 Play 不可见,但是鼠标按下原来按钮区域依然会有响应,在游戏开始之后,玩家按下原来按钮区域就会导致游戏重置,所以需要修复这个问题,也就是仅在游戏处于未开始状态才可以对鼠标点击按钮区域做出响应,修改如下(game_func.py):

def check_play_button(ai_settings, screen, stats, play_button, ship, aliens, bullets, mouse_x, mouse_y):
    # 在玩家单击play按钮时开始游戏
    button_cliend = play_button.rect.collidepoint(mouse_x, mouse_y)
    if button_cliend and not stats.game_active:
        # 重置游戏统计信息
        stats.reset_stats()
        stats.game_active = True
        
        # 清空外星人列表和子弹列表
        aliens.empty()
        bullets.empty()
        
        # 创建一群新外星人并将飞船居中
        create_fleet(ai_settings, screen, ship, aliens)
        ship.center_ship()

经过修改后,在游戏进行状态下,鼠标按键不会做出响应。

隐藏光标

为了让玩家能够开始游戏,我们需要使得光标可见,但是在游戏开始之后,光标就不需要显示了,此时就需要使得游戏在活动状态时光标不可见。

game_func.py 中添加如下代码:

def ship_hit(ai_settings, stats, screen, ship, aliens, bullets):
    # 响应外星人撞到飞船
    # 将 ships_left 减 1
    if stats.ships_left > 0:
        stats.ships_left -= 1
    
        # 清空外星人列表和子弹列表,相当于清屏
        aliens.empty()
        bullets.empty()
    
        # 创建一群新的外星人,并将飞船放置到屏幕底部中央
        create_fleet(ai_settings, screen, ship, aliens)
        ship.center_ship()
    
        # 暂停
        sleep(0.5)
    else:
        stats.game_active = False
        pygame.mouse.set_visible(True)

def check_play_button(ai_settings, screen, stats, play_button, ship, aliens, bullets, mouse_x, mouse_y):
    # 在玩家单击play按钮时开始游戏
    button_cliend = play_button.rect.collidepoint(mouse_x, mouse_y)
    if button_cliend and not stats.game_active:
        # 隐藏光标
        pygame.mouse.set_visible(False)
        
        # 重置游戏统计信息
        stats.reset_stats()
        stats.game_active = True
        
        # 清空外星人列表和子弹列表
        aliens.empty()
        bullets.empty()
        
        # 创建一群新外星人并将飞船居中
        create_fleet(ai_settings, screen, ship, aliens)
        ship.center_ship()

修改后,运行效果如下:

在这里插入图片描述

提高等级

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

修改速度设置

首先重新组织 Settings 类,将游戏设置划分为静态的和动态的两组。对于随着游戏进行而变化的设置,需要确保它们在开始新游戏时能够被重置。具体修改如下:

class Settings():
    '''存储《外星人大战》所有设置的类'''
    
    def __init__(self):
        # 初始化游戏的设置
        self.screen_width = 1200
        self.screen_height = 800
        self.bg_color = (230, 0, 0)
        
        # 飞船的设置
        self.ship_limit = 3
        
        # 子弹设置
        self.bullet_width = 1200
        self.bullet_height = 15
        self.bullet_color = 60, 60, 60
        self.bullets_allowed = 5
        
        # 外星人设置
        self.fleet_drop_speed = 50
        
        # 以什么样的速度加快游戏节奏
        self.speedup_scale = 1.1        
        self.initialize_dynamic_settings()
        
    def initialize_dynamic_settings(self):
        # 初始化动态设置参数
        self.ship_speed_factor = 1.5
        self.bullet_speed_factor = 3
        self.alien_speed_factor = 1
        # fleet_direction :1-表示向右移动,-1 表示向左移动
        self.fleet_direction = 1
        
    def increase_speed(self):
        # 提高速度设置
        self.ship_speed_factor *= self.speedup_scale
        self.bullet_speed_factor *= self.speedup_scale
        self.alien_speed_factor *= self.speedup_scale

并且在全部消灭外星人后,调用 increase_speed() 方法提升游戏节奏,修改如下:

def check_bullet_alien_collision(ai_settings, screen, ship, aliens, bullets):
    # 删除发生碰撞的子弹和外星人
    collisions = pygame.sprite.groupcollide(bullets, aliens, False, True)
    # 检查是否需要再次添加外星人
    if len(aliens) == 0:
        # 删除现有子弹并新建新的外星人群
        bullets.empty()
        ai_settings.increase_speed()
        create_fleet(ai_settings, screen, ship, aliens)

通过以上修改,就可以完成在每次消灭完全部外星人后,游戏速度会提升。

在这里插入图片描述

重置速度

每当玩家开始新游戏时,都需要将之前变化的参数重置为初始值,否则每次开始新游戏,游戏里的设置参数依然是前一次改变后的效果。修改 game_func.py, 如下:

def check_play_button(ai_settings, screen, stats, play_button, ship, aliens, bullets, mouse_x, mouse_y):
    # 在玩家单击play按钮时开始游戏
    button_cliend = play_button.rect.collidepoint(mouse_x, mouse_y)
    if button_cliend and not stats.game_active:
        # 重置游戏参数
        ai_settings.initialize_dynamic_settings()
        
        # 隐藏光标
        pygame.mouse.set_visible(False)
        
        # 重置游戏统计信息
        stats.reset_stats()
        stats.game_active = True
        
        # 清空外星人列表和子弹列表
        aliens.empty()
        bullets.empty()
        
        # 创建一群新外星人并将飞船居中
        create_fleet(ai_settings, screen, ship, aliens)
        ship.center_ship()

记分

接下来就需要实现一个计分系统,以方便实时记录玩家的的分,并且显示最高的分、当前游戏等级以及剩余飞船数量。

记录得分是游戏的一项统计信息,所以需要在 GameStats 类中增加一个 score 属性,如下:

class GameStats():
    # 跟踪游戏的统计信息
    
    def __init__(self, ai_settings):
        # 初始化统计信息
        self.ai_settings = ai_settings
        self.reset_stats() 
        # 让游戏一开始处于非活动状态
        self.game_active = False
        # 记录游戏得分
        self.score = 0

    def reset_stats(self):
        # 初始化在游戏运行中可能变化的统计信息
        # 飞船限制次数
        self.ships_left = self.ai_settings.ship_limit

创建得分显示类

为方便在屏幕上显示得分,需要创建一个新类 Scoreboard ,这个类只显示当前得分,但后面也将会使用它来显示最高得分、等级以及剩余飞船数量。下面创建模块 scoreboard.py ,在文件中添加如下代码:

import pygame.font

class Scoreboard():
    # 显示得分信息类
    
    def __init__(self, ai_settings, screen, stats):
        # 初始化显示得分类属性
        self.screen = screen
        self.screen_rect = screen.get_rect()
        self.ai_settings = ai_settings
        self.stats = stats
        
        # 显示得分信息使用的字符设置
        self.text_color = (30, 30, 30)
        self.font = pygame.font.SysFont(None, 40)
        
        # 准备初始化得分图像
        self.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.ai_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)

创建记分牌

现在为了在画面中显示得分,在 alien_invasion.py 中创建 Scoreboard 实例,如下:

import sys
import pygame
from settings import Settings
from ship import Ship
from alien import Alien
from button import Button
from game_stats import GameStats
from scoreboard import Scoreboard
import game_func as gf
from pygame.sprite import Group


def run_game():
    # 初始化游戏并创建一个屏幕对象
    pygame.init()
    ai_settings = Settings()
    screen = pygame.display.set_mode((ai_settings.screen_width, ai_settings.screen_height))
    pygame.display.set_caption("Alien Invasion")
    
    # 创建play按钮
    play_button = Button(ai_settings, screen, "Play")
    
    # 创建用于存储游戏统计信息的实例
    stats = GameStats(ai_settings)
    # 创建存储游戏统计信息实例,并创建记分牌
    sb = Scoreboard(ai_settings, screen, stats)
    # 创建一艘飞船
    ship = Ship(ai_settings, screen)
    # 创建一个用于存储子弹的编组
    bullets = Group()
    # 创建外星人群
    aliens = Group()
    gf.create_fleet(ai_settings, screen, ship, aliens)
    
    
    # 开始游戏的主循环
    while True:
        # 监视键盘和鼠标事件
        gf.check_events(ai_settings, screen, stats, play_button, ship, aliens, bullets)
        
        if stats.game_active:
            ship.update()
            # 调用 update_bullets() 更新子弹
            gf.update_bullets(ai_settings, screen, ship, aliens, bullets)
            # 调用 update_aliens() 更新外星人位置
            gf.update_aliens(ai_settings, stats, screen, ship, aliens, bullets)
        # 调用 update_screen() 更新屏幕
        gf.update_screen(ai_settings, screen, stats, sb, ship, aliens, bullets, play_button)
        

run_game()

修改 update_screen() 函数实现:

           
def update_screen(ai_settings, screen, stats, ship, aliens, bullets, play_button):
    # 更新屏幕上的图像,并切换到新屏幕
    # 每次循环时都重新绘制屏幕
    screen.fill(ai_settings.bg_color)
    
    # 在飞船和外星人后面重新绘制子弹
    for bullet in bullets.sprites():
        bullet.draw_bullet()
    ship.blitme()
    aliens.draw(screen)
    
    # 显示得分
    sb.show_score()
    
    # 如果游戏处于非活动状态,则绘制Play按钮
    if not stats.game_active:
        play_button.draw_button()
    
    # 让最近绘制的屏幕可见
    pygame.display.flip()

修改后,显示如下图,右上角会出现 0

在这里插入图片描述

消灭外星人后更新得分

在屏幕上实时显示得分,每当有外星人被击中时,都需要更新 stats.score 的值,然后调用 prep_score() 来更新得分图像,在 settings.py 中添加参数,指定每个外星人代表的积分数:

class Settings():
    '''存储《外星人大战》所有设置的类'''
        # 外星人设置
        self.fleet_drop_speed = 50
        # 积分
        self.alien_points = 50

然后在 game_func.py 中添加更新参数代码实现:


def update_bullets(ai_settings, screen, stats, sb, ship, aliens, bullets):
    # 更新子弹的位置
    bullets.update()
    
    # 删除已消失的子弹
    for bullet in bullets.copy():
        if bullet.rect.bottom <= 0:
            bullets.remove(bullet)
            
    # 检查是否有子弹与外星人发送碰撞
    check_bullet_alien_collision(ai_settings, screen, stats, sb, ship, aliens, bullets)
    
            
def check_bullet_alien_collision(ai_settings, screen, stats, sb, ship, aliens, bullets):
    # 删除发生碰撞的子弹和外星人
    collisions = pygame.sprite.groupcollide(bullets, aliens, False, True)
    
    if collisions:
        # 有被击中的外星人
        stats.score += ai_settings.alien_points
        sb.prep_score()

    # 检查是否需要再次添加外星人
    if len(aliens) == 0:
        # 删除现有子弹并新建新的外星人群
        bullets.empty()
        ai_settings.increase_speed()
        create_fleet(ai_settings, screen, ship, aliens)
    

修改后,执行如下图:

在这里插入图片描述

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

当前,如果同时被击中多个外星人,也只是算一个50分,所以前面能够看出,我们一瞬间击中一排外星人(3个)也是50分,这里需要根据碰撞返回的字典进行增加得分,修改如下:

def check_bullet_alien_collision(ai_settings, screen, stats, sb, ship, aliens, bullets):
    # 删除发生碰撞的子弹和外星人
    collisions = pygame.sprite.groupcollide(bullets, aliens, False, True)
    
    if collisions:
        # 有被击中的外星人
        for aliens in collisions.values():
            stats.score += ai_settings.alien_points
            sb.prep_score()

    # 检查是否需要再次添加外星人
    if len(aliens) == 0:
        # 删除现有子弹并新建新的外星人群
        bullets.empty()
        ai_settings.increase_speed()
        create_fleet(ai_settings, screen, ship, aliens)

修改之后,得分增加效果如下:

在这里插入图片描述

提高点数

玩家没提高一个等级,游戏都会变得更艰难,所以在更高等级下,每个外星人代表的积分点数也应该更高。为此,在 settings.py 中添加如下代码:

class Settings():
    '''存储《外星人大战》所有设置的类'''
    
    def __init__(self):
        # 初始化游戏的设置
        self.screen_width = 1200
        self.screen_height = 800
        self.bg_color = (230, 0, 0)
        
        # 飞船的设置
        self.ship_limit = 3
        
        # 子弹设置
        #self.bullet_width = 1200
        self.bullet_width = 120
        self.bullet_height = 15
        self.bullet_color = 60, 60, 60
        self.bullets_allowed = 5
        
        # 外星人设置
        self.fleet_drop_speed = 50
        # 积分
        self.alien_points = 50
        # 积分提高速度
        self.score_scale = 1.5
        
        # 以什么样的速度加快游戏节奏
        self.speedup_scale = 1.1        
        self.initialize_dynamic_settings()
        
    def initialize_dynamic_settings(self):
        # 初始化动态设置参数
        self.ship_speed_factor = 1.5
        self.bullet_speed_factor = 3
        self.alien_speed_factor = 1
        # fleet_direction :1-表示向右移动,-1 表示向左移动
        self.fleet_direction = 1
        
    def increase_speed(self):
        # 提高速度设置
        self.ship_speed_factor *= self.speedup_scale
        self.bullet_speed_factor *= self.speedup_scale
        self.alien_speed_factor *= self.speedup_scale
        self.alien_points = int(self.alien_points * self.score_scale)

得分圆整

大多数街机风格的设计游戏都将得分显示为 10 的整数倍,下面也将遵循这一点来进行修改。在 Scoreboard 类中添加如下修改:

    
    def prep_score(self):
        # 将得分转化为渲染图像
        rounded_score = int(round(self.stats.score, -1))
        score_str = "{:,}".format(rounded_score)
        self.score_image = self.font.render(score_str, True, self.text_color, self.ai_settings.bg_color)
        # score_str = str(self.stats.score)
        # self.score_image = self.font.render(score_str, True, self.text_color, self.ai_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、100、1000等整数倍。

添加最高得分

在每次游戏后,将目前最高得分保存下来做完得分记录显示,为玩家提供超越的目标,在 game_stats.py 中添加如下代码:

import pygame.font

class Scoreboard():
    # 显示得分信息类
    
    def __init__(self, ai_settings, screen, stats):
        # 初始化显示得分类属性
        self.screen = screen
        self.screen_rect = screen.get_rect()
        self.ai_settings = ai_settings
        self.stats = stats
        
        # 显示得分信息使用的字符设置
        self.text_color = (30, 30, 30)
        self.font = pygame.font.SysFont(None, 40)
        
        # 准备初始化得分图像
        self.prep_score()
        self.prep_heigh_score()
        
    def prep_score(self):
        # 将得分转化为渲染图像
        rounded_score = int(round(self.stats.score, -1))
        score_str = "{:,}".format(rounded_score)
        self.score_image = self.font.render(score_str, True, self.text_color, self.ai_settings.bg_color)
        # score_str = str(self.stats.score)
        # self.score_image = self.font.render(score_str, True, self.text_color, self.ai_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 prep_heigh_score(self):
        # 将最高得分转换成渲染的图像
        high_score = int(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.ai_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.screen_rect.top
        
    def show_score(self):
        # 在屏幕上显示
        self.screen.blit(self.score_image, self.score_rect)
        self.screen.blit(self.high_score_image, self.high_score_rect)

game_func.py 中添加一个新函数 check_high_score() 用来检查是否诞生了新的最高得分记录:

def check_high_score(stats, sb):
    # 检查是否诞生了新的最高记录
    if stats.score > stats.high_score:
        stats.high_score = stats.score
        sb.prep_high_score()

def check_bullet_alien_collision(ai_settings, screen, stats, sb, ship, aliens, bullets):
    # 删除发生碰撞的子弹和外星人
    collisions = pygame.sprite.groupcollide(bullets, aliens, False, True)
    
    if collisions:
        # 有被击中的外星人
        for aliens in collisions.values():
            stats.score += ai_settings.alien_points
            sb.prep_score()
        # 检查是否新记录
        check_high_score(stats, sb)

    # 检查是否需要再次添加外星人
    if len(aliens) == 0:
        # 删除现有子弹并新建新的外星人群
        bullets.empty()
        ai_settings.increase_speed()
        create_fleet(ai_settings, screen, ship, aliens)
    

修改后,效果如下:

在这里插入图片描述

显示等级

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

class GameStats():
    # 跟踪游戏的统计信息
    
    def __init__(self, ai_settings):
        # 初始化统计信息
        self.ai_settings = ai_settings
        self.reset_stats() 
        # 让游戏一开始处于非活动状态
        self.game_active = False
        # 在任何情况下都不应该重置最高得分
        self.high_score = 0
        # 记录游戏得分
        self.score = 0
        # 记录当前游戏等级
        self.level = 1

    def reset_stats(self):
        # 初始化在游戏运行中可能变化的统计信息
        # 飞船限制次数
        self.ships_left = self.ai_settings.ship_limit
        self.score = 0
        self.level = 1

scoreboard.py 中添加等级显示,如下:

import pygame.font

class Scoreboard():
    # 显示得分信息类
    
    def __init__(self, ai_settings, screen, stats):
        # 初始化显示得分类属性
        self.screen = screen
        self.screen_rect = screen.get_rect()
        self.ai_settings = ai_settings
        self.stats = stats
        
        # 显示得分信息使用的字符设置
        self.text_color = (30, 30, 30)
        self.font = pygame.font.SysFont(None, 40)
        
        # 准备初始化得分图像
        self.prep_score()
        self.prep_high_score()
        self.prep_level()
        
    def prep_score(self):
        # 将得分转化为渲染图像
        rounded_score = int(round(self.stats.score, -1))
        score_str = "{:,}".format(rounded_score)
        self.score_image = self.font.render(score_str, True, self.text_color, self.ai_settings.bg_color)
        # score_str = str(self.stats.score)
        # self.score_image = self.font.render(score_str, True, self.text_color, self.ai_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 prep_high_score(self):
        # 将最高得分转换成渲染的图像
        high_score = int(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.ai_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.screen_rect.top
        
    def prep_level(self):
        # 将登记转换成渲染的图像
        self.level_image = self.font.render(str(self.stats.level), True, self.text_color, self.ai_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
        
    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)

game_func.py 中添加等级显示:

def check_bullet_alien_collision(ai_settings, screen, stats, sb, ship, aliens, bullets):
    # 删除发生碰撞的子弹和外星人
    collisions = pygame.sprite.groupcollide(bullets, aliens, False, True)
    
    if collisions:
        # 有被击中的外星人
        for aliens in collisions.values():
            stats.score += ai_settings.alien_points
            sb.prep_score()
        # 检查是否新记录
        check_high_score(stats, sb)

    # 检查是否需要再次添加外星人
    if len(aliens) == 0:
        # 删除现有子弹并新建新的外星人群
        bullets.empty()
        ai_settings.increase_speed()
        # 提高等级
        stats.level += 1
        sb.prep_level()
        create_fleet(ai_settings, screen, ship, aliens)

修改后显示如下:

在这里插入图片描述

显示余下的飞船数量

为了方便提示玩家,在画面上提示当前剩余飞船数量,需要让 Ship 类继承 Sprite,以便能够创建飞船编组。

修改飞船模块 ship.py , 如下:

import pygame
from pygame.sprite import Sprite

class Ship(Sprite):
    # 管理飞船部分行为
    def __init__(self, ai_settings, screen):
        # 继承初始化
        super(Ship, self).__init__()
        # 初始化飞船并设置其初始位置
        self.screen = screen
        self.ai_settings = ai_settings
        
        # 加载飞船图像并获取其外接矩形
        self.image = pygame.image.load("images/ship.png")
        self.rect = self.image.get_rect()
        self.screen_rect = screen.get_rect()
        
        # 将每艘新产生的飞船放在屏幕底部正中央
        self.rect.centerx = self.screen_rect.centerx
        self.rect.bottom = self.screen_rect.bottom
        
        # 在飞船的属性center中存储小数值
        self.center = float(self.rect.centerx)
        
        # 移动标志
        self.moving_right = False
        self.moving_left = False
        
    def update(self):
        # 根据移动标志调整位置
        # 更新飞船的 center值而不是rect值
        '''
        if self.moving_right:
            #self.rect.centerx += 1
            self.center += self.ai_settings.ship_speed_factor
            if self.center > self.screen_rect.width:
                self.center -= self.ai_settings.ship_speed_factor
        if self.moving_left:
            #self.rect.centerx -= 1
            self.center -= self.ai_settings.ship_speed_factor
            if self.center < 0:
                self.center += self.ai_settings.ship_speed_factor
        self.rect.centerx = self.center
        '''
        if self.moving_right and self.rect.right < self.screen_rect.right:
            self.center += self.ai_settings.ship_speed_factor
        if self.moving_left and self.rect.left > 0:
            self.center -= self.ai_settings.ship_speed_factor
        self.rect.centerx = self.center
        
    def blitme(self):
        # 在指定位置绘制飞船
        self.screen.blit(self.image, self.rect)
        
    def center_ship(self):
        # 居中飞船位置
        self.center = self.screen_rect.centerx

然后在 scoreboard.py 中使用编组包含飞船,如下:

import pygame.font
from pygame.sprite import Group
from ship import Ship

class Scoreboard():
    # 显示得分信息类
    
    def __init__(self, ai_settings, screen, stats):
        # 初始化显示得分类属性
        self.screen = screen
        self.screen_rect = screen.get_rect()
        self.ai_settings = ai_settings
        self.stats = stats
        
        # 显示得分信息使用的字符设置
        self.text_color = (30, 30, 30)
        self.font = pygame.font.SysFont(None, 40)
        
        # 准备初始化得分图像
        self.prep_score()
        self.prep_high_score()
        self.prep_level()
        self.prep_ships()
        
    def prep_ships(self):
        # 显示剩余多少艘飞船
        self.ships = Group()
        for ship_number in range(self.stats.ships_left):
            ship = Ship(self.ai_settings, self.screen)
            ship.rect.x = 10 + ship_number * ship.rect.width
            ship.rect.y = 10
            self.ships.add(ship)
        
    def prep_score(self):
        # 将得分转化为渲染图像
        rounded_score = int(round(self.stats.score, -1))
        score_str = "{:,}".format(rounded_score)
        self.score_image = self.font.render(score_str, True, self.text_color, self.ai_settings.bg_color)
        # score_str = str(self.stats.score)
        # self.score_image = self.font.render(score_str, True, self.text_color, self.ai_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 prep_high_score(self):
        # 将最高得分转换成渲染的图像
        high_score = int(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.ai_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.screen_rect.top
        
    def prep_level(self):
        # 将登记转换成渲染的图像
        self.level_image = self.font.render(str(self.stats.level), True, self.text_color, self.ai_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
        
    def reset_prep(self):
        # 重置当前图像
        self.prep_score()
        self.prep_high_score()
        self.prep_level()
        self.prep_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)

修改后,画面显示如下:

在这里插入图片描述

总结

通过这个简单的游戏项目,可以大概了解到 Python 使用 pygame 模块进行游戏开发的简单操作流程了。这个游戏项目虽然小,但是其中也包含了对玩家操作响应、分数记录、游戏等级等等不同的处理过程,并且也基于面向对象的开发步骤,将飞船、外星人、子弹、按钮、基本参数、积分显示等等都创建成为类对象模块的方式进行管理,这样也方便后续的修改以及代码的维护。

整个项目的代码已经保存到了 GitHub 上,链接如下:

飞机大战项目链接

供大家一起参考学习,这里面还有很多没有考虑到的问题以及一些 bug 没有修改,大家可以研究研究(比如:飞船总是完整出现,能不能随机位置出现;一颗子弹就能消灭一竖行的敌人,能不能修改;每次关闭程序后,最高纪录都不能保存下来,如果修改呢?)等等问题,大家有兴趣可以改改玩一玩啊。

GitHub 加速计划 / li / linux-dash
10.39 K
1.2 K
下载
A beautiful web dashboard for Linux
最近提交(Master分支:2 个月前 )
186a802e added ecosystem file for PM2 4 年前
5def40a3 Add host customization support for the NodeJS version 4 年前
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐