Python项目1 外星人入侵

武装飞船

1 规划项目

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

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

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

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

2 安装Pygame

注意:在Python3.6及更新的版本中,PIP已经捆绑安装了,不需要再单独安装(可能需要更新)。

开始编码前,先来安装Pygame。下面介绍如何在Linux、OS X和Microsoft Windows中安装Pygame。

如果你使用的是Linux系统和Python 3,或者是OS X系统,就需要使用pip来安装Pygame。pip是一个负责为你下载并安装Python包的程序。接下来的几小节介绍如何使用pip来安装 Python包。

如果你使用的是Linux系统和Python 2.7,或者是Windows,就无需使用pip来安装Pygame;;在这种情况下,请直接跳到2.2节或2.4节。

注意 :接下来的部分包含在各种系统上安装pip的说明,因为数据可视化项目和Web应用程序项目都需要pip。这些说明也可在https:/www.nostarch.com/pythoncrashcourse/注意 在线资源中找到。如果安装时遇到麻烦,看看在线说明是否管用。

2.1 使用pip安装Python包

大多数较新的Python版本都自带pip,因此首先可检查系统是否已经安装了pip。在Python 3中,pip有时被称为pip3。

1. 在Linux和OS X系统中检查是否安装了pip

打开一个终端窗口,并执行如下命令:

Kotlin 复制代码
$ pip --version
 ❶ pip 7.0.3 from /usr/local/lib/python3.5/dist-packages (python 3.5)
 $

如果你的系统只安装了一个版本的Python,并看到了类似于上面的输出,请跳到2.2节或2.3节。如果出现了错误消息,请尝试将pip替换为pip3。如果这两个版本都没有安装到 你的系统中,请跳到"安装pip"。

如果你的系统安装了多个版本的Python,请核实pip关联到了你使用的Python版本,如python 3.5(见❶)。如果pip关联到了正确的Python版本,请跳到2.2节或2.3节。如果pip没 有关联到正确的Python版本,请尝试将pip替换为pip3。如果执行这两个命令时,输出都表明没有关联到正确的Python版本,请"安装pip"。

2. 在Windows系统中检查是否安装了pip

打开一个终端窗口,并执行如下命令:

python 复制代码
$ python -m pip --version
 ❶ pip 7.0.3 from C:\Python35\lib\site-packages (python 3.5)
 $

如果你的系统只安装了一个版本的Python,并看到了类似于上面的输出,请跳到2.4节。如果出现了错误消息,请尝试将pip替换为pip3。如果执行这两个命令时都出现错误消息,请"安装pip"。

如果你的系统安装了多个版本的Python,请核实pip关联到了你使用的Python版本,如python 3.5(见❶)。如果pip关联到了正确的Python版本,请跳到2.4节。如果pip没有关联到 正确的Python版本,请尝试将pip替换为pip3。如果执行这两个命令时都出现错误消息,请跳到"安装pip"。

3. 安装pip

要安装pip,请访问https:/bootstrap.pypa.io/get-pip.py。如果出现对话框,请选择保存文件;如果get-pip.py的代码出现在浏览器中,请将这些代码复制并粘贴到文本编辑器中,再将文 件保存为get-pip.py。将get-pip.py保存到计算机中后,你需要以管理员身份运行它,因为pip将在你的系统中安装新包。

注意: 如果你找不到get-pip.py,请访问https:/pip.pypa.io/ ,单击左边面板中的Instalation,再单击中间窗口中的链接get-pip.py

4. 在Linux和OS X系统中安装pip

使用下面的命令以管理员身份运行get-pip.py

cs 复制代码
$ sudo python get-pip.py

**注意 :**如果你启动终端会话时使用的是命令python3 ,那么在这里应使用命令sudo python3 get-pip.py

这个程序运行后,使用命令pip --version (或pip3 --version )确认正确地安装了pip。

5. 在Windows系统中安装pip

使用下面的命令运行get-pip.py

cs 复制代码
$ python get-pip.py

如果你在终端中运行Python时使用的是另一个命令,也请使用这个命令来运行get-pip.py。例如,你可能需要使用命令python3 get-pip.py 或C:\Python35\python get pip.py

这个程序运行后,执行命令python -m pip --version 以确认成功地安装了pip。

