python项目笔记

 

 

学习资料:

Python编程:从入门到实践(第2版)

 

 

一、外星人入侵

 

1.1武装飞船

 

1.1.1 规划项目

 

开发大型项目时,制定好规划后再动手编写代码很重要。规划可确保你不偏离轨 道,从而提高项目成功的可能性。
下面来编写有关游戏《外星人入侵》的描述,其中虽然没有涵盖这款游戏的所有细 节,但能让你清楚地知道该如何动手开发。
在游戏《外星人入侵》中,玩家控制一艘最初出现在屏幕底部中央的飞船。玩 家可以使用箭头键左右移动飞船,还可使用空格键射击。游戏开始时,一群外 星人出现在天空中,并向屏幕下方移动。玩家的任务是射杀这些外星人。玩家 将所有外星人都消灭干净后,将出现一群新的外星人,其移动速度更快。只要 有外星人撞到玩家的飞船或到达屏幕底部,玩家就损失一艘飞船。玩家损失三 艘飞船后,游戏结束。
开发的第一个阶段将创建一艘飞船,它可左右移动,并且能在用户按空格键时开 火。设置好这种行为后,就可以创建外星人并提高游戏的可玩性了。

 

1.1.2安装Pygame

C:\Users\yangy>python -m pip install --user pygame
Collecting pygame
  Downloading https://files.pythonhosted.org/packages/05/7a/835c5260cffb50d4b93fbbfe3af3a8760726bc20626bdf97d53469bb29e2/pygame-2.1.1-cp36-cp36m-win_amd64.whl (8.4MB)
    100% |████████████████████████████████| 8.4MB 193kB/s
Installing collected packages: pygame
Successfully installed pygame-2.1.1

 

1.1.3开始游戏项目

 

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

alien_invasion.py

import sys
import pygame
class AlienInvasion:
    """管理游戏资源和行为的类"""
    def __init__(self):
        """初始化游戏并创建游戏资源。"""
        pygame.init()
        self.screen = pygame.display.set_mode((1200, 800))
        pygame.display.set_caption("Alien Invasion")

    def run_game(self):
        """开始游戏的主循环"""
        while True:
            # 监视键盘和鼠标事件。
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    sys.exit()

            # 让最近绘制的屏幕可见。
            pygame.display.flip()
if __name__ == '__main__':
    # 创建游戏实例并运行游戏。
    ai = AlienInvasion()
    ai.run_game()

1.1.3.2设置背景色

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

import sys
import pygame
class AlienInvasion:
    """管理游戏资源和行为的类"""
    def __init__(self):
        """初始化游戏并创建游戏资源。"""
        pygame.init()
        self.screen = pygame.display.set_mode((1200, 800))
        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.bg_color)

            # 让最近绘制的屏幕可见。
            pygame.display.flip()
if __name__ == '__main__':
    # 创建游戏实例并运行游戏。
    ai = AlienInvasion()
    ai.run_game()

1.1.3.3创建设置类

settings.py

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

alien_invasion.py

import pygame

from settings import Settings

class AlienInvasion:
    """管理游戏资源和行为的类"""
    def __init__(self):
        """初始化游戏并创建游戏资源。"""
        pygame.init()
        self.settings = Settings()

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


    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)

            # 让最近绘制的屏幕可见。
            pygame.display.flip()
if __name__ == '__main__':
    # 创建游戏实例并运行游戏。
    ai = AlienInvasion()
    ai.run_game()

1.1.4添加飞船图像

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

1.1.4.1创建Ship 类

ship.py

import pygame
class Ship:
    """管理飞船的类"""
    def __init__(self, ai_game):
        """初始化飞船并设置其初始位置。"""
        self.screen = ai_game.screen
        self.screen_rect = ai_game.screen.get_rect()

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

        # 对于每艘新飞船,都将其放在屏幕底部的中央。
        self.rect.midbottom = self.screen_rect.midbottom
    def blitme(self):
        """在指定位置绘制飞船。"""
        self.screen.blit(self.image, self.rect)

1.1.4.2在屏幕上绘制飞船

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

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

        #导入Ship 类,并在创建屏幕后创建一个Ship 实例
        self.ship = Ship(self)



    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)

            #填充背景后,调用ship.blitme() 将飞船绘制到屏幕上,确保它出现在背景前面
            self.ship.blitme()

            # 让最近绘制的屏幕可见。
            pygame.display.flip()
if __name__ == '__main__':
    # 创建游戏实例并运行游戏。
    ai = AlienInvasion()
    ai.run_game()

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

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

1.1.5.1 方法_check_events()

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

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

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

        #导入Ship 类,并在创建屏幕后创建一个Ship 实例
        self.ship = Ship(self)



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

            #填充背景后,调用ship.blitme() 将飞船绘制到屏幕上,确保它出现在背景前面
            self.ship.blitme()

            # 让最近绘制的屏幕可见。
            pygame.display.flip()

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

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

1.1.5.2 方法_update_screen()

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

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

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

        #导入Ship 类,并在创建屏幕后创建一个Ship 实例
        self.ship = Ship(self)



    def run_game(self):
        """开始游戏的主循环"""
        while True:
            #管理事件的代码
            self._check_events()
            #更新屏幕的代码
            self._update_screen()

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

    def _update_screen(self):
        """更新屏幕上的图像,并切换到新屏幕。"""
        # 每次循环时都重绘屏幕。
        self.screen.fill(self.settings.bg_color)
        # 填充背景后,调用ship.blitme() 将飞船绘制到屏幕上,确保它出现在背景前面
        self.ship.blitme()
        # 让最近绘制的屏幕可见。
        pygame.display.flip()


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

1.1.6 驾驶飞船

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

1.1.6.1 响应按键

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

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

        #导入Ship 类,并在创建屏幕后创建一个Ship 实例
        self.ship = Ship(self)



    def run_game(self):
        """开始游戏的主循环"""
        while True:
            #管理事件的代码
            self._check_events()
            #更新屏幕的代码
            self._update_screen()

    def _check_events(self):
        """响应按键和鼠标事件。"""
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                sys.exit()
            elif event.type == pygame.KEYDOWN:
                if event.key == pygame.K_RIGHT:
                    # 向右移动飞船
                    self.ship.rect.x += 1


    def _update_screen(self):
        """更新屏幕上的图像,并切换到新屏幕。"""
        # 每次循环时都重绘屏幕。
        self.screen.fill(self.settings.bg_color)
        # 填充背景后,调用ship.blitme() 将飞船绘制到屏幕上,确保它出现在背景前面
        self.ship.blitme()
        # 让最近绘制的屏幕可见。
        pygame.display.flip()


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

#已经实现向右移动,但是只能响应一下

1.1.6.2 允许持续移动

玩家按住右箭头键不放时,我们希望飞船不断向右移动,直到玩家松开为止。我们 将让游戏检测pygame.KEYUP 事件,以便知道玩家何时松开右箭头键。然后,结合 使用KEYDOWN 和KEYUP 事件以及一个名为moving_right 的标志来实现持续移 动。当标志moving_right 为False 时,飞船不会移动。玩家按下右箭头键时,我们 将该标志设置为True ,在玩家松开时将该标志重新设置为False 。
飞船的属性都由Ship 类控制,因此要给这个类添加一个名为moving_right 的属 性和一个名为update() 的方法。方法update() 检查标志moving_right 的状 态。如果该标志为True ,就调整飞船的位置。我们将在while 循环中调用这个方 法,以调整飞船的位置。
下面是对Ship 类所做的修改:

ship.py

import pygame
class Ship:
    """管理飞船的类"""
    def __init__(self, ai_game):
        """初始化飞船并设置其初始位置。"""
        self.screen = ai_game.screen
        self.screen_rect = ai_game.screen.get_rect()

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

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

        # 移动标志
        self.moving_right = False

    def update(self):
        """根据移动标志调整飞船的位置。"""
        if self.moving_right:
            self.rect.x += 1

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

 

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

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

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

        #导入Ship 类,并在创建屏幕后创建一个Ship 实例
        self.ship = Ship(self)



    def run_game(self):
        """开始游戏的主循环"""
        while True:
            #管理事件的代码
            self._check_events()
            # 飞船的位置将在检测到键盘事件后(但在更新屏幕前)更新
            self.ship.update()
            #更新屏幕的代码
            self._update_screen()


    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
            elif event.type == pygame.KEYUP:
                if event.key == pygame.K_RIGHT:
                    self.ship.moving_right = False



    def _update_screen(self):
        """更新屏幕上的图像,并切换到新屏幕。"""
        # 每次循环时都重绘屏幕。
        self.screen.fill(self.settings.bg_color)
        # 填充背景后,调用ship.blitme() 将飞船绘制到屏幕上,确保它出现在背景前面
        self.ship.blitme()
        # 让最近绘制的屏幕可见。
        pygame.display.flip()


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

#已实现持续向右移动

1.1.6.3 左右移动

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

ship.py

import pygame
class Ship:
    """管理飞船的类"""
    def __init__(self, ai_game):
        """初始化飞船并设置其初始位置。"""
        self.screen = ai_game.screen
        self.screen_rect = ai_game.screen.get_rect()

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

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

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

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

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

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

 

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

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

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

        #导入Ship 类,并在创建屏幕后创建一个Ship 实例
        self.ship = Ship(self)



    def run_game(self):
        """开始游戏的主循环"""
        while True:
            #管理事件的代码
            self._check_events()
            # 飞船的位置将在检测到键盘事件后(但在更新屏幕前)更新
            self.ship.update()
            #更新屏幕的代码
            self._update_screen()


    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
                elif event.key == pygame.K_LEFT:
                    self.ship.moving_left = True

            elif event.type == pygame.KEYUP:
                if event.key == pygame.K_RIGHT:
                    self.ship.moving_right = False
                elif event.key == pygame.K_LEFT:
                    self.ship.moving_left = False


    def _update_screen(self):
        """更新屏幕上的图像,并切换到新屏幕。"""
        # 每次循环时都重绘屏幕。
        self.screen.fill(self.settings.bg_color)
        # 填充背景后,调用ship.blitme() 将飞船绘制到屏幕上,确保它出现在背景前面
        self.ship.blitme()
        # 让最近绘制的屏幕可见。
        pygame.display.flip()


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

