目录

外星人入侵-Python

外星人入侵-Python

武装飞船

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

你将安装Pygame,再创建一艘能够根据用户输入左右移动和射击的飞船。在接下来的两章,你将创建一群作为射杀目标的外星人,并改进该游戏:限制可供玩家使用的飞船数,并且添加记分牌。

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

一.第一阶段

创建一艘飞船,它可左右移动,并且能在用户按空格键时开火。设置好这种行为后,就可以创建外星人并提高游戏的可玩性了。

安装Pygame

在终端提示符下执行如下命令

$ python -m pip install --user pygame 

1.1开始游戏项目

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

创建一个表示游戏的类,以创建空的Pygame窗口。

alien_invasion.py

//首先导入模块sys 和pygame.模块pygame 包含开发游戏所需的功能玩家退
出时我们将使用模块sys 中的工具来退出游戏.
import sys 
import pygame
class AlienInvasion:
    """管理游戏资源和行为的类"""
    def __init__(self):
        """初始化游戏并创建游戏资源"""
        pygame.init()                                      #1

        self.screen = pygame.display.set_mode((1200, 800)) #2
        pygame.display.set_caption("Alien Invasion")

    def run_game(self):
        """开始游戏的主循环"""
        while True:                                        #3
            for event in pygame.event.get():               #4
                if event.type == pygame.QUIT:              #5
                    sys.exit()

             
            #让最近绘制的屏幕可见。
            pygame.display.flip()                          #6
if __name__ == '__main__':
    ai = AlienInvasion()
    ai.run_game()