2.2 在Linux系统中安装Pygame

如果你使用的是Python 2.7,请使用包管理器来安装Pygame。为此,打开一个终端窗口,并执行下面的命令,这将下载Pygame,并将其安装到你的系统中:

Kotlin 复制代码
$ sudo apt-get install python-pygame

执行如下命令,在终端会话中检查安装情况:

python 复制代码
$ python
 >>> import pygame
 >>>

如果没有任何输出,就说明Python导入了Pygame,你可以跳到3节。

如果你使用的是Python 3,就需要执行两个步骤:安装Pygame依赖的库;下载并安装Pygame。

执行下面的命令来安装Pygame依赖的库(如果你开始终端会话时使用的是命令python3.5 ,请将python3-dev 替换为python3.5-dev ):

cs 复制代码
$ sudo apt-get install python3-dev mercurial
 $ sudo apt-get install libsdl-image1.2-dev libsdl2-dev libsdl-ttf2.0-dev

这将安装运行《外星人入侵》时需要的库。如果你要启用Pygame的一些高级功能,如添加声音的功能,可安装下面这些额外的库:

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

接下来,执行下面的命令来安装Pygame(如有必要,将pip 替换为pip3 ):

Kotlin 复制代码
$ pip install --user hg+http://bitbucket.org/pygame/pygame

告知你Pygame找到了哪些库后,输出将暂停一段时间。请按回车键,即便有一些库没有找到。你将看到一条消息,说明成功地安装了Pygame。 要确认安装成功,请启动一个Python终端会话,并尝试执行下面的命令来导入Pygame:

Kotlin 复制代码
$ python3
 >>> import pygame
 >>>

如果导入成功,请跳到3节。

2.3 在OS X系统中安装Pygame

要安装Pygame依赖的有些包,需要Homebrew。

为安装Pygame依赖的库,请执行下面的命令:

cpp 复制代码
$ brew install hg sdl sdl_image sdl_ttf

这将安装运行游戏《外星人入侵》所需的库。每安装一个库后,输出都会向上滚动。

如果你还想启用较高级的功能,如在游戏中包含声音,可安装下面两个额外的库:

Kotlin 复制代码
$ brew install sdl_mixer portmidi

使用下面的命令来安装Pygame(如果你运行的是Python 2.7,请将pip3 替换为pip ):

vbnet 复制代码
$ pip3 install --user hg+http://bitbucket.org/pygame/pygame

启动一个Python终端会话,并导入Pygame以检查安装是否成功(如果你运行的是Python 2.7,请将python3 替换为python ):

python 复制代码
$ python3
 >>> import pygame
 >>>

如果导入成功,请跳到3节。

2.4 在Windows系统中安装Pygame

Pygame项目托管在代码分享网站Bitbucket中。要在Windows系统中安装Pygame,请访问https://bitbucket.org/pygame/pygame/downloads/,查找与你运行的Python版本匹配的Windows安装 程序。如果在Bitbucket上找不到合适的安装程序,请去http://www.lfd.uci.edu/~gohlke/pythonlibs/#pygame 看看。

下载合适的文件后,如果它是.exe文件,就运行它。

如果该文件的扩展名为.whl,就将它复制到你的项目文件夹中。再打开一个命令窗口,切换到该文件所在的文件夹,并使用pip来运行它:

cpp 复制代码
> python -m pip install --user pygame-1.9.2a0-cp35-none-win32.whl

3 开始游戏项目

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

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

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

alien_invasion.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能够正确地工作。在❷处,我们调 用pygame.display.set_mode() 来创建一个名为screen 的显示窗口,这个游戏的所有图形元素都将在其中绘制。实参(1200, 800) 是一个元组,指定了游戏窗口的尺 寸。通过将这些尺寸值传递给pygame.display.set_mode() ,我们创建了一个宽1200像素、高800像素的游戏窗口(你可以根据自己的显示器尺寸调整这些值)。

对象screen 是一个surface。在Pygame中,surface是屏幕的一部分,用于显示游戏元素。在这个游戏中,每个元素(如外星人或飞船)都是一个surface。display.set_mode() 返回的surface表示整个游戏窗口。我们激活游戏的动画循环后,每经过一次循环都将自动重绘这个surface。