#已实现左右可以持续移动,同时按下左右键,飞船不动。

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

 

1.1.6.4 调整飞船的速度

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

settings.py

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

        # 飞船设置
        self.ship_speed = 1.5

将ship_speed 的初始值设置为1.5 。现在需要移动飞船时,每次循环将移动1.5 像素而不是1像素。
通过将速度设置指定为小数值,可在后面加快游戏节奏时更细致地控制飞船的速 度。然而,rect 的x 等属性只能存储整数值,因此需要对Ship 类做些修改:
ship.py

import pygame
class Ship:
    """管理飞船的类"""
    def __init__(self, ai_game):
        """初始化飞船并设置其初始位置。"""
        self.screen = ai_game.screen
        self.settings = ai_game.settings
        self.screen_rect = ai_game.screen.get_rect()

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

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

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

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

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

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

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

 

1.1.6.5 限制飞船的活动范围

ship.py

import pygame
class Ship:
    """管理飞船的类"""
    def __init__(self, ai_game):
        """初始化飞船并设置其初始位置。"""
        self.screen = ai_game.screen
        self.settings = ai_game.settings
        self.screen_rect = ai_game.screen.get_rect()

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

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

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

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

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

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

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

上述代码在修改self.x 的值之前检查飞船的位置。self.rect.right 返回飞船 外接矩形右边缘的 坐标。如果这个值小于self.screen_rect.right 的值,就 说明飞船未触及屏幕右边缘(见❶)。左边缘的情况与此类似:如果rect 左边缘的 坐标大于零,就说明飞船未触及屏幕左边缘(见❷)。这确保仅当飞船在屏幕内 时,才调整self.x 的值。
如果此时运行alien_invasion.py,飞船将在触及屏幕左边缘或右边缘后停止移动。 真是太神奇了!只在if 语句中添加一个条件测试,就让飞船在到达屏幕左右边缘时 像被墙挡住了一样。

 

1.1.6.6 重构_check_events()

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

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

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

        #导入Ship 类,并在创建屏幕后创建一个Ship 实例
        self.ship = Ship(self)



    def run_game(self):
        """开始游戏的主循环"""
        while True:
            #管理事件的代码
            self._check_events()
            # 飞船的位置将在检测到键盘事件后(但在更新屏幕前)更新
            self.ship.update()
            #更新屏幕的代码
            self._update_screen()


    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


    def _update_screen(self):
        """更新屏幕上的图像,并切换到新屏幕。"""
        # 每次循环时都重绘屏幕。
        self.screen.fill(self.settings.bg_color)
        # 填充背景后,调用ship.blitme() 将飞船绘制到屏幕上,确保它出现在背景前面
        self.ship.blitme()
        # 让最近绘制的屏幕可见。
        pygame.display.flip()


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

 

1.1.6.7 按Q键退出

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

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

        #导入Ship 类,并在创建屏幕后创建一个Ship 实例
        self.ship = Ship(self)



    def run_game(self):
        """开始游戏的主循环"""
        while True:
            #管理事件的代码
            self._check_events()
            # 飞船的位置将在检测到键盘事件后(但在更新屏幕前)更新
            self.ship.update()
            #更新屏幕的代码
            self._update_screen()


    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
        elif event.key == pygame.K_q:
            sys.exit()

    def _check_keyup_events(self, event):
        """响应松开"""
        if event.key == pygame.K_RIGHT:
            self.ship.moving_right = False
        elif event.key == pygame.K_LEFT:
            self.ship.moving_left = False


    def _update_screen(self):
        """更新屏幕上的图像,并切换到新屏幕。"""
        # 每次循环时都重绘屏幕。
        self.screen.fill(self.settings.bg_color)
        # 填充背景后,调用ship.blitme() 将飞船绘制到屏幕上,确保它出现在背景前面
        self.ship.blitme()
        # 让最近绘制的屏幕可见。
        pygame.display.flip()


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

 

 

1.1.6.8 在全屏模式下运行游戏

lien_invasion.py

import sys
import pygame

from settings import Settings
from ship import Ship

class AlienInvasion:
    """管理游戏资源和行为的类"""
    def __init__(self):
        """初始化游戏并创建游戏资源。"""
        pygame.init()
        self.settings = Settings()

        self.screen = pygame.display.set_mode((0, 0), pygame.FULLSCREEN)
        self.settings.screen_width = self.screen.get_rect().width
        self.settings.screen_height = self.screen.get_rect().height
        pygame.display.set_caption("Alien Invasion")

        #导入Ship 类,并在创建屏幕后创建一个Ship 实例
        self.ship = Ship(self)

    def run_game(self):
        """开始游戏的主循环"""
        while True:
            #管理事件的代码
            self._check_events()
            # 飞船的位置将在检测到键盘事件后(但在更新屏幕前)更新
            self.ship.update()
            #更新屏幕的代码
            self._update_screen()


    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
        elif event.key == pygame.K_q:
            sys.exit()

    def _check_keyup_events(self, event):
        """响应松开"""
        if event.key == pygame.K_RIGHT:
            self.ship.moving_right = False
        elif event.key == pygame.K_LEFT:
            self.ship.moving_left = False


    def _update_screen(self):
        """更新屏幕上的图像,并切换到新屏幕。"""
        # 每次循环时都重绘屏幕。
        self.screen.fill(self.settings.bg_color)
        # 填充背景后,调用ship.blitme() 将飞船绘制到屏幕上,确保它出现在背景前面
        self.ship.blitme()
        # 让最近绘制的屏幕可见。
        pygame.display.flip()


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

 

1.1.7 简单回顾

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)包含的代码会被导入这个文件中。

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

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

 

1.1.8 射击

1.1.8.1 添加子弹设置

settings.py

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

        # 飞船设置
        self.ship_speed = 1.5

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

 

1.1.8.2 创建Bullet 类

import pygame
from pygame.sprite import Sprite

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

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

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

Bullet 类继承了从模块pygame.sprite 导入的Sprite 类。通过使用精灵 (sprite),可将游戏中相关的元素编组,进而同时操作编组中的所有元素。为创 建子弹实例,__init__() 需要当前的AlienInvasion 实例,我们还调用了 super() 来继承Sprite 。另外,我们还定义了用于存储屏幕以及设置对象和子弹 颜色的属性。
在❶处,创建子弹的属性rect 。子弹并非基于图像,因此必须使用 pygame.Rect() 类从头开始创建一个矩形。创建这个类的实例时,必须提供矩形 左上角的 坐标和 坐标,以及矩形的宽度和高度。我们在(0, 0)处创建这个矩 形,但下一行代码将其移到了正确的位置,因为子弹的初始位置取决于飞船当前的 位置。子弹的宽度和高度是从self.settings 中获取的。
在❷处,将子弹的rect.midtop 设置为飞船的rect.midtop 。这样子弹将从飞 船顶部出发,看起来像是从飞船中射出的。我们将子弹的 坐标存储为小数值,以 便能够微调子弹的速度(见❸)。
下面是bullet.py的第二部分,包括方法update() 和draw_bullet() :

bullet.py

import pygame
from pygame.sprite import Sprite

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

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

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

    def update(self):
        """向上移动子弹。"""
        # 更新表示子弹位置的小数值。
        self.y -= self.settings.bullet_speed
        # 更新表示子弹的rect的位置。
        self.rect.y = self.y

    def draw_bullet(self):
        """在屏幕上绘制子弹。"""
        pygame.draw.rect(self.screen, self.color, self.rect)

方法update() 管理子弹的位置。发射出去后,子弹向上移动,意味着其 坐标将 不断减小。为更新子弹的位置,从self.y 中减去settings .bullet_speed 的 值(见❶)。接下来,将self.rect.y 设置为self.y 的值(见❷)。
属性bullet_speed 让我们能够随着游戏的进行或根据需要提高子弹的速度,以调 整游戏的行为。子弹发射后,其 坐标始终不变,因此子弹将沿直线垂直向上飞 行。
需要绘制子弹时,我们调用draw_bullet() 。draw.rect() 函数使用存储在 self.color 中的颜色填充表示子弹的rect 占据的屏幕部分(见❸)。

 

 

1.1.8.3 将子弹存储到编组中

 

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

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

        self.screen = pygame.display.set_mode((0, 0), pygame.FULLSCREEN)
        self.settings.screen_width = self.screen.get_rect().width
        self.settings.screen_height = self.screen.get_rect().height
        pygame.display.set_caption("Alien Invasion")

        #导入Ship 类,并在创建屏幕后创建一个Ship 实例
        self.ship = Ship(self)

        # 创建用于存储子弹的编组
        self.ship = Ship(self)
        self.bullets = pygame.sprite.Group()

    def run_game(self):
        """开始游戏的主循环"""
        while True:
            #管理事件的代码
            self._check_events()
            # 飞船的位置将在检测到键盘事件后(但在更新屏幕前)更新
            self.ship.update()
            # 在while 循环中更新子弹的位置
            self.bullets.update()
            #更新屏幕的代码
            self._update_screen()


    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
        elif event.key == pygame.K_q:
            sys.exit()

    def _check_keyup_events(self, event):
        """响应松开"""
        if event.key == pygame.K_RIGHT:
            self.ship.moving_right = False
        elif event.key == pygame.K_LEFT:
            self.ship.moving_left = False


    def _update_screen(self):
        """更新屏幕上的图像,并切换到新屏幕。"""
        # 每次循环时都重绘屏幕。
        self.screen.fill(self.settings.bg_color)
        # 填充背景后,调用ship.blitme() 将飞船绘制到屏幕上,确保它出现在背景前面
        self.ship.blitme()
        # 让最近绘制的屏幕可见。
        pygame.display.flip()


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

 

1.1.8.4 开火

在AlienInvasion 中,需要修改_check_keydown_events() ,以便在玩家按 空格键时发射一颗子弹。无须修改_check_keyup_events() ,因为玩家松开空 格键时什么都不会发生。还需要修改_update_screen() ,确保在调用flip() 前在屏幕上重绘每颗子弹。
为发射子弹,需要做的工作不少,因此编写一个新方法_fire_bullet() 来完成 这项任务:

alien_invasion.py

import sys
import pygame

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

