【Python】第十二章_外星人入侵_武装飞船

目录

项目概述:

[1 项目需求分析](#1 项目需求分析)

[2 安装Pygame](#2 安装Pygame)

[3 开始游戏项目](#3 开始游戏项目)

[3.1 创建Pygame窗口以及响应用户输入](#3.1 创建Pygame窗口以及响应用户输入)

[3.2 设置背景色](#3.2 设置背景色)

[3.3 创建设置类](#3.3 创建设置类)

[4 添加飞船图像](#4 添加飞船图像)

[4.1 创建Ship 类](#4.1 创建Ship 类)

[4.2 在屏幕上绘制飞船](#4.2 在屏幕上绘制飞船)

[5 重构: 模块game_functions](#5 重构: 模块game_functions)

[5.1 函数check_events()](#5.1 函数check_events())

[5.2 函数update_screen()](#5.2 函数update_screen())

[6 驾驶飞船](#6 驾驶飞船)

[6.1 响应按键](#6.1 响应按键)

[6.2 允许不断移动](#6.2 允许不断移动)

[6.3 左右移动](#6.3 左右移动)

[6.4 调整飞船的速度](#6.4 调整飞船的速度)

[6.5 限制飞船的活动范围](#6.5 限制飞船的活动范围)

[6.6 重构check_events()](#6.6 重构check_events())

[7 射击](#7 射击)

[7.1 添加子弹设置](#7.1 添加子弹设置)

[7.2 创建Bullet 类](#7.2 创建Bullet 类)

[7.3 将子弹存储到编组中](#7.3 将子弹存储到编组中)

[7.4 开火](#7.4 开火)

[7.5 删除已消失的子弹](#7.5 删除已消失的子弹)

[7.6 限制子弹数量](#7.6 限制子弹数量)

[7.7 创建函数update_bullets()](#7.7 创建函数update_bullets())

[7.8 创建函数fire_bullet()](#7.8 创建函数fire_bullet())

[8 小结](#8 小结)


项目概述:

学习了Python基础知识,下面我们来学习使用 Python开发游戏。 在项目"外星人入侵"(第 12~14 章) 中, 你将使用 Pygame 包来开发一款 2D 游戏, 它在玩家每消灭一群向下移动的外星人后, 都将玩家提高一个等级; 而等级越高, 游戏的节奏
越快, 难度越大。 完成这个项目后, 你将获得自己动手使用 Pygame 开发 2D 游戏所需的技能。
使用 Pygame 开发 2D 游戏, 这是一组功能强大而有趣的模块, 可用于管理图形、 动画乃至声音, 让你能够更轻松地开发复杂的游戏。 通过使用 Pygame来处理在屏幕上绘制图像等任务, 你不用考虑众多烦琐而艰难的编码工作, 而是将重点放在程序的高级逻辑上。
在第12章中, 你将安装Pygame, 再创建一艘能够根据用户输入而左右移动和射击的飞船。 在接下来的第13-14章中, 你将创建一群作为射杀目标的外星人, 并做其他的改进, 如限制可供玩家使用的飞船数以及添加记分牌。
为了便于游戏《外星人入侵》项目的多个不同的文件管理,可新建一个文件夹, 并将其命名为alien_invasion 。 请务必将这个项目的所有文件都存储 到这个文件夹中, 这样相关的import语句才能正确地工作。

1 项目需求分析

开发大型项目时, 做好项目需求分析后再动手编写项目很重要。 需求分析可确保把控项目的主模块逻辑设计, 从而提高项目成功的可能性。

下面来编写有关游戏《外星人入侵》 的描述:

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

在第一个开发阶段, 我们将创建一艘可左右移动的飞船, 这艘飞船在用户按空格键时能够开火。 设置好这种行为后, 我们就能够将注意力转向外星人, 并提高这款游戏的可玩性。

2 安装Pygame

这里仅演示在Windows系统中安装Pygame,尝试以下方法:

pip install -i https://pypi.tuna.tsinghua.edu.cn/simple 包名

用pip管理工具安装库文件时,默认使用国外的源文件,因此在国内的下载速度会比较慢,可能只有50KB/s。幸好,国内的一些顶级科研机构已经给我们准备好了各种镜像,下载速度可达2MB/s。

其中,比较常用的国内镜像包括:

(1)阿里云 http://mirrors.aliyun.com/pypi/simple/

(2)豆瓣http://pypi.douban.com/simple/

(3)清华大学 https://pypi.tuna.tsinghua.edu.cn/simple/

(4)中国科学技术大学 http://pypi.mirrors.ustc.edu.cn/simple/

(5)华中科技大学http://pypi.hustunique.com/

可以在使用pip的时候,加上参数-i和镜像地址(如https://pypi.tuna.tsinghua.edu.cn/simple),

例如:pip install -i Simple Index pygame,这样就会从清华镜像安装pygame库。

测试 pygame库是否成功:
安装必要的包后, 对安装进行测试。 为此, 首先使用命令python 或python3 启动一个终端会话, 再尝试导入 pygame:

3 开始游戏项目

开发游戏《外星人入侵》时,首先创建一个空的Pygame窗口, 用来绘制游戏元素, 如飞船和外星人。 同时窗口响应用户输入、 设置背景色以及加载飞 船图像。

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

使用Pygame编写的游戏的基本结构如下:
alien_invasion1.py

python 复制代码
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()

首先, 导入了模块sys 和pygame 。 模块pygame 包含开发游戏所需的功能,模块sys 用来退出游戏。
游戏《外星人入侵》 的开头是函数run_game() ,pygame.init() 初始化背景设置(见❶), 调 用pygame.display.set_mode() 来创建一个名为screen 的显示窗口(见❷ ), 实参(1200, 800) 是一个元组, 指定了游戏窗口的尺 寸, 这个游戏的所有图形元素都将在这个窗口中绘制。
对象screen 是一个surface。 在Pygame中, surface是屏幕的一部分, 用于显示游戏元素。 在这个游戏中, 每个元素(如外星人或飞船) 都是一个surface。 display.set_mode() 返回的surface表示整个游戏窗口。 我们激活游戏的动画循环后, 每经过一次循环都将自动重绘这个surface。
这个游戏由一个while 循环(见❸) 控制, 其中包含一个事件循环以及管理屏幕更新的代码。 事件是用户玩游戏时执行的操作, 如按键或移动鼠标。 为让程序响应事件, 我们编 写一个事件循环(见 ❹ ), 以侦听事件, 并根据发生的事件执行相应的任务。
为访问Pygame检测到的事件, 我们使用方法pygame.event.get() 。 所有键盘和鼠标事件都将促使for 循环运行。 在这个循环中, 我们将编写一系列的if 语句来检测并响应 特定的事件。 例如, 玩家单击游戏窗口的关闭按钮时, 将检测到pygame.QUIT 事件, 而我们调用sys.exit() 来退出游戏(见❺) 。
❻处调用了pygame.display.flip() , 命令Pygame让最近绘制的屏幕可见。 在这里, 它在每次执行while 循环时都绘制一个空屏幕, 并擦去旧屏幕, 使得只有新屏幕可 见。 在我们移动游戏元素时, pygame.display.flip() 将不断更新屏幕, 以显示元素的新位置, 并在原来的位置隐藏元素, 从而营造平滑移动的效果。
run_game() 是初始化游戏并开始主循环。

3.2 设置背景色

Pygame默认创建一个黑色屏幕,下面将背景设置为另一种颜色:
alien_invasion2.py

python 复制代码
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,230,230)
    
    #开始游戏的主循环
    while True:
        
        #监视键盘和鼠标事件
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                sys.exit()
                
        #每次循环时都重绘屏幕
❷        screen.fill(bg_color)
                
        #让最近绘制的屏幕可见
        pygame.display.flip()
        
run_game()

bg_color存储自定义背景框颜色(见❶) 。 该颜色只需指定一次, 因此我们在进入主while 循环前定义它。在Pygame中, 颜色是以RGB值指定的。我们这里将背景设置为一种浅灰色。在❷处, 我们调用方法screen.fill() , 用背景色填充屏幕; 这个方法只接受一个实参: 一种颜色。

3.3 创建设置类

编写settings 的模块(包含一个Settings 的类), 存储游戏的所有设置。 通过传递一个设置对象, 而不是众多不同的设置,让函数调用更简单, 在项目增大时只需修改settings.py中的一些值, 而无需查找散布在文件中的不同设置。

下面是最初的Settings 类:

settings.py

python 复制代码
class Settings():
    """存储《外星人入侵》的所有设置的类"""
    
    def __init__(self):
        """初始化游戏的设置"""
        #屏幕设置
        self.screen_width = 1200
        self.screen_height = 800
        self.bg_color = (230,230,230)

为创建Settings 实例并使用它来访问设置:

alien_invasion3.py

python 复制代码
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")
    
    #开始游戏的主循环
    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()

创建Settings 实例 ai_settings (见❶) 。 创建屏幕时(见❷) , 使用
了ai_settings 的属性screen_width 和screen_height ; 接下来填充屏幕时, 也使用了ai_settings 来访问背景色(见❸) 。

4 添加飞船图像

下面使用Pygame方法blit() 绘制在屏幕上绘制玩家的飞船。
为游戏选择素材时, 可以使用http://pixabay.com/ 等网站提供的图形。
在游戏中几乎可以使用任何类型的图像文件, 但使用位图(.bmp) 文件最为简单, 因为Pygame默认加载位图。 虽然可配置Pygame以使用其他文件类型, 但有些文件类型要求你在 计算机上安装相应的图像库。 大多数图像都为.jpg、 .png或.gif格式, 但可使用Photoshop、 GIMP和Paint等工具将其转换为位图。
选择图像时尽可能选择背景透明的图像, 因为这样可使用图像编辑器将其背景设置为任何颜色。
在主项目文件夹(alien_invasion) 中新建一个文件夹, 将其命名为images, 并将文件ship.bmp保存到这个文件夹中。

4.1 创建Ship 类

在屏幕上显示飞船的图像后,我们创建一个名为ship 的模块, 其中包含Ship 类, 它负责管理飞船的大部分行为。
ship1.py

python 复制代码
import pygame

class Ship():
    
    def __init__(self,screen):
        """初始化飞船并设置其初始位置"""
        self.screen = screen
        
        #加载飞船图像并获取其外接矩形
❶        self.image = pygame.image.load('image/ship.bmp')
❷        self.rect = self.image.get_rect()
❸        self.screen_rect = screen.get_rect()
        
        #将每艘飞船放在屏幕底部中央
❹        self.rect.centerx = self.screen.centerx
        self.rect.bottom = self.screen_rect.bottom
        
❺    def blitme(self):
        """在指定位置绘制飞船"""
        self.screen.blit(self.image,self.rect)

Ship 的方法__init__() 接受两个参数: 引用self 和screen , 其中后者指定了要将飞船绘制到什么地方。调用pygame.image.load()来加载图像 (见❶) 。 self.image存储一个表示飞船的surface。然后我们可以使用get_rect() 获取飞船surface的属性rect (见❷) 。

处理rect 对象时, 可使用矩形四角和中心的 x 和 y 坐标。 可通过设置这些值来指定矩形的位置。要将游戏元素居中, 可设置相应rect 对象的属性center 、 centerx 或centery 。 要让游戏元素与屏幕边缘对齐, 可使用属性top 、 bottom 、 left 或right ; 要调整游戏元素的水平或垂直位置, 可使用属性x 和y , 它们分别是相应矩形左上角的 x 和 y 坐标。

注意 在Pygame中, 原点(0, 0)位于屏幕左上角, 向右下方移动时, 坐标值将增大。

屏幕的矩形存储在self.screen_rect 中(见❸) , 再将self.rect.centerx (飞船中心的x 坐标) 设置为表示屏幕的矩形的属性centerx (见❹) , 并将self.rect.bottom (飞船下边缘的y 坐标) 设置为表示屏幕的矩形的属性bottom 。 这样Pygame就会利用这些rect 属性将把飞船放在屏幕底部中央。最后,我们定义方法blitme() , 它根据self.rect 指定的位置将图像绘制到屏幕上(见❺)。

4.2 在屏幕上绘制飞船

下面演示创建一艘飞船, 并调用其方法blitme():

alien_invasion4.py

python 复制代码
import sys

import pygame

from settings import Settings
from ship1 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")
    
    #创建一艘飞船
❶    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()

导入Ship 类后创建屏幕后创建一个名为ship 的Ship 实例。填充背景后, 调用ship.blitme() 将飞船绘制到屏幕上, 确保它出现在背景前面(见❷) 。

运行alien_invasion.py, 如图12-2所示将看到飞船位于空游戏屏幕底部中央。

图12-2 游戏《外星人入侵》 屏幕底部中央有一艘飞船

5 重构: 模块game_functions

在大型项目中, 经常需要在添加新代码前重构既有代码。本节我们为了 避免alien_invasion.py太长, 创建一个名为game_functions 的新模块, 存储大量让游戏《外星人入侵》 运行的函数。

5.1 函数check_events()

我们将首先把管理事件的代码移到一个名为check_events() 的函数中, 以简化run_game() 并隔离事件管理循环。 通过隔离事件循环, 可将事件管理与游戏的其他方面(如 更新屏幕) 分离。
将check_events() 放在一个名为game_functions 的模块中:
game_functions1.py

python 复制代码
import sys

import pygame

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

首先模块game_functions导入事件检查循环要使用的sys 和pygame 。 check_events()函数体复制了alien_invasion.py的事件循环。

下面在alien_invasion5.py, 导入模块game_functions , 并调用事件循环函数check_events() :

alien_invasion5.py

python 复制代码
import pygame

from settings import Settings
from ship1 import Ship

import game_functions1 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()
                
        #每次循环时都重绘屏幕
        screen.fill(ai_settings.bg_color)
        ship.blitme()
                
        #让最近绘制的屏幕可见
        pygame.display.flip()
        
run_game()

5.2 函数update_screen()

为进一步简化run_game() ,更新屏幕的代码移到模块game_functions的update_screen() 函数:

game_functions2.py

python 复制代码
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()

新函数update_screen() 包含三个形参: ai_settings 、 screen 和ship 。 下面在alien_invasion6.py, 导入模块game_functions , 并调用更新屏幕的函数update_screen() :

alien_invasion6.py

python 复制代码
import pygame

from settings import Settings
from ship1 import Ship

import game_functions2 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()
        gf.update_screen(ai_settings,screen,ship)                
        
run_game()

对代码进行重构使其更容易扩展,一开始将代码编写得尽可能简单,我们可能只使用一个文件,但在项目越来越复杂时进 行重构。

6 驾驶飞船

玩家左右移动飞船是让用户按左或右箭头键时作出响应,即控制屏幕图像的移动。 我们将首先专注于向右移动, 再使用同样的原理来控制向左移动。

6.1 响应按键

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

python 复制代码
import sys

import pygame

def check_events(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 += 1

def update_screen(ai_settings,screen,ship):
    """更新屏幕上的图像,并切换到新屏幕"""
    #每次循环时都重绘屏幕
    screen.fill(ai_settings.bg_color)
    ship.blitme()
    #让最近绘制的屏幕可见
    pygame.display.flip()

玩家按右箭头键时, 需要将飞船向右移动,因此函数check_events() 包含形参ship 。函数check_events() 内部的事件循环中elif 代码块是为了Pygame 检测到KEYDOWN事件时作出响应(见❶) 右箭头键(event.key == K_RIGHT )被按下时(见❷), ship.rect.centerx 的值加1, 飞船向右移动(见❸) 。

alien_invasion7.py

python 复制代码
import pygame

from settings import Settings
from ship1 import Ship

import game_functions3 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(ship)
        gf.update_screen(ai_settings,screen,ship)                
        
run_game()

6.2 允许不断移动

玩家按住右箭头键不放时, 我们希望飞船不断地向右移动, 直到玩家松开为止。当玩家松开右箭头键时我们能够检测到pygame.KEYUP事件; 结合使用KEYDOWN和KEYUP事件, 以及一个moving_right 的标志位来实现持续移动。

飞船不动时, moving_right 为False 。 玩家按下右箭头键时,moving_right被设置为True ; 玩家松开时, moving_right被重新设置为False 。

飞船的属性都由Ship 类控制, 我们在Ship 类中添加moving_right 的属性和一个update() 的方法。 方法update() 检查标志moving_right 的状态,如果这个标志为True , 就调整飞船的位置。 每当需要调整飞船的位置时, 我们都调用这个方法。

根据上述描述对Ship 类所做以下修改:

ship2.py

python 复制代码
class Ship():
    
    def __init__(self,screen):
        """初始化飞船并设置其初始位置"""
        self.screen = screen
        
        #加载飞船图像并获取其外接矩形
        self.image = pygame.image.load('images/ship.bmp')
        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
    
❷    def update(self):
        """根据移动标志调整飞船的位置"""
        if self.moving_right:
            self,rect.centerx += 1
        
    def blitme(self):
        """在指定位置绘制飞船"""
        self.screen.blit(self.image,self.rect)

在方法__init__() 中, 我们添加了属性self.moving_right , 并将其初始值设置为False (见❶) 。 添加方法update() , 当moving_right为True 时,向右移动飞船(见❷) 。

下面修改game_functions模块check_events()函数,根据右箭头按下和松开分别将moving_right设置为True和False。

game_functions4.py

python 复制代码
import sys

import pygame

def check_events(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
                
        elif event.type == pygame.KEYUP:
❷            if event.key == pygame.K_RIGHT:
                ship.moving_right = False
                
def update_screen(ai_settings,screen,ship):
    """更新屏幕上的图像,并切换到新屏幕"""
    #每次循环时都重绘屏幕
    screen.fill(ai_settings.bg_color)
    ship.blitme()
    #让最近绘制的屏幕可见
    pygame.display.flip()

玩家按下右箭头键时,不再直接移动飞船位置,而是将moving_right 设置为True(见❶) 。然后添加了一个新的elif 代码块, 用于响应KEYUP事件: 玩家松开右箭头键(event.key == pygame.K_RIGHT) 时(见❷), 我们将moving_right 设置为False 。最后, 在alien_invasion8.py 的while 循环中调用飞船的方法update() :
alien_invasion8.py

python 复制代码
import pygame

from settings import Settings
from ship2 import Ship

import game_functions4 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(ship)
        ship.update()
        gf.update_screen(ai_settings,screen,ship)                
        
run_game()

飞船的位置将在检测到键盘事件后(但在更新屏幕前) 更新。如果你现在运行alien_invasion8.py并按住右箭头键, 飞船将不断地向右移动, 直到你松开为止。

6.3 左右移动

飞船能够不断地向右移动后, 添加向左移动的逻辑很容易。 下面修改Ship 类和函数check_events() 使得飞船左右移动。 下面显示了对Ship 类的方法__init__() 和update() 所做的相关修改:

ship3.py

python 复制代码
import pygame

class Ship():
    
    def __init__(self,screen):
        """初始化飞船并设置其初始位置"""
        self.screen = screen
        
        #加载飞船图像并获取其外接矩形
        self.image = pygame.image.load('images/ship.bmp')
        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.moving_left:
            self.rect.centerx -= 1
        
    def blitme(self):
        """在指定位置绘制飞船"""
        self.screen.blit(self.image,self.rect)

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

我们还需对check_events() 作两方面的调整:

game_functions5.py

python 复制代码
import sys

import pygame

def check_events(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
            elif event.key == pygame.K_LEFT:
                ship.moving_left = True    
                
        elif event.type == pygame.KEYUP:
            if event.key == pygame.K_RIGHT:
                ship.moving_right = False
            elif 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()

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

如果此时运行alien_invasion9.py, 将能够不断地左右移动飞船; 如果你同时按左右箭头键, 飞船将纹丝不动。

alien_invasion9.py

python 复制代码
import pygame

from settings import Settings
from ship3 import Ship

import game_functions5 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(ship)
        ship.update()
        gf.update_screen(ai_settings,screen,ship)                
        
run_game()

6.4 调整飞船的速度

为了控制飞船的速度,我们在Settings 类中添加属性ship_speed_factor,允许每次执行循环时,飞船最多移动1像素,根据这个属性决定飞船在每次循环时最多移动多少距离。 下面演示了如何在settings1.py中添加这个新属性:

settings1.py

python 复制代码
class Settings():
    """存储《外星人入侵》的所有设置的类"""
    
    def __init__(self):
        """初始化游戏的设置"""
        #屏幕设置
        self.screen_width = 1200
        self.screen_height = 800
        self.bg_color = (230,230,230)
        
        #飞船的位置
        self.ship_speed_factor = 1.5

settings1.py中,ship_speed_factor 的初始值设置成了1.5,即每次移动1.5像素。

Ship 类 rect 的centerx 等属性只能存储整数值, 因此我们需要对Ship 类做些修改:

ship4.py

python 复制代码
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.bmp')
        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.center += self.ai_settings.ship_speed_factor
        if self.moving_left:
            self.center -= self.ai_settings.ship_speed_factor
            
        #根据self.center更新rect对象
❺        self.rect.centerx = self.center
        
    def blitme(self):
        """在指定位置绘制飞船"""
        self.screen.blit(self.image,self.rect)

在❶处, 我们在__init__() 的形参列表中添加了ai_settings ,并将形参ai_settings 的值存储在一个属性中,以便能够在update() 中让飞船能够获取其速度设置(见❷) 。现在我们通过增加或减去一个单位为像素的小数值来调整飞船的位置, 因此需要将位置存储在一个能够存储小数值的变量中。 可以使用小数来设置rect 的属性, 但rect 将只存储这个值的整数部分。 为准确地存储飞船的位置, 我们定义了一个可存储小数值的新属性self.center (见❸) 。 我们使用函数float()将self.rect.centerx 的值转换为小数, 并将结果存储到self.center 中。

调整飞船的位置的update() 函数中, 将self.center 的值增加或减去ai_settings.ship_speed_factor 的值(见❹) 。 更新self.center 后, 我们再根据它来更新控制飞船位置的self.rect.centerx (见❺) self.rect.centerx 将只存储self.center 的整数部分, 但对显示飞船而言, 这问题不大。

在alien_invasion10.py中创建Ship 实例时, 需要传入实参ai_settings :

alien_invasion10.py

python 复制代码
import pygame

from settings1 import Settings
from ship4 import Ship

import game_functions5 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(ai_settings,screen)
    
    #开始游戏的主循环
    while True:
        gf.check_events(ship)
        ship.update()
        gf.update_screen(ai_settings,screen,ship)                
        
run_game()

6.5 限制飞船的活动范围

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

ship5.py

python 复制代码
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.bmp')
        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 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.center更新rect对象
        self.rect.centerx = self.center
        
    def blitme(self):
        """在指定位置绘制飞船"""
        self.screen.blit(self.image,self.rect)

上述代码确保在修改self.center 的值之前,飞船位置在屏幕内移动。 self.rect.right 返回飞船外接矩形的右边缘的 x 坐标, 如果这个值小于self.screen_rect.right 的值,就说明飞船未触及屏幕右边缘(见❶) 。 self.rect.left 的左边缘的 x 坐标大于零, 就说明飞船未触及屏幕左边缘(见❷) 。

如果此时运行alien_invasion11.py, 飞船将在触及屏幕左边缘或右边缘后停止移动。

alien_invasion11.py

python 复制代码
import pygame

from settings1 import Settings
from ship5 import Ship

import game_functions5 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(ai_settings,screen)
    
    #开始游戏的主循环
    while True:
        gf.check_events(ship)
        ship.update()
        gf.update_screen(ai_settings,screen,ship)                
        
run_game()

6.6 重构check_events()

随着游戏开发的进行, 函数check_events() 将越来越长, 我们将其部分代码放在两个函数中: 一个处理MOUSEBUTTONDOWN事件, 另一个处理MOUSEBUTTONUP事件:

game_functions6.py

python 复制代码
import sys

import pygame

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)
                
def update_screen(ai_settings,screen,ship):
    """更新屏幕上的图像,并切换到新屏幕"""
    #每次循环时都重绘屏幕
    screen.fill(ai_settings.bg_color)
    ship.blitme()
    #让最近绘制的屏幕可见
    pygame.display.flip()

如果此时运行alien_invasion12.py, 飞船将在触及屏幕左边缘或右边缘后停止移动。

alien_invasion12.py

python 复制代码
import pygame

from settings1 import Settings
from ship5 import Ship

import game_functions6 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(ai_settings,screen)

    #开始游戏的主循环
    while True:
        gf.check_events(ship)
        ship.update()
        gf.update_screen(ai_settings,screen,ship)                
        
run_game()

我们创建了两个新函数: check_keydown_events() 和check_keyup_events() , 分别实现鼠标按下和松开事件,它们都包含形参event 和ship 。然后简化check_events函数 ,对这两个函数的调用。 这样 使得函数check_events() 更简单, 代码结构更清晰。

7 射击

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

7.1 添加子弹设置

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

settings2.py

python 复制代码
class Settings():
    """存储《外星人入侵》的所有设置的类"""
    
    def __init__(self):
        """初始化游戏的设置"""
        #屏幕设置
        self.screen_width = 1200
        self.screen_height = 800
        self.bg_color = (230,230,230)
        
        #飞船的位置
        self.ship_speed_factor = 1.5
        
        #子弹设置:创建宽3像素、 高15像素的深灰色子弹
        self.bullet_speed_factor = 1
        self.bullet_width = 3
        self.bullet_height = 15
        self.bullet_color = 60,60,60

7.2 创建Bullet 类

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

bullet1.py

python 复制代码
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

Bullet 类继承了我们从模块pygame.sprite 中导入的Sprite 类。 通过使用精灵, 可将游戏中相关的元素编组, 进而同时操作编组中的所有元素。 为创建子弹实例, 需要向__init__() 传递ai_settings 、 screen 和ship 实例, 还调用了super() 来继承Sprite 。

这里的子弹并非基于图像,故使用pygame.Rect() 类从空白开始创建一个矩形作为子弹的属性rect(见❶), 创建实例时, 必须提供矩形左上角的

x 坐标和 y 坐标, 还有矩形的宽度和高度。

在❷处, 我们将子弹的centerx 设置为飞船的rect.centerx 。 子弹应从飞船顶部射出, 因此我们将表示子弹的rect 的top 属性设置为飞船的rect 的top 属性(见❸) 。

我们将子弹的 y 坐标存储为小数值, 以便能够微调子弹的速度(见❹) 。 在❺处, 我们将子弹的颜色和速度设置分别存储到self.color 和self.speed_factor 中。

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

bullet1.py

python 复制代码
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)

我们调用update() 方法管理子弹的位置。 子弹发射出去后,在屏幕中向上移动,y 坐标会不断减小,将self.y 中减去self.speed_factor的值(见❶)来更新子弹的位置 。 接下来, 我们将self.rect.y 设置为self.y 的值(见❷) 。

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

7.3 将子弹存储到编组中

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

alien_invasion13.py

python 复制代码
import pygame

from pygame.sprite import Group
from settings2 import Settings
from ship5 import Ship

import game_functions7 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(ai_settings,screen)
    
    #创建一个用于存储子弹的编组
❶    bullets = Group()
    
    #开始游戏的主循环
    while True:
        gf.check_events(ai_settings,screen,ship,bullets)
        ship.update()
❷        bullets.update()
        gf.update_screen(ai_settings,screen,ship,bullets)                
        
run_game()

我们导入了pygame.sprite 中的Group 类。 在❶处, 我们创建了一个Group 实例, 并将其命名为bullets 。并在每次循环时,使用创建的子弹编组。 在check_events() 中, 需要在玩家按空格键时处理bullets ; 而在update_screen() 中, 需要更新要绘制到屏幕上的bullets 。故需要将bullets 传递给了check_events() 和update_screen() 。

当你对编组调用update() 时(见❷), 编组将自动对其中的每个精灵调用update() , 因此代码行bullets.update() 将为编组bullets 中的每颗子弹调用bullet.update()。

想要运行alien_invasion13.py,需要适当修改game_functions7.py文件,将bullets 传递给了check_events() 和update_screen() :

game_functions7.py

python 复制代码
import sys

import pygame

from bullet1 import Bullet

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(ai_settings,screen,ship,bullets):
    """响应按键和鼠标事件"""
    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)
                
def update_screen(ai_settings,screen,ship,bullets):
    """更新屏幕上的图像,并切换到新屏幕"""
    #每次循环时都重绘屏幕
    screen.fill(ai_settings.bg_color)
    ship.blitme()
    #让最近绘制的屏幕可见
    pygame.display.flip()

7.4 开火

在game_functions8.py中, 我们需要修改check_keydown_events() , 以便在玩家按空格键时发射一颗子弹。 我们无需修改check_keyup_events() , 因为玩家松开空格键时什么都不会发生。 我们还需修改update_screen() , 确保在调用flip() 前在屏幕上重绘每颗子弹。 下面是对game_functions8.py所做的相关修改:

game_functions8.py

python 复制代码
import sys

import pygame

from bullet1 import 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.button == pygame.K_SPACE:
        #创建一颗子弹, 并将其加入到编组bullets中
        new_bullet = Bullet(ai_settings, screen, ship)
        bullets.add(new_bullet)

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, 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, ship, bullets)
        elif event.type == pygame.KEYUP:
            check_keyup_events(event,ship)

❹ 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()

编组bulltes 传递给了check_keydown_events() (见❶) 。 玩家按空格键时, 创建一颗新子弹(一个名为new_bullet 的Bullet 实例) , 并使用bullets.add(new_bullet) 将新子弹存储到编组bullets 中(见❷)。

在check_events() 的定义中, 我们需要添加形参bullets (见❸) ; 调用check_keydown_events() 时, 需要将bullets 作为实参传递给它。

在❹处, 我们给在屏幕上绘制子弹的update_screen() 添加了形参bullets 。 方法bullets.sprites() 返回一个列表, 其中包含编组bullets 中的所有精灵。 为在屏幕上绘制发射的所有子弹, 我们遍历编组bullets 中的精灵, 并对每个精灵都调用draw_bullet() (见❺) 。

运行alien_invasion14.py,不仅能够左右移动飞船,飞船还可以发射一系列子弹。还可在settings2.py中修改子弹的尺寸、颜色和速度。

python 复制代码
import pygame

from pygame.sprite import Group
from settings2 import Settings
from ship5 import Ship

import game_functions8 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(ai_settings,screen)
    
    #创建一个用于存储子弹的编组
    bullets = Group()
    
    #开始游戏的主循环
    while True:
        gf.check_events(ai_settings,screen,ship,bullets)
        ship.update()
        bullets.update()
        gf.update_screen(ai_settings,screen,ship,bullets)                
        
run_game()

如图12-3所示。

图12-3 飞船发射一系列子弹后的《外星人入侵》 游戏

7.5 删除已消失的子弹

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

所以需要将这些已消失的子弹删除, 否则游戏所做的无谓工作将越来越多, 进而变得越来越慢。 为此, 我们需要检测这样的条件, 即表示子弹的rect 的bottom 属性为零, 它表明子弹已穿过屏幕顶端:

alien_invasion15.py

python 复制代码
import pygame

from pygame.sprite import Group
from settings2 import Settings
from ship5 import Ship

import game_functions8 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(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))
            
        gf.update_screen(ai_settings,screen,ship,bullets)                
        
run_game()

在for 循环中, 不应从列表或编组中删除条目,而是应该遍历编组的副本。 我们使用了方法copy() 来设置for 循环(见❶) , 这让我们能够在循环中修改bullets 。首先判断每颗子弹从屏幕顶端消失(见❷) 。 一旦消失,将其从bullets 中删除(见❸) 。 并在for循环中使用一条print 语句, 以显示当前还有多少颗子弹(见❹),随着子弹一颗颗地在屏幕顶端消失, 子弹数将逐渐降为零。 运行这个游戏并确认子弹已被删除后, 将这条print 语句删除。 如果你留下这条语句, 游戏的速度将大大降低, 因为将输出写入到终端而花费的时间比将图形绘制到游戏窗口花费的时间还多。

7.6 限制子弹数量

很多射击游戏都对可同时出现在屏幕上的子弹数量进行限制,下面在settings3.py中存储所允许的最大子弹数:

settings3.py

python 复制代码
class Settings():
    """存储《外星人入侵》的所有设置的类"""
    
    def __init__(self):
        """初始化游戏的设置"""
        #屏幕设置
        self.screen_width = 1200
        self.screen_height = 800
        self.bg_color = (230,230,230)
        
        #飞船的位置
        self.ship_speed_factor = 1.5
        
        #子弹设置:创建宽3像素、 高15像素的深灰色子弹
        self.bullet_speed_factor = 1
        self.bullet_width = 3
        self.bullet_height = 15
        self.bullet_color = 60,60,60
        self.bullets_allowed = 3 #未消失的子弹数限制为3颗

在game_functions9.py的check_keydown_events() 中, 我们在创建新子弹前检查未消失的子弹数是否小于该设置:

game_functions9.py

python 复制代码
import sys

import pygame

from bullet1 import 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中
        if len(bullets) < ai_settings.bullets_allowed:
            new_bullet = Bullet(ai_settings, screen, ship)
            bullets.add(new_bullet)

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, 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, ship, bullets)
        elif event.type == pygame.KEYUP:
            check_keyup_events(event,ship)

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()

玩家按空格键时, 当界面上还有三颗子弹,即bullets 的长度大于3,则不创建新子弹。 如果len(bullets) 小于3, 我们就创建一个新子弹; 现在运行这个游戏程序alien_invasion16.py, 屏幕上最多只能有3颗子弹。

alien_invasion16.py

python 复制代码
import pygame

from pygame.sprite import Group
from settings3 import Settings
from ship5 import Ship

import game_functions9 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(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))
            
        gf.update_screen(ai_settings,screen,ship,bullets)                
        
run_game()

7.7 创建函数update_bullets()

让主程序文件alien_invasion17.py尽可能简单,可将子弹管理代码移到模块game_functions 中。 我们创建一个名为update_bullets() 的新函数, 并将其添加到game_functions10.py的末尾:

game_functions10.py

python 复制代码
import sys

import pygame

from bullet1 import 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中
        if len(bullets) < ai_settings.bullets_allowed:
            new_bullet = Bullet(ai_settings, screen, ship)
            bullets.add(new_bullet)

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, 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, ship, bullets)
        elif event.type == pygame.KEYUP:
            check_keyup_events(event,ship)

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()
    
def update_bullets(bullets):
    """更新子弹的位置,并删除已消失的子弹"""
    #更新子弹的位置
    bullets.update()

    #删除已消失的子弹
    for bullet in bullets.copy():
        if bullet.rect.bottom <= 0:
            bullets.remove(bullet)

编组bullets作为参数传入update_bullets() 函数,alien_invasion17.py中的while 循环就变得很简单了:

alien_invasion17.py

python 复制代码
import pygame

from pygame.sprite import Group
from settings3 import Settings
from ship5 import Ship

import game_functions10 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(ai_settings,screen)
    
    #创建一个用于存储子弹的编组
    bullets = Group()
    
    #开始游戏的主循环
    while True:
        gf.check_events(ai_settings,screen,ship,bullets)
❶        ship.update()
❷        bullets.update()        
❸        gf.update_bullets(bullets)    
❹        gf.update_screen(ai_settings,screen,ship,bullets)                
        
run_game()

我们让主循环尽可能精简, 通过函数名就能迅速知道游戏中发生的情况。 主循环检查玩家的输入(见❶) , 然后更新飞船的位置(见❷) 和所有未消失的子弹的位置(见❸) 。 接下来, 我们使用更新后的位置来绘制新屏幕(见❹) 。

7.8 创建函数fire_bullet()

下面将发射子弹的代码移到一个独立的函数中, 这样, 在check_keydown_events() 中只需使用一行代码来发射子弹, 让elif 代码块变得非常简单:

game_functions11.py

python 复制代码
import sys

import pygame

from bullet1 import 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:
        fire_bullet(ai_settings, screen, ship, bullets)

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, 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, ship, bullets)
        elif event.type == pygame.KEYUP:
            check_keyup_events(event,ship)

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()
    
def update_bullets(bullets):
    """更新子弹的位置,并删除已消失的子弹"""
    #更新子弹的位置
    bullets.update()

    #删除已消失的子弹
    for bullet in bullets.copy():
        if bullet.rect.bottom <= 0:
            bullets.remove(bullet)

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)

函数fire_bullet() 只包含玩家按空格键时用于发射子弹的代码; 在check_keydown_events() 中, 我们在玩家按空格键时调用fire_bullet() 。

请再次运行alien_invasion18.py, 确认发射子弹时依然没有错误。

alien_invasion18.py

python 复制代码
import pygame

from pygame.sprite import Group
from settings3 import Settings
from ship5 import Ship

import game_functions11 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(ai_settings,screen)
    
    #创建一个用于存储子弹的编组
    bullets = Group()
    
    #开始游戏的主循环
    while True:
        gf.check_events(ai_settings,screen,ship,bullets)
        ship.update()
        bullets.update()        
        gf.update_bullets(bullets)    
        gf.update_screen(ai_settings,screen,ship,bullets)                
        
run_game()

8 小结

在本章中, 你学习了: 游戏开发计划的制定; 使用Pygame编写的游戏的基本结构; 如何设置背景色, 以及如何将设置存储在可供游戏的各个部分访问的独立类中; 如何在屏幕上 绘制图像, 以及如何让玩家控制游戏元素的移动; 如何创建自动移动的元素, 如在屏幕中向上飞驰的子弹, 以及如何删除不再需要的对象; 如何定期重构项目的代码, 为后续开 发提供便利。

相关推荐
databook6 小时前
Manim实现闪光轨迹特效
后端·python·动效
Juchecar7 小时前
解惑:NumPy 中 ndarray.ndim 到底是什么?
python
用户8356290780517 小时前
Python 删除 Excel 工作表中的空白行列
后端·python
Json_7 小时前
使用python-fastApi框架开发一个学校宿舍管理系统-前后端分离项目
后端·python·fastapi
数据智能老司机14 小时前
精通 Python 设计模式——分布式系统模式
python·设计模式·架构
数据智能老司机15 小时前
精通 Python 设计模式——并发与异步模式
python·设计模式·编程语言
数据智能老司机15 小时前
精通 Python 设计模式——测试模式
python·设计模式·架构
数据智能老司机15 小时前
精通 Python 设计模式——性能模式
python·设计模式·架构
c8i15 小时前
drf初步梳理
python·django
每日AI新事件15 小时前
python的异步函数
python