这个游戏由一个while 循环(见❸)控制,其中包含一个事件循环以及管理屏幕更新的代码。事件是用户玩游戏时执行的操作,如按键或移动鼠标。为让程序响应事件,我们编 写一个事件循环,以侦听事件,并根据发生的事件执行相应的任务。❹处的for 循环就是一个事件循环。

为访问Pygame检测到的事件,我们使用方法pygame.event.get() 。所有键盘和鼠标事件都将促使for 循环运行。在这个循环中,我们将编写一系列的if 语句来检测并响应 特定的事件。例如,玩家单击游戏窗口的关闭按钮时,将检测到pygame.QUIT 事件,而我们调用sys.exit() 来退出游戏(见❺)。

❻处调用了pygame.display.flip() ,命令Pygame让最近绘制的屏幕可见。在这里,它在每次执行while 循环时都绘制一个空屏幕,并擦去旧屏幕,使得只有新屏幕可 见。在我们移动游戏元素时,pygame.display.flip() 将不断更新屏幕,以显示元素的新位置,并在原来的位置隐藏元素,从而营造平滑移动的效果。

在这个基本的游戏结构中,最后一行调用run_game() ,这将初始化游戏并开始主循环。

如果此时运行这些代码,你将看到一个空的Pygame窗口。

3.2 设置背景色

Pygame默认创建一个黑色屏幕,这太乏味了。下面来将背景设置为另一种颜色:

alien_invasion.py

python 复制代码
  --snip--
  def run_game():
      --snip--
      pygame.display.set_caption("Alien Invasion")
      # 设置背景色
❶     bg_color = (230, 230, 230)
      # 开始游戏主循环.
      while True:
          # 监听键盘和鼠标事件
          --snip--
          # 每次循环时都重绘屏幕
❷         screen.fill(bg_color)
          # 让最近绘制的屏幕可见
          pygame.display.flip()
  run_game()

首先,我们创建了一种背景色,并将其存储在bg_color 中(见❶)。该颜色只需指定一次,因此我们在进入主while 循环前定义它。

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

在❷处,我们调用方法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_invasion.py修改成下面这样:

alien_invasion.py

python 复制代码
  --snip--
  import pygame
  from settings import Settings
  def run_game():
      # 初始化pygame、设置和屏幕对象
      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:
          --snip--
          # 每次循环时都重绘屏幕
❸         screen.fill(ai_settings.bg_color)
          # 让最近绘制的屏幕可见
          pygame.display.flip()
  run_game()

在主程序文件中,我们导入Settings 类,调用pygame.init() ,再创建一个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等工具将其转换为位图。

选择图像时,要特别注意其背景色。请尽可能选择背景透明的图像,这样可使用图像编辑器将其背景设置为任何颜色。图像的背景色与游戏的背景色相同时,游戏看起来最漂 亮;你也可以将游戏的背景色设置成与图像的背景色相同。