class AlienInvasion:
    """管理游戏资源和行为的类"""
    def __init__(self):
        """初始化游戏并创建游戏资源。"""
        pygame.init()
        self.settings = Settings()

        self.screen = pygame.display.set_mode((0, 0), pygame.FULLSCREEN)
        self.settings.screen_width = self.screen.get_rect().width
        self.settings.screen_height = self.screen.get_rect().height
        pygame.display.set_caption("Alien Invasion")

        #导入Ship 类,并在创建屏幕后创建一个Ship 实例
        self.ship = Ship(self)

        # 创建用于存储子弹的编组
        self.ship = Ship(self)
        self.bullets = pygame.sprite.Group()

    def run_game(self):
        """开始游戏的主循环"""
        while True:
            #管理事件的代码
            self._check_events()
            # 飞船的位置将在检测到键盘事件后(但在更新屏幕前)更新
            self.ship.update()
            # 在while 循环中更新子弹的位置
            self.bullets.update()
            #更新屏幕的代码
            self._update_screen()


    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
        elif event.key == pygame.K_q:
            sys.exit()
        elif event.key == pygame.K_SPACE:
            self._fire_bullet()

    def _check_keyup_events(self, event):
        """响应松开"""
        if event.key == pygame.K_RIGHT:
            self.ship.moving_right = False
        elif event.key == pygame.K_LEFT:
            self.ship.moving_left = False

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


    def _update_screen(self):
        """更新屏幕上的图像,并切换到新屏幕。"""
        # 每次循环时都重绘屏幕。
        self.screen.fill(self.settings.bg_color)
        # 填充背景后,调用ship.blitme() 将飞船绘制到屏幕上,确保它出现在背景前面
        self.ship.blitme()

        for bullet in self.bullets.sprites():
            bullet.draw_bullet()

        # 让最近绘制的屏幕可见。
        pygame.display.flip()


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

首先导入Bullet 类(见❶),再在玩家按空格键时调用_fire_bullet() (见 ❷)。在_fire_bullet() 中,创建一个Bullet 实例并将其赋给new_bullet (见❸),再使用方法add() 将其加入编组bullets 中(见❹)。方法add() 类 似于append() ,不过是专门为Pygame编组编写的。
方法bullets.sprites() 返回一个列表,其中包含编组bullets 中的所有精 灵。为在屏幕上绘制发射的所有子弹,遍历编组bullets 中的精灵,并对每个精灵 调用draw_bullet() (见❺)。
如果此时运行alien_invasion.py,将能够左右移动飞船,并发射任意数量的子弹。 子弹在屏幕上向上飞行,抵达屏幕顶部后消失得无影无踪,如图12-3所示。你可在 settings.py中修改子弹的尺寸、颜色和速度。

1.1.8.5 删除消失的子弹

当前,子弹在抵达屏幕顶端后消失,但这仅仅是因为Pygame无法在屏幕外面绘制它 们。这些子弹实际上依然存在,其 坐标为负数且越来越小。这是个问题,因为它 们将继续消耗内存和处理能力。
需要将这些消失的子弹删除,否则游戏所做的无谓工作将越来越多,进而变得越来 越慢。为此,需要检测表示子弹的rect 的bottom 属性是否为零。如果是,则表 明子弹已飞过屏幕顶端:


alien_invasion.py

import sys
import pygame

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

class AlienInvasion:
    """管理游戏资源和行为的类"""
    def __init__(self):
        """初始化游戏并创建游戏资源。"""
        pygame.init()
        self.settings = Settings()

        self.screen = pygame.display.set_mode((0, 0), pygame.FULLSCREEN)
        self.settings.screen_width = self.screen.get_rect().width
        self.settings.screen_height = self.screen.get_rect().height
        pygame.display.set_caption("Alien Invasion")

        #导入Ship 类,并在创建屏幕后创建一个Ship 实例
        self.ship = Ship(self)

        # 创建用于存储子弹的编组
        self.ship = Ship(self)
        self.bullets = pygame.sprite.Group()

    def run_game(self):
        """开始游戏的主循环"""
        while True:
            #管理事件的代码
            self._check_events()
            # 飞船的位置将在检测到键盘事件后(但在更新屏幕前)更新
            self.ship.update()
            # 在while 循环中更新子弹的位置
            self.bullets.update()

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

            #更新屏幕的代码
            self._update_screen()


    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
        elif event.key == pygame.K_q:
            sys.exit()
        elif event.key == pygame.K_SPACE:
            self._fire_bullet()

    def _check_keyup_events(self, event):
        """响应松开"""
        if event.key == pygame.K_RIGHT:
            self.ship.moving_right = False
        elif event.key == pygame.K_LEFT:
            self.ship.moving_left = False

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


    def _update_screen(self):
        """更新屏幕上的图像,并切换到新屏幕。"""
        # 每次循环时都重绘屏幕。
        self.screen.fill(self.settings.bg_color)
        # 填充背景后,调用ship.blitme() 将飞船绘制到屏幕上,确保它出现在背景前面
        self.ship.blitme()

        for bullet in self.bullets.sprites():
            bullet.draw_bullet()

        # 让最近绘制的屏幕可见。
        pygame.display.flip()


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

使用for 循环遍历列表(或Pygame编组)时,Python要求该列表的长度在整个循环 中保持不变。因为不能从for 循环遍历的列表或编组中删除元素,所以必须遍历编 组的副本。我们使用方法copy() 来设置for 循环(见❶),从而能够在循环中修 改bullets 。我们检查每颗子弹,看看它是否从屏幕顶端消失(见❷)。如果是, 就将其从bullets 中删除(见❸)。在❹处,使用函数调用print() 显示当前还 有多少颗子弹,以核实确实删除了消失的子弹。
如果这些代码没有问题,我们发射子弹后查看终端窗口时,将发现随着子弹一颗颗 地在屏幕顶端消失,子弹数将逐渐降为零。运行该游戏并确认子弹被正确删除后, 请将这个函数调用print() 删除。如果不删除,游戏的速度将大大降低,因为将输 出写入终端花费的时间比将图形绘制到游戏窗口花费的时间还要多。

 

1.1.8.6 限制子弹数量

 

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

settings.py

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

        # 飞船设置
        self.ship_speed = 1.5

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

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

alien_invasion.py

import sys
import pygame

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

class AlienInvasion:
    """管理游戏资源和行为的类"""
    def __init__(self):
        """初始化游戏并创建游戏资源。"""
        pygame.init()
        self.settings = Settings()

        self.screen = pygame.display.set_mode((0, 0), pygame.FULLSCREEN)
        self.settings.screen_width = self.screen.get_rect().width
        self.settings.screen_height = self.screen.get_rect().height
        pygame.display.set_caption("Alien Invasion")

        #导入Ship 类,并在创建屏幕后创建一个Ship 实例
        self.ship = Ship(self)

        # 创建用于存储子弹的编组
        self.ship = Ship(self)
        self.bullets = pygame.sprite.Group()

    def run_game(self):
        """开始游戏的主循环"""
        while True:
            #管理事件的代码
            self._check_events()
            # 飞船的位置将在检测到键盘事件后(但在更新屏幕前)更新
            self.ship.update()
            # 在while 循环中更新子弹的位置
            self.bullets.update()

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

            #更新屏幕的代码
            self._update_screen()


    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
        elif event.key == pygame.K_q:
            sys.exit()
        elif event.key == pygame.K_SPACE:
            self._fire_bullet()

    def _check_keyup_events(self, event):
        """响应松开"""
        if event.key == pygame.K_RIGHT:
            self.ship.moving_right = False
        elif event.key == pygame.K_LEFT:
            self.ship.moving_left = False

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

    def _update_screen(self):
        """更新屏幕上的图像,并切换到新屏幕。"""
        # 每次循环时都重绘屏幕。
        self.screen.fill(self.settings.bg_color)
        # 填充背景后,调用ship.blitme() 将飞船绘制到屏幕上,确保它出现在背景前面
        self.ship.blitme()

        for bullet in self.bullets.sprites():
            bullet.draw_bullet()

        # 让最近绘制的屏幕可见。
        pygame.display.flip()


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

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

 

1.1.8.7 创建方法_update_bullets()

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

alien_invasion.py

import sys
import pygame

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

class AlienInvasion:
    """管理游戏资源和行为的类"""
    def __init__(self):
        """初始化游戏并创建游戏资源。"""
        pygame.init()
        self.settings = Settings()

        self.screen = pygame.display.set_mode((0, 0), pygame.FULLSCREEN)
        self.settings.screen_width = self.screen.get_rect().width
        self.settings.screen_height = self.screen.get_rect().height
        pygame.display.set_caption("Alien Invasion")

        #导入Ship 类,并在创建屏幕后创建一个Ship 实例
        self.ship = Ship(self)

        # 创建用于存储子弹的编组
        self.ship = Ship(self)
        self.bullets = pygame.sprite.Group()

    def run_game(self):
        """开始游戏的主循环"""
        while True:
            #管理事件的代码
            self._check_events()
            # 飞船的位置将在检测到键盘事件后(但在更新屏幕前)更新
            self.ship.update()
            # 在while 循环中更新子弹的位置
            self._update.bullets()
            #更新屏幕的代码
            self._update_screen()


    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
        elif event.key == pygame.K_q:
            sys.exit()
        elif event.key == pygame.K_SPACE:
            self._fire_bullet()

    def _check_keyup_events(self, event):
        """响应松开"""
        if event.key == pygame.K_RIGHT:
            self.ship.moving_right = False
        elif event.key == pygame.K_LEFT:
            self.ship.moving_left = False

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

    def _update_bullets(self):
        """更新子弹的位置并删除消失的子弹。"""
        # 更新子弹的位置。
        self.bullets.update()

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


    def _update_screen(self):
        """更新屏幕上的图像,并切换到新屏幕。"""
        # 每次循环时都重绘屏幕。
        self.screen.fill(self.settings.bg_color)
        # 填充背景后,调用ship.blitme() 将飞船绘制到屏幕上,确保它出现在背景前面
        self.ship.blitme()

        for bullet in self.bullets.sprites():
            bullet.draw_bullet()

        # 让最近绘制的屏幕可见。
        pygame.display.flip()


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

 

1.2 外星人来了