为开发游戏《外星人入侵》,我们创建了一个表示它的类,名为AlienInvasion 。在这个类的方法__init__() 中,调用函数pygame.init() 来初始化背景设置,让Pygame能够正确地工作. (#1)

(#2) 调用 pygame.display.set_mode() 来创建一个显示窗口,游戏的所有图形元素都将在其中绘制。实参(1200, 800) 是一个元组,指定了游戏窗口的尺寸——宽1200 像素、高800像素(你可以根据自己的显示器尺寸调整这些值)。将这个显示窗口赋给属性self.screen ,让这个类中的所有方法都能够使用它。

赋给属性self.screen 的对象是一个

surface

。在Pygame中,surface是屏幕的一 部分,用于显示游戏元素。在这个游戏中,每个元素(如外星人或飞船)都是一个 surface。display.set_mode() 返回的surface表示整个游戏窗口。激活游戏的 动画循环后,每经过一次循环都将自动重绘这个surface,将用户输入触发的所有变化都反映出来。

这个游戏由方法run_game() 控制。该方法包含一个不断运行的while 循环 (#3) ,而这个循环包含一个事件循环以及管理屏幕更新的代码。 事件 是用户玩游戏 时执行的操作,如按键或移动鼠标。为程序响应事件,可编写一个 事件循环 ,以 侦听事件并根据发生的事件类型执行合适的任务。(#4)for 循环就是一个事件循环。

为访问Pygame检测到的事件,我们使用了函数pygame.event.get() 。这个函数 返回一个列表,其中包含它在上一次被调用后发生的所有事件。所有键盘和鼠标事 件都将导致这个for 循环运行。在这个循环中,我们将编写一系列if 语句来检测 并响应特定的事件。例如,当玩家单击游戏窗口的关闭按钮时,将检测到 pygame.QUIT 事件,进而调用sys.exit() 来退出游戏. (#5)

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

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

https://i-blog.csdnimg.cn/direct/5312f6fe159740d39383ed85016f05a2.png

1.1.2设置背景色

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

 def __init__(self): 
          --snip-- 
          pygame.display.set_caption("Alien Invasion") 
          #设置背景色。 
         self.bg_color = (230, 230, 230) 
      def run_game(self): 
          --snip-- 
              for event in pygame.event.get(): 
                  if event.type == pygame.QUIT: 
                      sys.exit() 
 
              # 每次循环时都重绘屏幕。 
              self.screen.fill(self.bg_color) 
 
              # 让最近绘制的屏幕可见。 
              pygame.display.flip()
1.1.3创建设置类

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

settings.py

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

import sys
import pygame
from settings import Settings


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

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

        self.screen = pygame.display.set_mode(        
            (self.settings.screen_width, self.settings.screen_height))#2
        pygame.display.set_caption("Alien Invasion")

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

    def run_game(self):
        """开始游戏的主循环"""
        while True:
            for event in pygame.event.get():
                 if event.type == pygame.QUIT:
                    sys.exit()

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

            # 让最近绘制的屏幕可见。
            pygame.display.flip()
if __name__ == '__main__':
    ai = AlienInvasion()
    ai.run_game()

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

1.2添加飞船图像

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

1.2.1创建 Ship 类

我们创建一个名为ship 的 模块,其中包含Ship 类,负责管理飞船的大部分行为。

ship.py

import pygame
from settings import Settings
class Ship:
    """管理飞船的类"""
    def __init__(self,ai_game):
        """初始化飞船并设置其初始位置"""
        self.screen = ai_game.screen                      #1
        self.screen_rect = ai_game.screen.get_rect()      #2
        #加载飞船图像并获取其外接矩形
        self.image = pygame.image.load('images/ship.bmp') #3
        self.rect = self.image.get_rect()
        #将每艘新飞船放在屏幕底部中央
        self.rect.midbottom = self.screen_rect.midbottom  #4
    def blitme(self):                                     #5
        """在指定位置绘制飞船。"""
        self.screen.blit(self.image, self.rect)

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

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

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

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

  • 注意 在Pygame中,原点(0, 0)位于屏幕左上角,向右下方移动时,坐标值将 增大。在1200 × 800的屏幕上,原点位于左上角,而右下角的坐标为(1200, 800)。这些坐标对应的是游戏窗口,而不是物理屏幕。我们要将飞船放在屏幕底部的中央。为此,将self.rect.midbottom 设置为表 示屏幕的矩形的属性midbottom (#4)。Pygame使用这些rect 属性来放置飞船 图像,使其与屏幕下边缘对齐并水平居中。

在(#5)处,定义了方法blitme() ,它将图像绘制到self.rect 指定的位置。

1.2.2在屏幕上绘制飞船

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

alien_invasion.py

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


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

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

        self.screen = pygame.display.set_mode(
            (self.settings.screen_width, self.settings.screen_height))
        pygame.display.set_caption("Alien Invasion")

        self.ship= Ship(self)                          #1
    def run_game(self):
        """开始游戏的主循环"""
        while True:
            for event in pygame.event.get():
                 if event.type == pygame.QUIT:
                    sys.exit()

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

            # 让最近绘制的屏幕可见。
            pygame.display.flip()
if __name__ == '__main__':
    ai = AlienInvasion()
    ai.run_game()

导入Ship 类,并在创建屏幕后创建一个Ship 实例 (#1) 。调用Ship() 时,必 须提供一个参数:一个AlienInvasion 实例。在这里,self 指向的是当前 AlienInvasion 实例。这个参数让Ship 能够访问游戏资源,如对象screen 。 我们将这个Ship 实例赋给了self.ship 。 填充背景后,调用ship.blitme() 将飞船绘制到屏幕上,确保它出现在背景前面 (#2) 。

https://i-blog.csdnimg.cn/direct/0b6c17be448f443dbf804a615cbc928f.png

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

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

1.3.1 方法 _check_events()

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

alien_invasion.py

    def run_game(self):
        """开始游戏的主循环"""
        while True:
            self._check_events()                      #1
            # 每次循环时都重绘屏幕。
            self.screen.fill(self.settings.bg_color)
            self.ship.blitme()

            # 让最近绘制的屏幕可见。
            pygame.display.flip()
    def _check_events(self):                          #2
        """响应按键和鼠标事件"""
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                sys.exit()
        

新增方法_check_events() (#2),并将检查玩家是否单击了关闭窗口按钮的 代码移到该方法中。 要调用当前类的方法,可使用句点表示法,并指定变量名self 和要调用的方法的 名称(#1)。我们在run_game() 的while 循环中调用这个新增的方法。

1.3.2 方法 _update_screen()

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

alien_invasion.py

 def run_game(self):
        """开始游戏的主循环"""
        while True:
            self._check_events()
            self._update_screen()
    def _check_events(self):
        --snip--
        
    def _update_screen(self):
        """更新屏幕上的图像,并切换到新屏幕"""
        self.screen.fill(self.settings.bg_color)
        self.ship.blitme()
        # 让最近绘制的屏幕可见。
        pygame.display.flip()

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

1.4驾驶飞船

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

1.4.1响应按键

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

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

alien_invasion.py

    def _check_events(self):
        """响应按键和鼠标事件"""
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                sys.exit()
            elif event.type == pygame.KEYDOWN:     #1
                if event.key == pygame.K_d:        #2
                    self.ship.rect.x+= 1           #3

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

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

1.4.2允许持续移动

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

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

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

ship.py

    def __init__(self,ai_game):
        """初始化飞船并设置其初始位置"""
        self.screen = ai_game.screen
        self.screen_rect = ai_game.screen.get_rect()
        #加载飞船图像并获取其外接矩形
        self.image = pygame.image.load('images/ship.bmp')
        self.rect = self.image.get_rect()
        #将每艘新飞船放在屏幕底部中央  
        self.rect.midbottom = self.screen_rect.midbottom  #1
        #移动标志
        self.moving_right = False 
    def update(self):                                     #2
        """根据移动标志调整飞船的位置"""
        if self.moving_right:
            self.rect.x += 1

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

alien_invasion.py

    def _check_events(self):
        """响应按键和鼠标事件"""
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                sys.exit()
            elif event.type == pygame.KEYDOWN:
                if event.key == pygame.K_RIGHT:
                    self.ship.moving_right = True    #1
            elif event.type == pygame.KEYUP:         #2
                if event.key == pygame.K_RIGHT:
                    self.ship.moving_right = False

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

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

alien_invasion.py

def run_game(self): 
        """开始游戏主循环。""" 
        while True: 
            self._check_events() 
            self.ship.update() 
            self._update_screen() 

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

1.4.3左右移动

现在飞船能够持续向右移动了,添加向左移动的逻辑也很容易。我们将再次修改 Ship 类和方法_check_events()

ship.py

  def __init__(self, ai_game): 
        --snip-- 
        # 
移动标志 
        self.moving_right = False 
        self.moving_left = False 
    def update(self): 
        """根据移动标志调整飞船的位置。""" 
        if self.moving_right: 
            self.rect.x += 1 
        if self.moving_left: 
            self.rect.x -= 1

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

alien_invasion.py

 def _check_events(self): 
        """响应按键和鼠标事件。""" 
        for event in pygame.event.get(): 
            --snip-- 
            elif event.type == pygame.KEYDOWN: 
                if event.key == pygame.K_RIGHT: 
                    self.ship.moving_right = True 
                elif event.key == pygame.K_LEFT: 
                    self.ship.moving_left = True 
            elif event.type == pygame.KEYUP: 
                if event.key == pygame.K_RIGHT: 
                    self.ship.moving_right = False 
                elif event.key == pygame.K_LEFT: 
                    self.ship.moving_left = False

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

1.4.4调整飞船的速度

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

class Settings: 
    """存储游戏《外星人入侵》中所有设置的类。""" 
    def __init__(self): 
        --snip-- 
        # 飞船设置 
        self.ship_speed = 1.5

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

ship.py

class Ship:
    """管理飞船的类"""
    def __init__(self,ai_game):                           #1
        """初始化飞船并设置其初始位置"""
        self.screen = ai_game.screen
        self.settings = ai_game.settings
        self.screen_rect = ai_game.screen.get_rect()
        
        #加载飞船图像并获取其外接矩形
        self.image = pygame.image.load('images/ship.bmp')
        self.rect = self.image.get_rect()
        
        #将每艘新飞船放在屏幕底部中央
        self.rect.midbottom = self.screen_rect.midbottom
        
        #在飞船的属性中存储小数值
        self.x = float(self.rect.x)                       #2
        
        #移动标志
        self.moving_right = False
        self.moving_left = False
    def update(self):
        """根据移动标志调整飞船的位置"""
        #更新飞船而不是rect对象的x值。
        if self.moving_right:
            self.x += self.settings.ship_speed             #3
        if self.moving_left:
            self.x -= self.settings.ship_speed
        
        #根据self.x更新rect对象
        self.rect.x = self.x                               #4

在 (#1) 处,给Ship 类添加属性settings ,以便能够在update() 中使用它。鉴于 现在调整飞船的位置时,将增减一个单位为像素的小数值,因此需要将位置赋给一 个能够存储小数值的变量。可使用小数来设置rect 的属性,但rect 将只存储这个 值的整数部分。为准确存储飞船的位置,定义一个可存储小数值的新属性self.x (#2) 。使用函数float() 将self.rect.x 的值转换为小数,并将结果赋给 self.x 。 现在在update() 中调整飞船的位置时,将self.x 的值增减 settings.ship_speed 的值 (#3) 。更新self.x 后,再根据它来更新控制飞 船位置的self.rect.x (#4) 。self.rect.x 只存储self.x 的整数部分, 但对显示飞船而言,这问题不大。 现在可以修改ship_speed 的值了。只要它的值大于1,飞船的移动速度就会比以 前更快。这有助于让飞船的反应速度足够快,以便射杀外星人,还让我们能够随着 游戏的进行加快游戏的节奏。

1.4.5限制飞船的活动范围

当前,如果玩家按住箭头键的时间足够长,飞船将飞到屏幕之外,消失得无影无踪。

ship.py

 def update(self):
        """根据移动标志调整飞船的位置"""
        #更新飞船而不是rect对象的x值。
        if self.moving_right and self.rect.right < self.screen_rect.right: #1
            self.x += self.settings.ship_speed         
        if self.moving_left and self.rect.left > 0:                        #2
            self.x -= self.settings.ship_speed

        #根据self.x更新rect对象
        self.rect.x = self.x

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

1.3.6 重构 _check_events()

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

alien_invasion.py

 def _check_events(self): 
        """响应鼠标和按键事件。""" 
        for event in pygame.event.get(): 
            if event.type == pygame.QUIT: 
                sys.exit() 
            elif event.type == pygame.KEYDOWN: 
                self._check_keydown_events(event) 
            elif event.type == pygame.KEYUP: 
                self._check_keyup_events(event) 
 
    def _check_keydown_events(self, event): 
        """响应按键。""" 
        if event.key == pygame.K_RIGHT: 
            self.ship.moving_right = True 
        elif event.key == pygame.K_LEFT: 
            self.ship.moving_left = True 
 
    def _check_keyup_events(self, event): 
        """响应松开。""" 
        if event.key == pygame.K_RIGHT: 
            self.ship.moving_right = False 
        elif event.key == pygame.K_LEFT: 
            self.ship.moving_left = False

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

1.5简单回顾

1.5.1 alien_invasion.py

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

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

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

1.5.2 settings.py

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

1.5.3 ship.py

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

1.6射击

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

1.6.1 添加子弹设置

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

settings.py

def __init__(self): 
        --snip-- 
        # 子弹设置 
        self.bullet_speed = 1.0 
        self.bullet_width = 3 
        self.bullet_height = 15 
        self.bullet_color = (60, 60, 60)
1.6.2创建 Bullet 类

bullet.py

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

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

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

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

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

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

bullet.py

    def update(self):
        """向上移动子弹"""
        self.y-=self.settings.bullet_speed                    #1
        #更新表示子弹的rect的位置
        self.rect.y=self.y                                    #2
    def draw_bullet(self):
        """在屏幕上绘制子弹"""
        pygame.draw.rect(self.screen,self.color,self.rect)    #3

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

属性bullet_speed 让我们能够随着游戏的进行或根据需要提高子弹的速度,以调 整游戏的行为。子弹发射后,其 坐标始终不变,因此子弹将沿直线垂直向上飞 行。 需要绘制子弹时,我们调用draw_bullet() 。draw.rect() 函数使用存储在 self.color 中的颜色填充表示子弹的rect 占据的屏幕部分 (#3) 。

1.6.3将子弹存储到编组中

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

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

alien_invasion.py

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

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

alien_invasion.py

 def run_game(self): 
          """开始游戏主循环。""" 
          while True: 
              self._check_events() 
              self.ship.update() 
              self.bullets.update() #1
              self._update_screen()

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

1.6.4开火

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

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

alien_invasion.py

--snip-- 
  from ship import Ship 
  from bullet import Bullet                             #1
 
  class AlienInvasion: 
      --snip-- 
      def _check_keydown_events(self, event): 
          --snip-- 
          elif event.key == pygame.K_q: 
              sys.exit() 
          elif event.key == pygame.K_SPACE:             #2
              self._fire_bullet() 
 
      def _check_keyup_events(self, event): 
          --snip-- 
 
      def _fire_bullet(self): 
          """创建一颗子弹,并将其加入编组bullets中。""" 
          new_bullet = Bullet(self)                     #3
          self.bullets.add(new_bullet)                  #4
 
      def _update_screen(self): 
          """更新屏幕上的图像,并切换到新屏幕。""" 
          self.screen.fill(self.settings.bg_color) 
          self.ship.blitme() 
          for bullet in self.bullets.sprites():         #5
              bullet.draw_bullet() 
          pygame.display.flip() 
  --snip--
1.6.5删除消失的子弹

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

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

 def run_game(self): 
          """开始游戏主循环。""" 
          while True: 
              self._check_events() 
              self.ship.update() 
              self.bullets.update() 

              #删除消失的子弹。 
              for bullet in self.bullets.copy():  #1
                if bullet.rect.bottom <= 0:       #2
                     self.bullets.remove(bullet)  #3
              print(len(self.bullets))            #4
              self._update_screen()

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

1.6.6限制子弹数量

settings.py

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

这将未消失的子弹数限制为三颗。在AlienInvasion 的_fire_bullet() 中, 在创建新子弹前检查未消失的子弹数是否小于该设置:

alien_invasion.py

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

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

1.6.7 创建方法 _update_bullets()

编写并检查子弹管理代码后,可将其移到一个独立的方法中。

alien_invasion.py

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

alien_invasion.py

while True: 
          self._check_events() 
          self.ship.update() 
          self._update_bullets() 
          self._update_screen()

效果图