就游戏《外星人入侵》而言,你可以使用文件ship.bmp,这个文件可在本书的配套资源(https://www.nostarch.com/pythoncrashcourse/ )中找到。这个文件的背景色 与这个项目使用的设置相同。请在主项目文件夹(alien_invasion)中新建一个文件夹,将其命名为images,并将文件ship.bmp保存到这个文件夹中。

4.1 创建Ship 类

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

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

首先,我们导入了模块pygame 。Ship 的方法__init__() 接受两个参数:引用self 和screen ,其中后者指定了要将飞船绘制到什么地方。为加载图像,我们调用 了pygame.image.load() (见❶)。这个函数返回一个表示飞船的surface,而我们将这个surface存储到了self.image 中。

加载图像后,我们使用get_rect() 获取相应surface的属性rect (见❷)。Pygame的效率之所以如此高,一个原因是它让你能够像处理矩形(rect 对象)一样处理游戏元 素,即便它们的形状并非矩形。像处理矩形一样处理游戏元素之所以高效,是因为矩形是简单的几何形状。这种做法的效果通常很好,游戏玩家几乎注意不到我们处理的不是游 戏元素的实际形状。

处理rect 对象时,可使用矩形四角和中心的 x 和 y 坐标。可通过设置这些值来指定矩形的位置。

要将游戏元素居中,可设置相应rect 对象的属性center 、centerx 或centery 。要让游戏元素与屏幕边缘对齐,可使用属性top 、bottom 、left 或right ;要调整游 戏元素的水平或垂直位置,可使用属性x 和y ,它们分别是相应矩形左上角的 x 和 y 坐标。这些属性让你无需去做游戏开发人员原本需要手工完成的计算,你经常会用到这些属 性。

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

我们将把飞船放在屏幕底部中央。为此,首先将表示屏幕的矩形存储在self.screen_rect 中(见❸),再将self.rect.centerx (飞船中心的x 坐标)设置为表示屏幕 的矩形的属性centerx (见❹),并将self.rect.bottom (飞船下边缘的y 坐标)设置为表示屏幕的矩形的属性bottom 。Pygame将使用这些rect 属性来放置飞船图像, 使其与屏幕下边缘对齐并水平居中。

在❺处,我们定义了方法blitme() ,它根据self.rect 指定的位置将图像绘制到屏幕上。

4.2 在屏幕上绘制飞船

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

alien_invasion.py

python 复制代码
 --snip--
  from settings import Settings
  from ship import Ship
  def run_game():
      --snip--
      pygame.display.set_caption("Alien Invasion")
      # 创建一艘飞船
❶     ship = Ship(screen)
      # 开始游戏主循环
      while True:
          --snip--
          # 每次循环时都重绘屏幕
          screen.fill(ai_settings.bg_color)
 ❷         ship.blitme()
          # 让最近绘制的屏幕可见
          pygame.display.flip()
  run_game()

我们导入Ship 类,并在创建屏幕后创建一个名为ship 的Ship 实例。必须在主while 循环前面创建该实例(见❶),以免每次循环时都创建一艘飞船。填充背景后,我们调 用ship.blitme() 将飞船绘制到屏幕上,确保它出现在背景前面(见❷)。

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

5 重构:模块game_functions

在大型项目中,经常需要在添加新代码前重构既有代码。重构旨在简化既有代码的结构,使其更容易扩展。在本节中,我们将创建一个名为game_functions 的新模块,它将 存储大量让游戏《外星人入侵》运行的函数。通过创建模块game_functions ,可避免alien_invasion.py太长,并使其逻辑更容易理解。

5.1 函数check_events()

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

将check_events() 放在一个名为game_functions 的模块中:

game_functions.py

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

这个模块中导入了事件检查循环要使用的sys 和pygame 。当前,函数check_events() 不需要任何形参,其函数体复制了alien_invasion.py的事件循环。

下面来修改alien_invasion.py,使其导入模块game_functions ,并将事件循环替换为对函数check_events() 的调用:

alien_invasion.py

php 复制代码
 import pygame
 from settings import Settings
 from ship import Ship
 import game_functions as gf
 def run_game():
    --snip--
    # 开始游戏主循环
    while True:
        gf.check_events()
        # 让最近绘制的屏幕可见
        --snip--

在主程序文件中,不再需要直接导入sys ,因为当前只在模块game_functions 中使用了它。出于简化的目的,我们给导入的模块game_functions 指定了别名gf 。

5.2 函数update_screen()

为进一步简化run_game() ,下面将更新屏幕的代码移到一个名为update_screen() 的函数中,并将这个函数放在模块game_functions.py 中:

game_functions.py

python 复制代码
--snip--
 def check_events():
    --snip--
 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_invasion.py的while 循环中更新屏幕的代码替换为对函数update_screen() 的调用:

alien_invasion.py

python 复制代码
--snip--
    # 开始游戏主循环
    while True:
        gf.check_events()
        gf.update_screen(ai_settings, screen, ship)
 run_game()

这两个函数让while 循环更简单,并让后续开发更容易:在模块game_functions 而不是run_game() 中完成大部分工作。

鉴于我们一开始只想使用一个文件,因此没有立刻引入模块game_functions 。这让你能够了解实际的开发过程:一开始将代码编写得尽可能简单,并在项目越来越复杂时进 行重构。

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

动手试一试

蓝色天空 :创建一个背景为蓝色的Pygame窗口。

游戏角色 :找一幅你喜欢的游戏角色位图图像或将一幅图像转换为位图。创建一个类,将该角色绘制到屏幕中央,并将该图像的背景色设置为屏幕背景色,或将 屏幕背景色设置为该图像的背景色。

6 驾驶飞船

下面来让玩家能够左右移动飞船。为此,我们将编写代码,在用户按左或右箭头键时作出响应。我们将首先专注于向右移动,再使用同样的原理来控制向左移动。通过这样做, 你将学会如何控制屏幕图像的移动。

6.1 响应按键

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

检测到KEYDOWN 事件时,我们需要检查按下的是否是特定的键。例如,如果按下的是右箭头键,我们就增大飞船的rect.centerx 值,将飞船向右移动:

game_functions.py

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

我们在函数check_events() 中包含形参ship ,因为玩家按右箭头键时,需要将飞船向右移动。在函数check_events() 内部,我们在事件循环中添加了一个elif 代码 块,以便在Pygame 检测到KEYDOWN 事件时作出响应(见❶)。我们读取属性event.key ,以检查按下的是否是右箭头键(pygame.K_RIGHT )(见❷)。如果按下的是右 箭头键,就将ship.rect.centerx 的值加1,从而将飞船向右移动(见❸)。

在alien_invasion.py中,我们需要更新调用的check_events() 代码,将ship 作为实参传递给它:

alien_invasion.py

python 复制代码
  # 开始游戏主循环
    while True:
        gf.check_events(ship)
        gf.update_screen(ai_settings, screen, ship)

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

6.2 允许不断移动

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

飞船不动时,标志moving_right 将为False 。玩家按下右箭头键时,我们将这个标志设置为True ;而玩家松开时,我们将这个标志重新设置为False。

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

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

ship.py

python 复制代码
 class Ship():
      def __init__(self, screen):
          --snip--
          # 将每艘新飞船放在屏幕底部中央
          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):
          --snip--

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