本章将在游戏《外星人入侵》中添加外星人。我们将首先在屏 幕上边缘附近添加一个外星人,再生成一群外星人。然后让这群外星人向两边 和下面移动,并删除被子弹击中的外星人。最后,显示玩家拥有的飞船数量, 并在玩家的飞船用完后结束游戏。
通过阅读本章,你将更深入地了解Pygame和大型项目管理,还将学习如何检测 游戏对象之间的碰撞,如子弹和外星人之间的碰撞。检测碰撞有助于定义游戏 元素之间的交互。例如,可以将角色限定在迷宫墙壁之内,或者在两个角色之 间传球。我们将不时查看游戏开发计划,确保编程工作不偏离轨道。
着手编写在屏幕上添加一群外星人的代码前,先来回顾一下这个项目,并更新 开发计划。

1.2.1 项目回顾

开发大型项目时,要在进入每个开发阶段之前回顾一下开发计划,搞清楚接下来要 通过编写代码来完成哪些任务。本章涉及以下内容。
研究既有代码,确定实现新功能前是否要重构。 在屏幕左上角添加一个外星人,并指定合适的边距。
根据第一个外星人的边距和屏幕尺寸计算屏幕上可容纳多少个外星人。编写一 个循环来创建一系列外星人,使其填满屏幕的上半部分。 让外星人群向两边和下方移动,直到外星人被全部击落、有外星人撞到飞船或 有外星人抵达屏幕底端。如果整群外星人都被击落,将再创建一群外星人。如 果有外星人撞到了飞船或抵达屏幕底端,将销毁飞船并再创建一群外星人。 限制玩家可用的飞船数量。当配给的飞船用完之后,游戏将结束。
我们将在实现功能的同时完善这个计划,但就目前而言,该计划已足够详尽。
在项目中添加新功能前,还应审核既有代码。每进入一个新阶段,项目通常会更复 杂,因此最好对混乱或低效的代码进行清理。我们一直在不断重构,因此当前没有 需要重构的代码。

 

1.2.2 创建第一个外星人

 

1.2.2.1 创建Alien 类

import pygame
from pygame.sprite import Sprite

class Alien(Sprite):
    """表示单个外星人的类。"""
    def __init__(self, ai_game):
        """初始化外星人并设置其起始位置。"""
        super().__init__()
        self.screen = ai_game.screen

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

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

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

除位置不同外,这个类的大部分代码与Ship 类相似。每个外星人最初都位于屏幕 左上角附近。将每个外星人的左边距都设置为外星人的宽度,并将上边距设置为外 星人的高度(见❶),这样更容易看清。我们主要关心的是外星人的水平速度,因此 精确地记录了每个外星人的水平位置(见❷)。
Alien 类不需要一个在屏幕上绘制外星人的方法,因为我们将使用一个Pygame编组 方法,自动在屏幕上绘制编组中的所有元素。

 

1.2.2.2 创建Alien 实例

要让第一个外星人在屏幕上现身,需要创建一个Alien 实例。这属于设置工作,因 此将把这些代码放在AlienInvasion 类的方法__init__() 末尾。我们最终会创 建一群外星人,涉及的工作量不少,因此将新建一个名为_create_fleet() 的辅 助方法。
在类中,方法的排列顺序无关紧要,只要按统一的标准排列就行。我们将把 _create_fleet() 放在_update_screen() 前面,不过放在AlienInvasion 类的任何地方其实都可行。首先,需要导入Alien 类。
下面是alien_invasion.py中修改后的import 语句:

alien_invasion.py

 

----
from alien import Alien

下面是修改后的方法__init__() :

        # 创建了一个用于存储外星人群的编组,还调用了接下来将编写的方法
        self.aliens = pygame.sprite.Group()
        self._create_fleet()

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

新编写的方法_create_fleet() :

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

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

import sys
import pygame

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

class AlienInvasion:
    """管理游戏资源和行为的类"""
    def __init__(self):
        """初始化游戏并创建游戏资源。"""
        pygame.init()
        self.settings = Settings()

        self.screen = pygame.display.set_mode((0, 0), pygame.FULLSCREEN)
        self.settings.screen_width = self.screen.get_rect().width
        self.settings.screen_height = self.screen.get_rect().height
        pygame.display.set_caption("Alien Invasion")

         # 创建用于存储子弹的编组
        self.ship = Ship(self)
        self.bullets = pygame.sprite.Group()

        # 创建了一个用于存储外星人群的编组,还调用了接下来将编写的方法
        self.aliens = pygame.sprite.Group()
        self._create_fleet()

    def run_game(self):
        """开始游戏的主循环"""
        while True:
            #管理事件的代码
            self._check_events()
            # 飞船的位置将在检测到键盘事件后(但在更新屏幕前)更新
            self.ship.update()
            # 在while 循环中更新子弹的位置
            self._update_bullets()
            #更新屏幕的代码
            self._update_screen()


    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
        elif event.key == pygame.K_q:
            sys.exit()
        elif event.key == pygame.K_SPACE:
            self._fire_bullet()

    def _check_keyup_events(self, event):
        """响应松开"""
        if event.key == pygame.K_RIGHT:
            self.ship.moving_right = False
        elif event.key == pygame.K_LEFT:
            self.ship.moving_left = False

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

    def _update_bullets(self):
        """更新子弹的位置并删除消失的子弹。"""
        # 更新子弹的位置。
        self.bullets.update()

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


    def _update_screen(self):
        """更新屏幕上的图像,并切换到新屏幕。"""
        # 每次循环时都重绘屏幕。
        self.screen.fill(self.settings.bg_color)
        # 填充背景后,调用ship.blitme() 将飞船绘制到屏幕上,确保它出现在背景前面
        self.ship.blitme()
  

     # 要让外星人现身,需要在_update_screen() 中对外星人编组调用方法draw()
for bullet in self.bullets.sprites(): bullet.draw_bullet() self.aliens.draw(self.screen) # 让最近绘制的屏幕可见。 pygame.display.flip() def _create_fleet(self): """创建外星人群。""" # 创建一个外星人。 alien = Alien(self) self.aliens.add(alien) if __name__ == '__main__': # 创建游戏实例并运行游戏。 ai = AlienInvasion() ai.run_game()

 

1.2.3 创建一群外星人

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

 

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

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

available_space_x = settings.screen_width – (2 * alien_width)

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

number_aliens_x = available_space_x // (2 * alien_width)

我们将在创建外星人群时使用这些公式。
注意 令人欣慰的是,在程序中执行计算时,无须在一开始确定公式是正确 的,而是可以尝试运行程序,看看结果是否符合预期。即便是在最坏的情况 下,也只是屏幕上显示的外星人太多或太少。随后可根据在屏幕上看到的情况 调整计算公式。

 

1.2.3.2 创建一行外星人

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

import sys
import pygame

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

class AlienInvasion:
    """管理游戏资源和行为的类"""
    def __init__(self):
        """初始化游戏并创建游戏资源。"""
        pygame.init()
        self.settings = Settings()

        self.screen = pygame.display.set_mode((0, 0), pygame.FULLSCREEN)
        self.settings.screen_width = self.screen.get_rect().width
        self.settings.screen_height = self.screen.get_rect().height
        pygame.display.set_caption("Alien Invasion")

         # 创建用于存储子弹的编组
        self.ship = Ship(self)
        self.bullets = pygame.sprite.Group()

        # 创建了一个用于存储外星人群的编组,还调用了接下来将编写的方法
        self.aliens = pygame.sprite.Group()
        self._create_fleet()

    def run_game(self):
        """开始游戏的主循环"""
        while True:
            #管理事件的代码
            self._check_events()
            # 飞船的位置将在检测到键盘事件后(但在更新屏幕前)更新
            self.ship.update()
            # 在while 循环中更新子弹的位置
            self._update_bullets()
            #更新屏幕的代码
            self._update_screen()


    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
        elif event.key == pygame.K_q:
            sys.exit()
        elif event.key == pygame.K_SPACE:
            self._fire_bullet()

    def _check_keyup_events(self, event):
        """响应松开"""
        if event.key == pygame.K_RIGHT:
            self.ship.moving_right = False
        elif event.key == pygame.K_LEFT:
            self.ship.moving_left = False

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

    def _update_bullets(self):
        """更新子弹的位置并删除消失的子弹。"""
        # 更新子弹的位置。
        self.bullets.update()

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


    def _update_screen(self):
        """更新屏幕上的图像,并切换到新屏幕。"""
        # 每次循环时都重绘屏幕。
        self.screen.fill(self.settings.bg_color)
        # 填充背景后,调用ship.blitme() 将飞船绘制到屏幕上,确保它出现在背景前面
        self.ship.blitme()

        #要让外星人现身,需要在_update_screen() 中对外星人编组调用方法draw()
        for bullet in self.bullets.sprites():
            bullet.draw_bullet()
        self.aliens.draw(self.screen)

        # 让最近绘制的屏幕可见。
        pygame.display.flip()

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

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

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

 

这些代码大多在前面详细介绍过。为放置外星人,需要知道外星人的宽度和高度, 因此在执行计算前,创建一个外星人(见❶)。这个外星人不是外星人群的成员,因 此没有将其加入编组aliens 中。在❷处,从外星人的rect 属性中获取外星人宽 度,并将这个值存储到alien_width 中,以免反复访问属性rect 。在❸处,计算 可用于放置外星人的水平空间以及其中可容纳多少个外星人。
接下来,编写一个循环,从零数到要创建的外星人数(见❹)。在这个循环中,创建 一个新的外星人,并通过设置 坐标将其加入当前行(见❺)。将每个外星人都往右 推一个外星人宽度。接下来,将外星人宽度乘以2,得到每个外星人占据的空间(其 中包括右边的空白区域),再据此计算当前外星人在当前行的位置。我们使用外星 人的属性x 来设置其rect 的位置。最后,将每个新创建的外星人都添加到编组 aliens 中。

 

1.2.3.3 重构_create_fleet()

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

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

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

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

1.2.3.4 添加行

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

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

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

number_rows = available_space_y // (2 * alien_height)

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

alien_invasion.py

import sys
import pygame

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