下面来修改check_events() ,使其在玩家按下右箭头键时将moving_right 设置为True ,并在玩家松开时将moving_right 设置为False :

game_functions.py

python 复制代码
 def check_events(ship):
      """响应按键和鼠标事件"""
      for event in pygame.event.get():
          --snip--
          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

在❶处,我们修改了游戏在玩家按下右箭头键时响应的方式:不直接调整飞船的位置,而只是将moving_right 设置为True 。在❷处,我们添加了一个新的elif 代码块,用 于响应KEYUP 事件:玩家松开右箭头键(K_RIGHT )时,我们将moving_right 设置为False 。

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

alien_invasion.py

python 复制代码
  # 开始游戏主循环
    while True:
        gf.check_events(ship)
        ship.update()
        gf.update_screen(ai_settings, screen, ship)

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

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

6.3 左右移动

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

ship.py

python 复制代码
 def __init__(self, screen):
        --snip--
        # 移动标志
        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

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

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

game_functions.py

python 复制代码
def check_events(ship):
    """响应按键和鼠标事件"""
    for event in pygame.event.get():
        --snip--
        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

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

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

下面来进一步优化飞船的移动方式:调整飞船的速度;限制飞船的移动距离,以免它移到屏幕外面去。

6.4 调整飞船的速度

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

settings.py

python 复制代码
class Settings():
    """一个存储游戏《外星人入侵》的所有设置的类"""
    def __init__(self):
        --snip--
        # 飞船的设置
        self.ship_speed_factor = 1.5

我们将ship_speed_factor 的初始值设置成了1.5 。需要移动飞船时,我们将移动1.5像素而不是1像素。

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

ship.py

python 复制代码
 class Ship():
 ❶     def __init__(self, ai_settings, screen):
          """初始化飞船并设置其初始位置"""
          self.screen = screen
 ❷         self.ai_settings = ai_settings
          --snip--
          # 将每艘新飞船放在屏幕底部中央
          --snip--
          # 在飞船的属性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):
          --snip--

在❶处,我们在__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_invasion.py中创建Ship 实例时,需要传入实参ai_settings :

alien_invasion.py

python 复制代码
--snip--
 def run_game():
    --snip--
    # 创建飞船
    ship = Ship(ai_settings, screen)
    --snip--

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

6.5 限制飞船的活动范围

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

ship.py

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

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

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

6.6 重构check_events()

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

game_functions.py

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

我们创建了两个新函数:check_keydown_events() 和check_keyup_events() ,它们都包含形参event 和ship 。这两个函数的代码是从check_events() 中复制 而来的,因此我们将函数check_events 中相应的代码替换成了对这两个函数的调用。现在,函数check_events() 更简单,代码结构更清晰。这样,在其中响应其他玩家 输入时将更容易。

7 简单回顾

下一节将添加射击功能,这需要新增一个名为bullet.py的文件,并对一些既有文件进行修改。当前,我们有四个文件,其中包含很多类、函数和方法。添加其他功能之前,为让你 清楚这个项目的组织结构,先来回顾一下这些文件。

7.1 alien_invasion.py

主文件alien_invasion.py创建一系列整个游戏都要用到的对象:存储在ai_settings 中的设置、存储在screen 中的主显示surface以及一个飞船实例。文件alien_invasion.py还包含游 戏的主循环,这是一个调用check_events() 、ship.update() 和update_screen() 的while 循环。

要玩游戏《外星人入侵》,只需运行文件alien_invasion.py。其他文件(settings.py、game_functions.py、ship.py)包含的代码被直接或间接地导入到这个文件中。

7.2 settings.py

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

7.3 game_functions.py

文件game_functions.py包含一系列函数,游戏的大部分工作都是由它们完成的。函数check_events() 检测相关的事件,如按键和松开,并使用辅助函 数check_keydown_events() 和check_keyup_events() 来处理这些事件。就目前而言,这些函数管理飞船的移动。模块game_functions 还包含函 数update_screen() ,它用于在每次执行主循环时都重绘屏幕。

7.4 ship.py

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

动手试一试

火箭 :编写一个游戏,开始时屏幕中央有一个火箭,而玩家可使用四个方向键上下左右移动火箭。请务必确保火箭不会移到屏幕外面。

按键 :创建一个程序,显示一个空屏幕。在事件循环中,每当检测到pygame.KEYDOWN 事件时都打印属性event.key 。运行这个程序,并按各种键,看看 Pygame如何响应。

8 射击

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

8.1 添加子弹设置

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

settings.py

python 复制代码
  def __init__(self):
        --snip--
        # 子弹设置
        self.bullet_speed_factor = 1
        self.bullet_width = 3
        self.bullet_height = 15
        self.bullet_color = 60, 60, 60

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

8.2 创建Bullet 类

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

bullet.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 。

注意代码super(Bullet, self).init() 使用了Python 2.7语法。这种语法也适用于Python 3,但你也可以将这行代码简写为super().init() 。

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

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

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

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

bullet.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 的值(见❷)。属性speed_factor 让我们能够随着游戏的进行或根据需要提高子弹的速度,以调整游戏的行 为。子弹发射后,其 x 坐标始终不变,因此子弹将沿直线垂直地往上穿行。

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

8.3 将子弹存储到编组中

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

alien_invasion.py

python 复制代码
 import pygame
  from pygame.sprite import Group
  --snip--
  def run_game():
      --snip--
      # 创建一艘飞船
      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 。这个编组是在while 循环外面创建的,这样就无需每次运行该 循环时都创建一个新的子弹编组。

注意如果在循环内部创建这样的编组,游戏运行时将创建数千个子弹编组,导致游戏慢得像蜗牛。如果游戏停滞不前,请仔细查看主while 循环中发生的情况。

我们将bullets 传递给了check_events() 和update_screen() 。在check_events() 中,需要在玩家按空格键时处理bullets ;而在update_screen() 中,需要 更新要绘制到屏幕上的bullets 。

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

8.4 开火

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

game_functions.py

python 复制代码
 --snip--
  from bullet import Bullet
 ❶ def check_keydown_events(event, ai_settings, screen, ship, bullets):
      --snip--
 ❷     elif event.key == pygame.K_SPACE:
          # 创建一颗子弹,并将其加入到编组bullets中
          new_bullet = Bullet(ai_settings, screen, ship)
          bullets.add(new_bullet)
         --snip--
 ❸ def check_events(ai_settings, screen, ship, bullets):
      """响应按键和鼠标事件"""
      for event in pygame.event.get():
          --snip--
          elif event.type == pygame.KEYDOWN:
              check_keydown_events(event, ai_settings, screen, ship, bullets)
          --snip--
 ❹ def update_screen(ai_settings, screen, ship, bullets):
      --snip--
      # 在飞船和外星人后面重绘所有子弹