class AlienInvasion:
    """管理游戏资源和行为的类"""
    def __init__(self):
        """初始化游戏并创建游戏资源。"""
        pygame.init()
        self.settings = Settings()

        self.screen = pygame.display.set_mode((0, 0), pygame.FULLSCREEN)
        self.settings.screen_width = self.screen.get_rect().width
        self.settings.screen_height = self.screen.get_rect().height
        pygame.display.set_caption("Alien Invasion")

         # 创建用于存储子弹的编组
        self.ship = Ship(self)
        self.bullets = pygame.sprite.Group()

        # 创建了一个用于存储外星人群的编组,还调用了接下来将编写的方法
        self.aliens = pygame.sprite.Group()
        self._create_fleet()

    def run_game(self):
        """开始游戏的主循环"""
        while True:
            #管理事件的代码
            self._check_events()
            # 飞船的位置将在检测到键盘事件后(但在更新屏幕前)更新
            self.ship.update()
            # 在while 循环中更新子弹的位置
            self._update_bullets()
            #更新屏幕的代码
            self._update_screen()


    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
        elif event.key == pygame.K_q:
            sys.exit()
        elif event.key == pygame.K_SPACE:
            self._fire_bullet()

    def _check_keyup_events(self, event):
        """响应松开"""
        if event.key == pygame.K_RIGHT:
            self.ship.moving_right = False
        elif event.key == pygame.K_LEFT:
            self.ship.moving_left = False

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

    def _update_bullets(self):
        """更新子弹的位置并删除消失的子弹。"""
        # 更新子弹的位置。
        self.bullets.update()

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


    def _update_screen(self):
        """更新屏幕上的图像,并切换到新屏幕。"""
        # 每次循环时都重绘屏幕。
        self.screen.fill(self.settings.bg_color)
        # 填充背景后,调用ship.blitme() 将飞船绘制到屏幕上,确保它出现在背景前面
        self.ship.blitme()

        #要让外星人现身,需要在_update_screen() 中对外星人编组调用方法draw()
        for bullet in self.bullets.sprites():
            bullet.draw_bullet()
        self.aliens.draw(self.screen)

        # 让最近绘制的屏幕可见。
        pygame.display.flip()

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

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

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

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

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

 

1.2.4 让外星人群移动

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

 

1.2.4. 1 向右移动外星人群

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

settings.py

----
        # 外星人设置
        self.alien_speed = 1.0

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

alien.py

import pygame
from pygame.sprite import Sprite

class Alien(Sprite):
    """表示单个外星人的类。"""
    def __init__(self, ai_game):
        """初始化外星人并设置其起始位置。"""
        super().__init__()
        self.screen = ai_game.screen
        self.settings = ai_game.settings

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

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

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

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

在__init__() 中添加了属性settings ,以便能够在update() 中访问外星人 的速度。每次更新外星人时,都将它向右移动,移动量为alien_speed 的值。我 们使用属性self.x 跟踪每个外星人的准确位置,该属性可存储小数值(见❶)。 然后,使用self.x 的值来更新外星人的rect 的位置(见❷)。
主while 循环中已调用了更新飞船和子弹的方法,现在还需更调用更新每个外星人 位置的方法:

alien_invasion.py

----
    def run_game(self):
        """开始游戏的主循环"""
        while True:
            #管理事件的代码
            self._check_events()
            # 飞船的位置将在检测到键盘事件后(但在更新屏幕前)更新
            self.ship.update()
            # 在while 循环中更新子弹的位置
            self._update_bullets()
            #更新每个外星人 位置的方法
            self._update_aliens()
            #更新屏幕的代码
            self._update_screen()
----

需要编写一些代码来管理外星人群的移动,因此新建一个名为_update_aliens() 的方法。我们在更新子弹后再更新外星人的位置,因为稍后要检查是否有子弹击中 了外星人。
将这个方法放在模块的什么地方都无关紧要,但为确保代码组织有序,我将它放在 方法_update_bullets() 的后面,以便与while 循环中的调用顺序一致。下面 是_update_aliens() 的第一版:
alien_invasion.py

----
    def _update_aliens(self):
        """更新外星人群中所有外星人的位置。"""
        self.aliens.update()
----

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

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

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

settings.py

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