❺     for bullet in bullets.sprites():
          bullet.draw_bullet()
      ship.blitme()
      --snip--

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

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

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

如果此时运行alien_invasion.py,将能够左右移动飞船,并发射任意数量的子弹。子弹在屏幕上向上穿行,抵达屏幕顶部后消失,如图所示。可在settings.py中修改子弹的尺寸、 颜色和速度。

8.5 删除已消失的子弹

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

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

alien_invasion.py

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

在for 循环中,不应从列表或编组中删除条目,因此必须遍历编组的副本。我们使用了方法copy() 来设置for 循环(见❶),这让我们能够在循环中修改bullets 。我们检 查每颗子弹,看看它是否已从屏幕顶端消失(见❷)。如果是这样,就将其从bullets 中删除(见❸)。在❹处,我们使用了一条print 语句,以显示当前还有多少颗子弹, 从而核实已消失的子弹确实删除了。

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

8.6 限制子弹数量

很多射击游戏都对可同时出现在屏幕上的子弹数量进行限制,以鼓励玩家有目标地射击。下面在游戏《外星人入侵》中作这样的限制。

首先,在settings.py中存储所允许的最大子弹数:

settings.py

python 复制代码
 # 子弹设置
        self.bullet_width = 3
        self.bullet_height = 15
        self.bullet_color = 60, 60, 60
        self.bullets_allowed = 3

这将未消失的子弹数限制为3颗。在game_functions.py的check_keydown_events() 中,我们在创建新子弹前检查未消失的子弹数是否小于该设置:

game_functions.py

python 复制代码
 def check_keydown_events(event, ai_settings, screen, ship, bullets):
    --snip--
    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)

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

8.7 创建函数update_bullets()

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

game_functions.py

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

update_bullets() 的代码是从alien_invasion.py剪切并粘贴而来的,它只需要一个参数,即编组bullets 。

alien_invasion.py中的while 循环又变得很简单了:

alien_invasion.py

python 复制代码
 # 开始游戏主循环
      while True:
 ❶         gf.check_events(ai_settings, screen, ship, bullets)
 ❷         ship.update()
 ❸         gf.update_bullets(bullets)
 ❹         gf.update_screen(ai_settings, screen, ship, bullets)

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

8.8 创建函数fire_bullet()

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

game_functions.py

python 复制代码
def check_keydown_events(event, ai_settings, screen, ship, bullets):
 """响应按键"""--snip--
 elif event.key == pygame.K_SPACE:
 fire_bullet(ai_settings, screen, ship, bullets)
 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_invasion.py,确认发射子弹时依然没有错误。

动手试一试

**侧面射击 :**编写一个游戏,将一艘飞船放在屏幕左边,并允许玩家上下移动飞船。在玩家按空格键时,让飞船发射一颗在屏幕中向右穿行的子弹,并在子弹离开 屏幕而消失后将其删除。

9 小结

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

相关推荐
青 春 记 忆5 小时前
Dify Docker Compose 通用无损升级指南:从备份、双版本预演到切换与回滚
运维·人工智能·python·docker·容器
GlueNa2SiO35 小时前
03-Flask模板引擎Jinja2详解
笔记·python·flask
临沂GEO8 小时前
GEO搜索优化科普|正规地理位置流量运营入门指南
大数据·人工智能·python·流量运营
大模型码小白8 小时前
Spring AI Tool 实现自然语言操作 MySQL 数据库详解
服务器·开发语言·数据库·人工智能·python·mysql·spring
荷蒲9 小时前
【小白量化Qbuddy】利用AI学习中文Python
人工智能·python·学习
数据狐(Datafox)11 小时前
京东商品列表API技术解析与落地应用(含标准 JSON 示例)
java·大数据·前端·人工智能·python·数据分析·json
量化吞吐机11 小时前
2026年下半年手工交易规则走向量化表达,产品该先接住哪一步
人工智能·python
Metaphor69212 小时前
使用 Python 读取和删除 Excel 文件属性
python·excel
迷迭香yy12 小时前
Python构建转融券多空识别系统从接口到因子的全流程 IG50免费开源股票数据API接口
开发语言·python·开源
月光船幽幽13 小时前
真实即粗糙,光滑是伪造
人工智能·python