设置fleet_drop_speed 指定有外星人撞到屏幕边缘时,外星人群向下移动的速 度。将这个速度与水平速度分开是有好处的,便于分别调整这两个速度。
要实现设置fleet_direction ,可将其设置为文本值,如'left' 或'right' ,但这样就必须编写if-elif 语句来检查外星人群的移动方向。鉴于只有两个可能 的方向,我们使用值1和-1来表示,并在外星人群改变方向时在这两个值之间切换。 (向右移时需要增大每个外星人的 坐标,而向左移时需要减小每个外星人的 坐 标,因此使用数字来表示方向十分合理。

 

1.2.4. 3 检查外星人是否撞到了屏幕边缘

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

alien.py

 

import pygame
from pygame.sprite import Sprite

class Alien(Sprite):
    """表示单个外星人的类。"""
    def __init__(self, ai_game):
        """初始化外星人并设置其起始位置。"""
        super().__init__()
        self.screen = ai_game.screen
        self.settings = ai_game.settings

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

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

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

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


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

#现在还不能避免撞到边缘,只是可以检测出来

 

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

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

alien_invasion.py

import sys
import pygame

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


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

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

        self.screen = pygame.display.set_mode((0, 0), pygame.FULLSCREEN)
        self.settings.screen_width = self.screen.get_rect().width
        self.settings.screen_height = self.screen.get_rect().height
        pygame.display.set_caption("Alien Invasion")

         # 创建用于存储子弹的编组
        self.ship = Ship(self)
        self.bullets = pygame.sprite.Group()

        # 创建了一个用于存储外星人群的编组,还调用了接下来将编写的方法
        self.aliens = pygame.sprite.Group()
        self._create_fleet()

    def run_game(self):
        """开始游戏的主循环"""
        while True:
            #管理事件的代码
            self._check_events()
            # 飞船的位置将在检测到键盘事件后(但在更新屏幕前)更新
            self.ship.update()
            # 在while 循环中更新子弹的位置
            self._update_bullets()
            #更新每个外星人 位置的方法
            self._update_aliens()
            #更新屏幕的代码
            self._update_screen()


    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
        elif event.key == pygame.K_q:
            sys.exit()
        elif event.key == pygame.K_SPACE:
            self._fire_bullet()

    def _check_keyup_events(self, event):
        """响应松开"""
        if event.key == pygame.K_RIGHT:
            self.ship.moving_right = False
        elif event.key == pygame.K_LEFT:
            self.ship.moving_left = False

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

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

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



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

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

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

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

    def _check_fleet_edges(self):
        """Respond appropriately if any aliens have reached an edge."""
        for alien in self.aliens.sprites():
            if alien.check_edges():
                self._change_fleet_direction()
                break

    def _change_fleet_direction(self):
        """Drop the entire fleet and change the fleet's direction."""
        for alien in self.aliens.sprites():
            alien.rect.y += self.settings.fleet_drop_speed
        self.settings.fleet_direction *= -1


    def _update_screen(self):
        """更新屏幕上的图像,并切换到新屏幕。"""
        # 每次循环时都重绘屏幕。
        self.screen.fill(self.settings.bg_color)
        # 填充背景后,调用ship.blitme() 将飞船绘制到屏幕上,确保它出现在背景前面
        self.ship.blitme()

        #要让外星人现身,需要在_update_screen() 中对外星人编组调用方法draw()
        for bullet in self.bullets.sprites():
            bullet.draw_bullet()
        self.aliens.draw(self.screen)

        # 让最近绘制的屏幕可见。
        pygame.display.flip()


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

在_check_fleet_edges() 中,遍历外星人群并对其中的每个外星人调用 check_edges() (见❶)。如果check_edges() 返回True ,就表明相应的外 星人位于屏幕边缘,需要改变外星人群的方向,因此调用 _change_fleet_direction() 并退出循环(见❷)。在 _change_fleet_direction() 中,遍历所有外星人,将每个外星人下移设置 fleet_drop_speed 的值(见❸)。然后,将fleet_direction 的值改为其当 前值与-1的乘积。调整外星人群移动方向的代码行没有包含在for 循环中,因为我 们要调整每个外星人的垂直位置,但只想调整外星人群移动方向一次。

我们将方法_update_aliens() 修改成了先调用_check_fleet_edges() ,再 更新每个外星人的位置。

如果现在运行这个游戏,外星人群将在屏幕上来回移动,并在抵达屏幕边缘后向下 移动。现在可以开始射杀外星人,并检查是否有外星人撞到飞船或抵达了屏幕底 端。

1.2.5 射杀外星人

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

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

子弹击中外星人时,我们需要马上知道,以便碰撞发生后让子弹立即消失。为此, 我们将在更新子弹的位置后立即检测碰撞。
函数sprite.groupcollide() 将一个编组中每个元素的rect 同另一个编组中 每个元素的rect 进行比较。在这里,是将每颗子弹的rect 同每个外星人的rect 进行比较,并返回一个字典,其中包含发生了碰撞的子弹和外星人。在这个字典 中,每个键都是一颗子弹,而关联的值是被该子弹击中的外星人(第14章实现记分 系统时,也将使用该字典)。
在方法_update_bullets() 末尾,添加如下检查子弹和外星人碰撞的代码:

alien_invasion.py

import sys
import pygame

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


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

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

        self.screen = pygame.display.set_mode((0, 0), pygame.FULLSCREEN)
        self.settings.screen_width = self.screen.get_rect().width
        self.settings.screen_height = self.screen.get_rect().height
        pygame.display.set_caption("Alien Invasion")

         # 创建用于存储子弹的编组
        self.ship = Ship(self)
        self.bullets = pygame.sprite.Group()

        # 创建了一个用于存储外星人群的编组,还调用了接下来将编写的方法
        self.aliens = pygame.sprite.Group()
        self._create_fleet()

    def run_game(self):
        """开始游戏的主循环"""
        while True:
            #管理事件的代码
            self._check_events()
            # 飞船的位置将在检测到键盘事件后(但在更新屏幕前)更新
            self.ship.update()
            # 在while 循环中更新子弹的位置
            self._update_bullets()
            #更新每个外星人 位置的方法
            self._update_aliens()
            #更新屏幕的代码
            self._update_screen()


    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
        elif event.key == pygame.K_q:
            sys.exit()
        elif event.key == pygame.K_SPACE:
            self._fire_bullet()

    def _check_keyup_events(self, event):
        """响应松开"""
        if event.key == pygame.K_RIGHT:
            self.ship.moving_right = False
        elif event.key == pygame.K_LEFT:
            self.ship.moving_left = False

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

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

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

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



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

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

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

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

    def _check_fleet_edges(self):
        """Respond appropriately if any aliens have reached an edge."""
        for alien in self.aliens.sprites():
            if alien.check_edges():
                self._change_fleet_direction()
                break

    def _change_fleet_direction(self):
        """Drop the entire fleet and change the fleet's direction."""
        for alien in self.aliens.sprites():
            alien.rect.y += self.settings.fleet_drop_speed
        self.settings.fleet_direction *= -1


    def _update_screen(self):
        """更新屏幕上的图像,并切换到新屏幕。"""
        # 每次循环时都重绘屏幕。
        self.screen.fill(self.settings.bg_color)
        # 填充背景后,调用ship.blitme() 将飞船绘制到屏幕上,确保它出现在背景前面
        self.ship.blitme()

        #要让外星人现身,需要在_update_screen() 中对外星人编组调用方法draw()
        for bullet in self.bullets.sprites():
            bullet.draw_bullet()
        self.aliens.draw(self.screen)

        # 让最近绘制的屏幕可见。
        pygame.display.flip()


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

 

 

 

1.2.5.2 为测试创建大子弹

请尝试将bullet_width 设置为300乃至3000,看看将 所有外星人全部射杀有多快!

 

1.2.5.3 生成新的外星人群

这个游戏的一个重要特点是,外星人无穷无尽:一群外星人被消灭后,又会出现另 一群外星人。
要在一群外星人被消灭后再显示一群外星人,首先需要检查编组aliens 是否为 空。如果是,就调用_create_fleet() 。我们将在_update_bullets() 末尾 执行这项任务,因为外星人都是在这里被消灭的:


alien_invasion.py

 

import sys
import pygame

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


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

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

        self.screen = pygame.display.set_mode((0, 0), pygame.FULLSCREEN)
        self.settings.screen_width = self.screen.get_rect().width
        self.settings.screen_height = self.screen.get_rect().height
        pygame.display.set_caption("Alien Invasion")

         # 创建用于存储子弹的编组
        self.ship = Ship(self)
        self.bullets = pygame.sprite.Group()

        # 创建了一个用于存储外星人群的编组,还调用了接下来将编写的方法
        self.aliens = pygame.sprite.Group()
        self._create_fleet()

    def run_game(self):
        """开始游戏的主循环"""
        while True:
            #管理事件的代码
            self._check_events()
            # 飞船的位置将在检测到键盘事件后(但在更新屏幕前)更新
            self.ship.update()
            # 在while 循环中更新子弹的位置
            self._update_bullets()
            #更新每个外星人 位置的方法
            self._update_aliens()
            #更新屏幕的代码
            self._update_screen()


    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
        elif event.key == pygame.K_q:
            sys.exit()
        elif event.key == pygame.K_SPACE:
            self._fire_bullet()

    def _check_keyup_events(self, event):
        """响应松开"""
        if event.key == pygame.K_RIGHT:
            self.ship.moving_right = False
        elif event.key == pygame.K_LEFT:
            self.ship.moving_left = False

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

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

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

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

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



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

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

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

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

    def _check_fleet_edges(self):
        """Respond appropriately if any aliens have reached an edge."""
        for alien in self.aliens.sprites():
            if alien.check_edges():
                self._change_fleet_direction()
                break

    def _change_fleet_direction(self):
        """Drop the entire fleet and change the fleet's direction."""
        for alien in self.aliens.sprites():
            alien.rect.y += self.settings.fleet_drop_speed
        self.settings.fleet_direction *= -1


    def _update_screen(self):
        """更新屏幕上的图像,并切换到新屏幕。"""
        # 每次循环时都重绘屏幕。
        self.screen.fill(self.settings.bg_color)
        # 填充背景后,调用ship.blitme() 将飞船绘制到屏幕上,确保它出现在背景前面
        self.ship.blitme()

        #要让外星人现身,需要在_update_screen() 中对外星人编组调用方法draw()
        for bullet in self.bullets.sprites():
            bullet.draw_bullet()
        self.aliens.draw(self.screen)

        # 让最近绘制的屏幕可见。
        pygame.display.flip()


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

 

1.2.5.4 提高子弹的速度

settings.py

----
        # 子弹设置
        self.bullet_speed = 3.0
----

 

1.2.5.5 重构_update_bullets()

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

 

import sys
import pygame

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


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

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

        self.screen = pygame.display.set_mode((0, 0), pygame.FULLSCREEN)
        self.settings.screen_width = self.screen.get_rect().width
        self.settings.screen_height = self.screen.get_rect().height
        pygame.display.set_caption("Alien Invasion")

         # 创建用于存储子弹的编组
        self.ship = Ship(self)
        self.bullets = pygame.sprite.Group()

        # 创建了一个用于存储外星人群的编组,还调用了接下来将编写的方法
        self.aliens = pygame.sprite.Group()
        self._create_fleet()

    def run_game(self):
        """开始游戏的主循环"""
        while True:
            #管理事件的代码
            self._check_events()
            # 飞船的位置将在检测到键盘事件后(但在更新屏幕前)更新
            self.ship.update()
            # 在while 循环中更新子弹的位置
            self._update_bullets()
            #更新每个外星人 位置的方法
            self._update_aliens()
            #更新屏幕的代码
            self._update_screen()


    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
        elif event.key == pygame.K_q:
            sys.exit()
        elif event.key == pygame.K_SPACE:
            self._fire_bullet()

    def _check_keyup_events(self, event):
        """响应松开"""
        if event.key == pygame.K_RIGHT:
            self.ship.moving_right = False
        elif event.key == pygame.K_LEFT:
            self.ship.moving_left = False

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

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

        self._check_bullet_alien_collisions()

    def _check_bullet_alien_collisions(self):
        """响应子弹和外星人碰撞。"""
        # 删除发生碰撞的子弹和外星人。
        collisions = pygame.sprite.groupcollide(
            self.bullets, self.aliens, True, True)

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


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



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

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

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

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

    def _check_fleet_edges(self):
        """Respond appropriately if any aliens have reached an edge."""
        for alien in self.aliens.sprites():
            if alien.check_edges():
                self._change_fleet_direction()
                break

    def _change_fleet_direction(self):
        """Drop the entire fleet and change the fleet's direction."""
        for alien in self.aliens.sprites():
            alien.rect.y += self.settings.fleet_drop_speed
        self.settings.fleet_direction *= -1


    def _update_screen(self):
        """更新屏幕上的图像,并切换到新屏幕。"""
        # 每次循环时都重绘屏幕。
        self.screen.fill(self.settings.bg_color)
        # 填充背景后,调用ship.blitme() 将飞船绘制到屏幕上,确保它出现在背景前面
        self.ship.blitme()

        #要让外星人现身,需要在_update_screen() 中对外星人编组调用方法draw()
        for bullet in self.bullets.sprites():
            bullet.draw_bullet()
        self.aliens.draw(self.screen)

        # 让最近绘制的屏幕可见。
        pygame.display.flip()


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

  

1.2.6 结束游戏

 

1.2.6. 1 检测外星人和飞船碰撞

首先检查外星人和飞船之间的碰撞,以便在外星人撞上飞船时做出合适的响应。为 此,在AlienInvasion 中更新每个外星人的位置后,立即检测外星人和飞船之间 的碰撞:
alien_invasion.py

----
    def _update_aliens(self):
        """检查是否有外星人位于屏幕边缘,并更新整群外星人的位置。"""
        self._check_fleet_edges()
        self.aliens.update()

        # 检测外星人和飞船之间的碰撞。
        if pygame.sprite.spritecollideany(self.ship, self.aliens):
            print("Ship hit!!!")
----
C:\Users\yangy\AppData\Local\Programs\Python\Python36\python.exe C:/pythonsrc/chapter_12/chapter_12_test/alien_invasion.py
pygame 2.1.1 (SDL 2.0.18, Python 3.6.6)
Hello from the pygame community. https://www.pygame.org/contribute.html
Ship hit!!!
Ship hit!!!
Ship hit!!!

 

1.2.6. 2 响应外星人和飞船碰撞

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

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

class GameStats:
    """跟踪游戏的统计信息。"""

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

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

在游戏运行期间,只创建一个GameStats 实例,但每当玩家开始新游戏时,需要 重置一些统计信息。为此,在方法reset_stats() 中初始化大部分统计信息,而 不是在__init__() 中直接初始化。我们在__init__() 中调用这个方法,这样 创建GameStats 实例时将妥善地设置这些统计信息,在玩家开始新游戏时也能调 用reset_stats() 。
当前,只有一项统计信息ships_left ,其值在游戏运行期间不断变化。一开始玩 家拥有的飞船数存储在settings.py的ship_limit 中:
settings.py

-----
        # 飞船设置
        self.ship_speed = 5
        self.ship_limit = 3
-----

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

alien_invasion.py

import sys
from time import sleep

import pygame

from settings import Settings
from game_stats import GameStatsz
from ship import Ship
from bullet import Bullet
from alien import Alien
----

 

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

alien_invasion.py

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

        self.screen = pygame.display.set_mode((0, 0), pygame.FULLSCREEN)
        self.settings.screen_width = self.screen.get_rect().width
        self.settings.screen_height = self.screen.get_rect().height
        pygame.display.set_caption("Alien Invasion")

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

         # 创建用于存储子弹的编组
        self.ship = Ship(self)
        self.bullets = pygame.sprite.Group()
----

在创建游戏窗口后、定义诸如飞船等其他游戏元素前,创建一个GameStats 实 例。
有外星人撞到飞船时,将余下的飞船数减1,创建一群新的外星人,并将飞船重新放 到屏幕底端的中央。另外,让游戏暂停片刻,让玩家在新外星人群出现前注意到发 生了碰撞并将重新创建外星人群。
下面将实现这些功能的大部分代码放到新方法_ship_hit() 中。在 _update_aliens() 中,将在有外星人撞到飞船时调用这个方法:
alien_invasion.py

----
        if pygame.sprite.spritecollideany(self.ship, self.aliens):
            print("Ship hit!!!")

    def _ship_hit(self):
        """响应飞船被外星人撞到。"""

        # 将ships_left减1。
        self.stats.ships_left -= 1

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

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

        # 暂停。
        sleep(0.5)

    def _create_fleet(self):
----

新方法_ship_hit() 在飞船被外星人撞到时做出响应。在这个方法中,将余下的 飞船数减1(见❶),再清空编组aliens 和bullets (见❷)。接下来,创建一群新的外星人,并将飞船居中(见❸)。(稍后将在Ship 类中添加 方法center_ship() 。)最后,在更新所有元素后(但在将修改显示到屏幕前) 暂停,让玩家知道飞船被撞到了(见❹)。这里的函数调用sleep() 让游戏暂停半 秒钟,让玩家能够看到外星人撞到了飞船。函数sleep() 执行完毕后,将接着执行 方法_update_screen() ,将新的外星人群绘制到屏幕上。
在_update_aliens() 中,当有外星人撞到飞船时,不调用函数print() ,而调 用_ship_hit() :
alien_invasion.py

----
    def _update_aliens(self):
        """检查是否有外星人位于屏幕边缘,并更新整群外星人的位置。"""
        self._check_fleet_edges()
        self.aliens.update()

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

    def _ship_hit(self):
        """响应飞船被外星人撞到。"""
----

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

ship.py

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

这里像__init__() 中那样让飞船在屏幕底端居中。让飞船在屏幕底端居中后,重 置用于跟踪飞船确切位置的属性self.x 。
注意 我们根本没有创建多艘飞船。在整个游戏运行期间,只创建了一个飞船 实例,并在该飞船被撞到时将其居中。统计信息ships_left 指出玩家是否用 完了所有的飞船。
请运行这个游戏,射杀几个外星人,并让一个外星人撞到飞船。游戏暂停片刻后, 将出现一群新的外星人,而飞船将在屏幕底端居中。

 

1.2.6. 3 有外星人到达屏幕底端

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

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

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

alien_invasion.py

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

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

        # 检查是否有外星人到达了屏幕底端。
        self._check_aliens_bottom()

 

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

  

1.2.6. 4 游戏结束

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

game_stats.py

class GameStats:
    """Track statistics for Alien Invasion."""

    def __init__(self, ai_game):
        """Initialize statistics."""
        self.settings = ai_game.settings
        self.reset_stats()

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

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

alien_invasion.py

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

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

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

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

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

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

alien_invasion.py

    def run_game(self):
        """开始游戏的主循环"""
        while True:
            #管理事件的代码
            self._check_events()

            if self.stats.game_active:
                # 飞船的位置将在检测到键盘事件后(但在更新屏幕前)更新
                self.ship.update()
                # 在while 循环中更新子弹的位置
                self._update_bullets()
                #更新每个外星人 位置的方法
                self._update_aliens()
            #更新屏幕的代码
            self._update_screen()

 

1.3 记分

 

1.3.1 添加Play按钮

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

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

game_stats.py

class GameStats:
    """跟踪游戏的统计信息。"""

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

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

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

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

1.3.1.1 创建Button 类

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

button.py

import pygame.font
class Button:
    def __init__(self, ai_game, msg):
        """初始化按钮的属性。"""
        self.screen = ai_game.screen
        self.screen_rect = self.screen.get_rect()

        # 设置按钮的尺寸和其他属性。
        self.width, self.height = 200, 50
        self.button_color = (0, 255, 0)
        self.text_color = (255, 255, 255)
        self.font = pygame.font.SysFont(None, 48)

        # 创建按钮的rect对象,并使其居中。
        self.rect = pygame.Rect(0, 0, self.width, self.height)
        self.rect.center = self.screen_rect.center

        # 按钮的标签只需创建一次。
        self._prep_msg(msg)

 

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

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

Pygame处理文本的方式是,将要显示的字符串渲染为图像。在❺处,调用了 _prep_msg() 来处理这样的渲染。

_prep_msg() 的代码如下:

button.py

import pygame.font
class Button:
    def __init__(self, ai_game, msg):
        """初始化按钮的属性。"""
        self.screen = ai_game.screen
        self.screen_rect = self.screen.get_rect()

        # 设置按钮的尺寸和其他属性。
        self.width, self.height = 200, 50
        self.button_color = (0, 255, 0)
        self.text_color = (255, 255, 255)
        self.font = pygame.font.SysFont(None, 48)

        # 创建按钮的rect对象,并使其居中。
        self.rect = pygame.Rect(0, 0, self.width, self.height)
        self.rect.center = self.screen_rect.center

        # 按钮的标签只需创建一次。
        self._prep_msg(msg)

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

方法_prep_msg() 接受实参self 以及要渲染为图像的文本(msg )。调用 font.render() 将存储在msg 中的文本转换为图像,再将该图像存储在 self.msg_image 中(见❶)。方法font.render() 还接受一个布尔实参,该 实参指定开启还是关闭反锯齿功能(反锯齿让文本的边缘更平滑)。余下的两个实 参分别是文本颜色和背景色。我们启用了反锯齿功能,并将文本的背景色设置为按 钮的颜色。(如果没有指定背景色,Pygame渲染文本时将使用透明背景。)
在❷处,让文本图像在按钮上居中:根据文本图像创建一个rect ,并将其center 属性设置为按钮的center 属性。
最后,创建方法draw_button() ,用于将这个按钮显示到屏幕上:

button.py

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

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

 

1.3.1.2 在屏幕上绘制按钮

 我们将在AlienInvasion 中使用Button 类来创建一个Play按钮。首先,更新 import 语句:

alien_invasion.py

----
from button import Button

只需要一个Play按钮,因此在AlienInvasion 类的方法__init__() 中创建它。 可将这些代码放在方法__init__() 的末尾:

alien_invasion.py

----
        self._create_fleet()

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

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

    def _update_screen(self):
        """更新屏幕上的图像,并切换到新屏幕。"""
        # 每次循环时都重绘屏幕。
        self.screen.fill(self.settings.bg_color)
        # 填充背景后,调用ship.blitme() 将飞船绘制到屏幕上,确保它出现在背景前面
        self.ship.blitme()

        #要让外星人现身,需要在_update_screen() 中对外星人编组调用方法draw()
        for bullet in self.bullets.sprites():
            bullet.draw_bullet()
        self.aliens.draw(self.screen)

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

        # 让最近绘制的屏幕可见。
        pygame.display.flip()

 

  

 游戏处于非活动状态时出现的Play按钮,但是还不能点击

 

1.3.1.3 开始游戏

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

            elif event.type == pygame.MOUSEBUTTONDOWN:
                mouse_pos = pygame.mouse.get_pos()
                self._check_play_button(mouse_pos)

 

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

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

这里使用了rect 的方法collidepoint() 检查鼠标单击位置是否在Play按钮的 rect 内(见❶)。如果是,就将game_active 设置为True ,让游戏开始!
至此,现在应该能够开始这个游戏了。游戏结束时,应将game_active 设置为 False ,并重新显示Play按钮。

 

1.3.1.4 重置游戏

前面编写的代码只处理了玩家第一次单击Play按钮的情况,而没有处理游戏结束的 情况,因为没有重置导致游戏结束的条件。
为在玩家每次单击Play按钮时都重置游戏,需要重置统计信息、删除现有的外星人 和子弹、创建一群新的外星人并让飞船居中,如下所示:
alien_invasion.py

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

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

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

在❶处,重置游戏统计信息,给玩家提供三艘新飞船。接下来,将game_active 设置为True 。这样,这个方法的代码执行完毕后,游戏就将开始。清空编组 aliens 和bullets (见❷),然后创建一群新的外星人并将飞船居中(见❸)。
现在,每当玩家单击Play按钮时,这个游戏都将正确地重置,让玩家想玩多少次就 玩多少次!

 

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

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

alien_invasion.py

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

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

 

1.3.1.6 隐藏鼠标光标

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

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

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

            # 创建一群新的外星人并让飞船居中。
            self._create_fleet()
            self.ship.center_ship()
# 隐藏鼠标光标。 pygame.mouse.set_visible(False)

通过向set_visible() 传递False ,让Pygame在光标位于游戏窗口内时将其隐藏 起来。
游戏结束后,将重新显示光标,让玩家能够单击Play按钮来开始新游戏。相关的代 码如下:
alien_invasion.py

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

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

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

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

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

 

1.3.2 提高等级

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

 

1.3.2. 1 修改速度设置

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

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

        # 飞船设置
        #self.ship_speed = 5
        self.ship_limit = 2

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

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

        # 加快游戏节奏的速度。
        self.speedup_scale = 1.1

        self.initialize_dynamic_settings()

依然在__init__() 中初始化静态设置。在❶处,添加设置speedup_scale ,用 于控制游戏节奏的加快速度:2表示玩家每提高一个等级,游戏的节奏就翻一倍;1 表示游戏节奏始终不变。将其设置为1.1能够将游戏节奏提高到足够快,让游戏既有 难度又并非不可完成。最后,调用initialize_dynamic_settings() 初始化 随游戏进行而变化的属性(见❷)。
initialize_dynamic_settings() 的代码如下:

settings.py

----
    def initialize_dynamic_settings(self):
        """初始化随游戏进行而变化的设置。"""
        self.ship_speed = 1.5
        self.bullet_speed = 3.0
        self.alien_speed = 1.0

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

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

----
    def increase_speed(self):
        """提高速度设置"""
        self.ship_speed *= self.speedup_scale
        self.bullet_speed *= self.speedup_scale
        self.alien_speed *= self.speedup_scale

为提高这些游戏元素的速度,将每个速度设置都乘以speedup_scale 的值。
在_check_bullet_alien_collisions() 中,在整群外星人都被消灭后调用 increase_speed() 来加快游戏的节奏:

alien_invasion.py

    def _check_bullet_alien_collisions(self):
        """响应子弹和外星人碰撞。"""
        # 删除发生碰撞的子弹和外星人。
        collisions = pygame.sprite.groupcollide(
            self.bullets, self.aliens, True, True)

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

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

 

1.3.2.2 重置速度

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

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

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

 

1.3.3 记分

下面来实现一个记分系统,以实时跟踪玩家的得分,并显示最高得分、等级和余下 的飞船数。
得分是游戏的一项统计信息,因此在GameStats 中添加一个score 属性:

game_stats.py

----
    def reset_stats(self):
        """初始化随游戏进行可能变化的统计信息。"""
        self.ships_left = self.ai_settings.ship_limit
        self.score = 0

为在每次开始游戏时都重置得分,我们在reset_stats() 而不是__init__() 中 初始化score 。

 

1.3.3.1 显示得分

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

import pygame.font
class Scoreboard:
    """显示得分信息的类。"""
    def __init__(self, ai_game):
        """初始化显示得分涉及的属性。"""
        self.screen = ai_game.screen
        self.screen_rect = self.screen.get_rect()
        self.settings = ai_game.settings
        self.stats = ai_game.stats

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

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

由于Scoreboard 在屏幕上显示文本,首先导入模块pygame.font 。接下来,在 __init__() 中包含形参ai_game ,以便访问报告跟踪的值所需的对象settings 、screen 和stats (见❶)。然后,设置文本颜色(见❷)并实例化 一个字体对象(见❸)。
为将要显示的文本转换为图像,调用prep_score() (见❹),其定义如下:

scoreboard.py

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

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

在prep_score() 中,将数值stats.score 转换为字符串(见❶),再将这个字 符串传递给创建图像的render() (见❷)。为在屏幕上清晰地显示得分,向 render() 传递屏幕背景色和文本颜色。
将得分放在屏幕右上角,并在得分增大导致数变宽时让其向左延伸。为确保得分始 终锚定在屏幕右边,创建一个名为score_rect 的rect (见❸),让其右边缘与 屏幕右边缘相距20像素(见❹),并让其上边缘与屏幕上边缘也相距20像素(见 ❺)。
接下来,创建方法show_score() ,用于显示渲染好的得分图像:

scoreboard.py

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

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

 

1.3.3.2 创建记分牌

为显示得分,在AlienInvasion 中创建一个Scoreboard 实例。先来更新 import 语句:

alien_invasion.py

import sys
from time import sleep

import pygame

from settings import Settings
from game_stats import GameStats
from ship import Ship
from bullet import Bullet
from alien import Alien
from button import Button
from scoreboard import Scoreboard

接下来,在方法__init__() 中创建一个Scoreboard 实例:

alien_invasion.py

----
        self.settings.screen_height = self.screen.get_rect().height
        pygame.display.set_caption("Alien Invasion")

        # 创建存储游戏统计信息的实例, # 并创建记分牌。
        self.stats = GameStats(self)
        self.sb = Scoreboard(self)

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

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

alien_invasion.py

    def _update_screen(self):
        """更新屏幕上的图像,并切换到新屏幕。"""
        # 每次循环时都重绘屏幕。
        self.screen.fill(self.settings.bg_color)
        # 填充背景后,调用ship.blitme() 将飞船绘制到屏幕上,确保它出现在背景前面
        self.ship.blitme()

        #要让外星人现身,需要在_update_screen() 中对外星人编组调用方法draw()
        for bullet in self.bullets.sprites():
            bullet.draw_bullet()
        self.aliens.draw(self.screen)

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

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

        # 让最近绘制的屏幕可见。
        pygame.display.flip()

在显示Play按钮前调用show_score() 。
如果现在运行这个游戏,将在屏幕右上角看到0。(当前,我们只想在进一步开发记 分系统前确认得分出现在正确的地方。)图14-2显示了游戏开始前的得分。

 

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

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

    def initialize_dynamic_settings(self):
        """初始化随游戏进行而变化的设置。"""
        self.ship_speed = 1.5
        self.bullet_speed = 3.0
        self.alien_speed = 1.0

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

        # 记分
        self.alien_points = 50

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

    def _check_bullet_alien_collisions(self):
        """响应子弹和外星人碰撞。"""
        # 删除发生碰撞的子弹和外星人。
        collisions = pygame.sprite.groupcollide(
            self.bullets, self.aliens, True, True)

        if collisions:
            self.stats.score += self.settings.alien_points
            self.sb.prep_score()

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

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

 

1.3.3.4 重置得分

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

alien_invasion.py

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

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

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

            # 创建一群新的外星人并让飞船居中。
            self._create_fleet()
            self.ship.center_ship()
            # 隐藏鼠标光标。
            pygame.mouse.set_visible(False)

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

 

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

当前的代码可能会遗漏一些被消灭的外星人。例如,如果在一次循环中,有两颗子 弹击中了外星人,或者因子弹较宽而同时击中了多个外星人,玩家将只能得到一个 外星人的分数。为修复这种问题,我们来调整检测子弹和外星人碰撞的方式。
在_check_bullet_alien_collisions() 中,与外星人碰撞的子弹都是字典 collisions 中的一个键,而与每颗子弹相关的值都是一个列表,其中包含该子弹 击中的外星人。我们遍历字典collisions ,确保将消灭的每个外星人都计入得 分:

alien_invasion.py

    def _check_bullet_alien_collisions(self):
        """响应子弹和外星人碰撞。"""
        # 删除发生碰撞的子弹和外星人。
        collisions = pygame.sprite.groupcollide(
            self.bullets, self.aliens, True, True)

        if collisions:
            for aliens in collisions.values():
                self.stats.score += self.settings.alien_points * len(aliens)
                self.sb.prep_score()
            #self.stats.score += self.settings.alien_points
            #self.sb.prep_score()

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

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

 

1.3.3.6 提高分数

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

----
        # 加快游戏节奏的速度。
        self.speedup_scale = 1.1

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

        self.initialize_dynamic_settings()

    def initialize_dynamic_settings(self):
        """初始化随游戏进行而变化的设置。"""
        self.ship_speed = 1.5
        self.bullet_speed = 3.0
        self.alien_speed = 1.0

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

        # 记分
        self.alien_points = 50

    def increase_speed(self):
        """提高速度设置"""
        self.ship_speed *= self.speedup_scale
        self.bullet_speed *= self.speedup_scale
        self.alien_speed *= self.speedup_scale

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

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

    def increase_speed(self):
        """提高速度设置"""
        self.ship_speed *= self.speedup_scale
        self.bullet_speed *= self.speedup_scale
        self.alien_speed *= self.speedup_scale

        self.alien_points = int(self.alien_points * self.score_scale)
        print(self.alien_points)
C:\Users\yangy\AppData\Local\Programs\Python\Python36\python.exe C:/pythonsrc/chapter_12/chapter_12_test/alien_invasion.py
pygame 2.1.1 (SDL 2.0.18, Python 3.6.6)
Hello from the pygame community. https://www.pygame.org/contribute.html
75
112
168
252
378
567
850

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

 

1.3.3.7 舍入得分

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

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

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

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

 

1.3.3.8 最高得分

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

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

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

        self.high_score = 0
----

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

scoreboard.py

----
        self.font = pygame.font.SysFont(None, 48)

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

    def prep_score(self):
----

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

scoreboard.py

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

将最高得分舍入到最近的10的整数倍,并添加用逗号表示的千分位分隔符(见❶)。 然后,根据最高得分生成一幅图像(见❷),使其水平居中(见❸),并将其top 属性设置为当前得分图像的top 属性(见❹)。
现在,方法show_score() 需要在屏幕右上角显示当前得分,并在屏幕顶部中央显 示最高得分:
scoreboard.py

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

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

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

方法check_high_score() 比较当前得分和最高得分。如果当前得分更高,就更 新high_score 的值,并调用prep_high_score() 来更新包含最高得分的图像。
在_check_bullet_alien_collisions() 中,每当有外星人被消灭时,都需要 在更新得分后调用check_high_score() :

alien_invasion.py

----
        if collisions:
            for aliens in collisions.values():
                self.stats.score += self.settings.alien_points * len(aliens)
                self.sb.prep_score()
                self.sb.check_high_score()       
----

 

1.3.3.9 显示等级

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

game_stats.py

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

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

import pygame.font
class Scoreboard:
    """显示得分信息的类。"""
    def __init__(self, ai_game):
        """初始化显示得分涉及的属性。"""
        self.screen = ai_game.screen
        self.screen_rect = self.screen.get_rect()
        self.settings = ai_game.settings
        self.stats = ai_game.stats

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

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

prep_level() 的代码如下:

scoreboard.py

----
    def prep_level(self):
        """将等级转换为渲染的图像。"""
        level_str = str(self.stats.level)
        self.level_image = self.font.render(level_str, True,
                                            self.text_color, self.settings.bg_color)
        self.level_rect = self.level_image.get_rect()
        self.level_rect.right = self.score_rect.right
        self.level_rect.top = self.score_rect.bottom + 10

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

scoreboard.py

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

新增的代码行在屏幕上显示等级图像。 我们在_check_bullet_alien_collisions() 中提高等级并更新等级图像:

alien_invasion.py

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

            # 提高等级。
            self.stats.level += 1
            self.sb.prep_level()

 

如果整群外星人都被消灭,就将stats.level 的值加1,并调用prep_level() 确保正确地显示了新等级。
为确保在开始新游戏时更新等级图像,还需在玩家单击按钮Play时调用 prep_level() :
alien_invasion.py

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

            # 重置游戏统计信息。
            self.stats.reset_stats()
----

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

  

1.3.3.10 显示余下的飞船数

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

ship.py

import pygame
class Ship(Sprite):
    """管理飞船的类"""
    def __init__(self, ai_game):
        """初始化飞船并设置其初始位置。"""
        super().__init__()
        self.screen = ai_game.screen
        self.settings = ai_game.settings
        self.screen_rect = ai_game.screen.get_rect()
----

这里导入了Sprite ,让Ship 继承Sprite (见❶),并在__init__() 的开头 调用super() (见❷)。接下来,需要修改Scoreboard ,以创建可供显示的飞船编组。下面是其中的 import 语句:
scoreboard.py

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

class Scoreboard:
----

鉴于需要创建飞船编组,导入Group 和Ship 类。 下面是方法__init__() :

scoreboard.py

----
        # 准备包含最高得分和当前得分的图像。
        self.prep_score()

        """初始化记录得分的属性。"""
        self.ai_game = ai_game
        self.screen = ai_game.screen
        
        self.prep_high_score()
        self.prep_level()
----

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

scoreboard.py

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

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

scoreboard.py

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

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

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

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

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

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

 

posted @ 2022-01-02 18:59  一代肝帝  阅读(274)  评论(0编辑  收藏  举报