《Python编程:从入门到实践》外星人入侵

一、规划

在游戏《外星人入侵》中,玩家控制着一艘最初出现在屏幕底部中央的飞船。玩家可以使用箭头键左右移动飞船,还可使用空格键进行射击。游戏开始时,一群外星人出现在天空中,他们在屏

幕中向下移动。玩家的任务是射杀这些外星人。玩家将所有外星人都消灭干净后,将出现一群新的外星人,他们移动的速度更快。只要有外星人撞到了玩家的飞船或到达了屏幕底部,玩家就损失一艘飞船。玩家损失三艘飞船后,游戏结束。

二、依赖库

Pygame

三、创建飞船

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

python 复制代码
import sys
import pygame

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

run_game()

pygame.init()初始化背景设置,让Pygame能够正确地工作。调用pygame.display.set_mode()来创建一个名为screen的显示窗口,这个游戏的所有图形元素都将在其中绘制。实参(1200, 800)是一个元组,指定了游戏窗口的尺寸。

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

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

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

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

2.设置背景色

python 复制代码
import sys
import pygame

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

run_game()

在Pygame中,颜色是以RGB值指定的。这种颜色由红色、绿色和蓝色值组成,其中每个值的可能取值范围都为0~255。我们调用方法screen.fill(),用背景色填充屏幕;这个方法只接受一个实参:一种颜色。

3.创建设置类

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

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

run_game()

编写一个名为settings的模块,其中包含一个名为Settings的类,用于将所有设置存储在一个地方,以免在代码中到处添加设置。这样,我们就能传递一个设置对象,而不是众多不同的设置。

4.添加飞船图像

在游戏中几乎可以使用任何类型的图像文件,但使用位图(.bmp)文件最为简单,因为Pygame默认加载位图。虽然可配置Pygame以使用其他文件类型,但有些文件类型要求你在计算机上安装相应的图像库。图灵社区

5.创建Ship类

python 复制代码
import pygame

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

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

Ship的方法__init__()接受两个参数:引用self和screen,其中后者指定了要将飞船绘制到什么地

方。为加载图像,我们调用了pygame.image.load()。这个函数返回一个表示飞船的surface,而我们将这个surface存储到了self.image中。

加载图像后,我们使用get_rect()获取相应surface的属性rect。Pygame的效率之所以如此高,一个原因是它让你能够像处理矩形一样处理游戏元素,即便它们的形状并非矩形。

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

6.在屏幕上绘制飞船

python 复制代码
import sys
import pygame
from settings import Settings
from ship import Ship
def run_game():
    # 初始化游戏并创建一个屏幕对象
    pygame.init()
    ai_settings = Settings()
    screen=pygame.display.set_mode((ai_settings.screen_width,ai_settings.screen_height))
    pygame.display.set_caption("Alien Invasion")
    ship = Ship(screen)

    #游戏开始的主循环
    while True:
        #监视键盘和鼠标
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                sys.exit()
        # 每次循环时都重绘屏幕
        screen.fill(ai_settings.bg_color)
        ship.blitme()
        #让最近绘制的屏幕可见
        pygame.display.flip()

run_game()

7.重构:模块game_functions

在大型项目中,经常需要在添加新代码前重构既有代码。重构旨在简化既有代码的结构,使其更容易扩展。

(1)函数check_events()

python 复制代码
import pygame
import sys

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

(2)函数update_screen()

python 复制代码
import pygame
import sys

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

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

8.驾驶飞船

(1)响应按键

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

python 复制代码
def check_events(ship):
    """响应按键和鼠标事件"""
    for event in pygame.event.get():
        if event.type ==pygame.QUIT:
            sys.exit()
        elif event.type == pygame.KEYDOWN:
            if event.key == pygame.K_RIGHT:
                ship.rect.center_x += 1

(2)允许不断移动

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

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

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

python 复制代码
import pygame

class Ship():
    def __init__(self,screen):
        """初始化飞船并设置其初始位置"""
        self.screen = screen
        # 加载飞船图像并获取其外接矩形
        self.image = pygame.image.load('images/ship.bmp')
        self.rect = self.image.get_rect()
        self.screen_rect = screen.get_rect()
        # 将每艘新飞船放在屏幕底部中央
        self.rect.centerx=self.screen_rect.centerx
        self.rect.bottom=self.screen_rect.bottom
        # 移动标志
        self.moving_right = False
    def update(self):
        """根据移动标志调整飞船的位置"""
        if self.moving_right:
            self.rect.centerx+=1
    def blitme(self):
        """在指定位置绘制飞船"""
        self.screen.blit(self.image, self.rect)
python 复制代码
def check_events(ship):
    """响应按键和鼠标事件"""
    for event in pygame.event.get():
        if event.type ==pygame.QUIT:
            sys.exit()
        elif event.type == pygame.KEYDOWN:
            if event.key == pygame.K_RIGHT:
                ship.moving_right = True
        elif event.type == pygame.KEYUP:
            if event.key == pygame.K_RIGHT:
                ship.moving_right = False

修改alien_invasion.py中的while循环

python 复制代码
    while True:
        #监视键盘和鼠标
        gf.check_events(ship)
        ship.update()
        gf.update_screen(ai_settings, screen, ship)

(3)左右移动

ship.py更新

python 复制代码
import pygame

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

game_functions.py更新

python 复制代码
def check_events(ship):
    """响应按键和鼠标事件"""
    for event in pygame.event.get():
        if event.type ==pygame.QUIT:
            sys.exit()
        elif event.type == pygame.KEYDOWN:
            if event.key == pygame.K_RIGHT:
                ship.moving_right = True
            elif event.key == pygame.K_LEFT:
                ship.moving_left = True
        elif event.type == pygame.KEYUP:
            if event.key == pygame.K_RIGHT:
                ship.moving_right = False
            elif event.key == pygame.K_LEFT:
                ship.moving_left = False

(4)调整飞船的速度

在Settings类中添加属性ship_speed_factor,用于控制飞船的速度。

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

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

python 复制代码
import pygame

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

(5)限制飞船的活动范围

将修改Ship类的方法update()

python 复制代码
    def update(self):
        """根据移动标志调整飞船的位置"""
        # 更新飞船的center值,而不是rect
        if self.moving_right and self.rect.right < self.screen_rect.right:
            self.center +=self.ai_settings.ship_speed_factor
        if self.moving_left and self.rect.left > 0:
            self.center -=self.ai_settings.ship_speed_factor
        # 根据self.center更新rect对象
        self.rect.centerx=self.center

(6)重构check_events()

我们将其部分代码放在两个函数中:一个处理KEYDOWN事件,另一个处理KEYUP事件

python 复制代码
def check_keydown_events(event,ship):
    if event.key == pygame.K_RIGHT:
        ship.moving_right = True
    elif event.key == pygame.K_LEFT:
        ship.moving_left = True

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

9.射击

(1)添加子弹设置

python 复制代码
class Settings():
    """存储《外星人入侵》的所有设置的类"""
    def __init__(self):
        """初始化游戏的设置"""
        # 屏幕设置
        self.screen_width = 1200
        self.screen_height = 600
        self.bg_color = (230, 230, 230)
        self.ship_speed_factor = 1.5
        # 子弹设置
        self.bullet_speed_factor = 1
        self.bullet_width = 3
        self.bullet_height = 15
        self.bullet_color = 60, 60, 60

(2)创建Bullet类

python 复制代码
import pygame
from pygame.sprite import Sprite

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

Bullet类继承了我们从模块pygame.sprite中导入的Sprite类。通过使用Sprite,可将游戏中相关的元素编组,进而同时操作编组中的所有元素。

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

(3)将子弹存储到编组中

python 复制代码
import pygame

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

run_game()

我们将在alien_invasion.py中创建一个编组(group),用于存储所有有效的子弹,以便能够管理发射出去的所有子弹。这个编组将是pygame.sprite.Group类的一个实例;pygame.sprite.Group类类似于列表,但提供了有助于开发游戏的额外功能。

我们将bullets传递给了check_events()和update_screen()。在check_events()中,需要在玩家按空格键时处理bullets;而在update_screen()中,需要更新要绘制到屏幕上的bullets。当你对编组调用update()时,编组将自动对其中的每个精灵调用update(),因此代码行bullets.update()将为编组bullets中的每颗子弹调用bullet.update()。

(4)开火

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

python 复制代码
import pygame
import sys
from bullet import Bullet

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

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

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

编组bulltes传递给了check_keydown_events()。玩家按空格键时,创建一颗新子弹,并使用方法add()将其加入到编组bullets中。

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

(5)删除已消失的子弹

我们检查每颗子弹,看看它是否已从屏幕顶端消失。如果是这样,就将其从bullets中删除。我们使用了一条print语句,以显示当前还有多少颗子弹,从而核实已消失的子弹确实删除了。

python 复制代码
#游戏开始的主循环
    while True:
        #监视键盘和鼠标
        gf.check_events(ai_settings, screen, ship, bullets)
        ship.update()
        bullets.update()

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

        gf.update_screen(ai_settings, screen, ship, bullets)

(6)限制子弹数量

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

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

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

python 复制代码
    elif event.key == pygame.K_SPACE:
        # 创建一颗子弹,并将其加入到编组bullets中
        if len(bullets) < ai_settings.bullets_allowed:
            new_bullet = Bullet(ai_settings, screen, ship)
            bullets.add(new_bullet)

(7)创建函数update_bullets()

我们创建一个名为update_bullets()的新函数,并将其添加到game_functions.py的末尾

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

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

python 复制代码
#游戏开始的主循环
    while True:
        #监视键盘和鼠标
        gf.check_events(ai_settings, screen, ship, bullets)
        ship.update()
        gf.update_bullets(bullets)
        gf.update_screen(ai_settings, screen, ship, bullets)

(8)创建函数fire_bullet()

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

python 复制代码
def check_keydown_events(event,ai_settings, screen, ship,
bullets):
    if event.key == pygame.K_RIGHT:
        ship.moving_right = True
    elif event.key == pygame.K_LEFT:
        ship.moving_left = True
    elif event.key == pygame.K_SPACE:
        fire_bullet(ai_settings, screen, ship, bullets)


def fire_bullet(ai_settings, screen, ship, bullets):
    """如果还没有到达限制,就发射一颗子弹"""
    # 创建新子弹,并将其加入到编组bullets中
    if len(bullets) < ai_settings.bullets_allowed:
        new_bullet = Bullet(ai_settings, screen, ship)
        bullets.add(new_bullet)

(9)添加一个结束游戏的快捷键Q

python 复制代码
def check_keydown_events(event,ai_settings, screen, ship,
bullets):
    if event.key == pygame.K_RIGHT:
        ship.moving_right = True
    elif event.key == pygame.K_LEFT:
        ship.moving_left = True
    elif event.key == pygame.K_SPACE:
        fire_bullet(ai_settings, screen, ship, bullets)
    elif event.key == pygame.K_q:
        sys.exit()

四、创建外星人

1.创建Alien类

python 复制代码
import pygame
from pygame.sprite import Sprite

class Alien(Sprite):
    """表示单个外星人的类"""
    def __init__(self, ai_settings, screen):
        """初始化外星人并设置其起始位置"""
        super().__init__()
        self.screen = screen
        self.ai_settings = ai_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 blitme(self):
        """在指定位置绘制外星人"""
        self.screen.blit(self.image, self.rect)
        

2.创建Alien实例

python 复制代码
import pygame

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

run_game()

3.让外星人出现在屏幕上

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

4.创建一群外星人

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

python 复制代码
import pygame

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

run_game()

我们还需要修改update_screen()

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

5.创建外星人群

下面是新函数create_fleet()

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

(1)重构create_fleet()

下面是create_fleet()和两个新函数,get_number_aliens_x()和create_alien()。

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

(2)添加行

python 复制代码
def create_fleet(ai_settings, screen, ship,aliens):
    """创建外星人群"""
    # 创建一个外星人,并计算每行可容纳多少个外星人
    alien = Alien(ai_settings, screen)
    number_aliens_x = get_number_aliens_x(ai_settings,
                                          alien.rect.width)
    number_rows = get_number_rows(ai_settings, ship.rect.height,
                                  alien.rect.height)
    # 创建外星人群
    for row_number in range(number_rows):
        for alien_number in range(number_aliens_x):
            create_alien(ai_settings, screen, aliens,alien_number,row_number)

def get_number_rows(ai_settings, ship_height, alien_height):
    """计算屏幕可容纳多少行外星人"""
    available_space_y = (ai_settings.screen_height -(3 * alien_height) - ship_height)
    number_rows = int(available_space_y / (2 * alien_height))
    return number_rows
def create_alien(ai_settings, screen, aliens, alien_number,row_number):
    """创建一个外星人并将其放在当前行"""
    alien = Alien(ai_settings, screen)
    alien_width = alien.rect.width
    alien.x = alien_width + 2 * alien_width * alien_number
    alien.rect.x = alien.x
    alien.rect.y = alien.rect.height + 2 * alien.rect.height * row_number
    aliens.add(alien)

6.让外星人群移动

(1)向右移动外星人

我们将使用alien.py中的方法update(),且对外星人群中的每个外星人都调用它。

python 复制代码
def __init__(self):
    --snip--
    # 外星人设置
    self.alien_speed_factor = 1
python 复制代码
    def update(self):
        """向右移动外星人"""
        self.x += self.ai_settings.alien_speed_factor 
        self.rect.x = self.x

在主while循环中已调用了更新飞船和子弹的方法,但现在还需更新每个外星人的位置:

python 复制代码
#游戏开始的主循环
    while True:
        #监视键盘和鼠标
        gf.check_events(ai_settings, screen, ship, bullets)
        ship.update()
        gf.update_bullets(bullets)
        gf.update_aliens(aliens)
        gf.update_screen(ai_settings, screen, ship, aliens,
                         bullets)

最后,在文件game_functions.py末尾添加新函数update_aliens()

python 复制代码
def update_aliens(aliens):
    """更新外星人群中所有外星人的位置"""
    aliens.update()

我们对编组aliens调用方法update(),这将自动对每个外星人调用方法update()。

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

下面来创建让外星人撞到屏幕右边缘后向下移动、再向左移动的设置。

python 复制代码
# 外星人设置
self.alien_speed_factor = 1
self.fleet_drop_speed = 10
# fleet_direction为1表示向右移,为-1表示向左移
self.fleet_direction = 1

设置fleet_drop_speed指定了有外星人撞到屏幕边缘时,外星人群向下移动的速度。

(3)检查外星人是否撞到了屏幕边缘

需要编写一个方法来检查是否有外星人撞到了屏幕边缘,还需修改update(),以让每个外星人都沿正确的方向移动。

python 复制代码
    def update(self):
        """向左或向右移动外星人"""
        self.x += (self.ai_settings.alien_speed_factor * self.ai_settings.fleet_direction)
        self.rect.x = self.x

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

(4)向下移动外星人群并改变移动方向

需要对game_functions.py做重大修改,我们编写函数check_fleet_edges()和change_fleet_direction(),并对update_aliens()进行修改。

python 复制代码
def check_fleet_edges(ai_settings, aliens):
    """有外星人到达边缘时采取相应的措施"""
    for alien in aliens.sprites():
        if alien.check_edges():
            change_fleet_direction(ai_settings, aliens)
            break
def change_fleet_direction(ai_settings, aliens):
    """将整群外星人下移,并改变它们的方向"""
    for alien in aliens.sprites():
        alien.rect.y += ai_settings.fleet_drop_speed
        ai_settings.fleet_direction *= -1
def update_aliens(ai_settings, aliens):
    """检查是否有外星人位于屏幕边缘,并更新整群外星人的位置"""
    check_fleet_edges(ai_settings, aliens)
    aliens.update()

alien_invasion.py也需要进行相应修改

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

五、射杀外星人

我们将使用sprite.groupcollide()检测两个编组的成员之间的碰撞。

1.检测子弹与外星人的碰撞

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

在函数update_bullets()中,使用下面的代码来检查碰撞。

python 复制代码
def update_bullets(aliens, bullets):
    """更新子弹的位置,并删除已消失的子弹"""
    # 更新子弹的位置
    bullets.update()
    # 删除已消失的子弹
    for bullet in bullets.copy():
        if bullet.rect.bottom <= 0:
            bullets.remove(bullet)
    # 检查是否有子弹击中了外星人
    # 如果是这样,就删除相应的子弹和外星人
    collisions = pygame.sprite.groupcollide(bullets, aliens, True,
                                            True)

每当有子弹和外星人的rect重叠时,groupcollide()就在它返回的字典中添加一个键-值对。两个实参True告诉Pygame删除发生碰撞的子弹和外星人。

我们调用update_bullets()时,传递了实参aliens

python 复制代码
    while True:
        gf.check_events(ai_settings, screen, ship, bullets)
        ship.update()
        gf.update_bullets(aliens, bullets)
        gf.update_aliens(ai_settings, aliens)
        gf.update_screen(ai_settings, screen, ship, aliens,
                     bullets)

2.生成新的外星人群

首先需要检查编组aliens是否为空。如果为空,就调用create_fleet()。我们将在update_bullets()中执行这种检查,因为外星人都是在这里被消灭的。

python 复制代码
def update_bullets(ai_settings, screen, ship, aliens, bullets):
    --snip--
    # 检查是否有子弹击中了外星人
    # 如果是,就删除相应的子弹和外星人
    collisions = pygame.sprite.groupcollide(bullets, aliens, True,True)
    if len(aliens) == 0: 
        # 删除现有的子弹并新建一群外星人
        bullets.empty() 
        create_fleet(ai_settings, screen, ship, aliens)

我们需要更新alien_invasion.py中对update_bullets()的调用

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

3.重构update_bullets()

python 复制代码
def update_bullets(ai_settings, screen, ship, aliens, bullets):
    --snip--
    # 删除已消失的子弹
    for bullet in bullets.copy():
        if bullet.rect.bottom <= 0:
            bullets.remove(bullet)
    check_bullet_alien_collisions(ai_settings, screen, ship,aliens, bullets)
def check_bullet_alien_collisions(ai_settings, screen, ship,aliens, bullets):
    """响应子弹和外星人的碰撞"""
    # 删除发生碰撞的子弹和外星人
    collisions = pygame.sprite.groupcollide(bullets, aliens, True,True)
    if len(aliens) == 0:
        # 删除现有的所有子弹,并创建一个新的外星人群
        bullets.empty()
        create_fleet(ai_settings, screen, ship, aliens)

我们创建了一个新函数------check_bullet_alien_collisions(),以检测子弹和外星人之间的碰撞,以及在整群外星人都被消灭干净时采取相应的措施。这避免了update_bullets()太长。

六、结束游戏

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

我们在更新每个外星人的位置后立即检测外星人和飞船之间的碰撞。

python 复制代码
def update_aliens(ai_settings, ship, aliens):
    """检查是否有外星人到达屏幕边缘然后更新所有外星人的位置"""
    check_fleet_edges(ai_settings, aliens)
    aliens.update()
    # 检测外星人和飞船之间的碰撞
    if pygame.sprite.spritecollideany(ship, aliens): 
        print("Ship hit!!!")

方法spritecollideany()接受两个实参:一个sprite和一个编组。它检查编组是否有成员与sprite发生了碰撞,并在找到与sprite发生了碰撞的成员后就停止遍历编组。

如果没有发生碰撞,spritecollideany()将返回None,因此if代码块不会执行。如果找到了与飞船发生碰撞的外星人,它就返回这个外星人。

现在,我们需要将ship传递给update_aliens()

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

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

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

python 复制代码
class GameStats():
    """跟踪游戏的统计信息"""
    def __init__(self, ai_settings):
        """初始化统计信息"""
        self.ai_settings = ai_settings
        self.reset_stats() 
    def reset_stats(self):
        """初始化在游戏运行期间可能变化的统计信息"""
        self.ships_left = self.ai_settings.ship_limit

在这个游戏运行期间,我们只创建一个GameStats实例,但每当玩家开始新游戏时,需要重置一些统计信息。为此,我们在方法reset_stats()中初始化大部分统计信息,而不是在__init__()中直接初始化它们。

一开始玩家拥有的飞船数存储在settings.py的ship_limit中:

python 复制代码
# 飞船设置
self.ship_speed_factor = 1.5
self.ship_limit = 3

我们还需对alien_invasion.py做些修改,以创建一个GameStats实例

python 复制代码
--snip--
from settings import Settings
from game_stats import GameStats 
--snip--
def run_game():
    --snip--
    pygame.display.set_caption("Alien Invasion")
    # 创建一个用于存储游戏统计信息的实例
    stats = GameStats(ai_settings) 
    --snip--
    # 开始游戏主循环
    while True:
        --snip--
        gf.update_bullets(ai_settings, screen, ship, aliens,bullets)
        gf.update_aliens(ai_settings, stats, screen, ship, aliens,bullets) 
        --snip--

实现响应功能的大部分代码放到函数ship_hit()中

python 复制代码
import sys
from time import sleep 
import pygame
--snip--
def ship_hit(ai_settings, stats, screen, ship, aliens, bullets):
    """响应被外星人撞到的飞船"""
    # 将ships_left减1
    stats.ships_left -= 1 
    # 清空外星人列表和子弹列表
    aliens.empty() 
    bullets.empty()
    # 创建一群新的外星人,并将飞船放到屏幕底端中央
    create_fleet(ai_settings, screen, ship, aliens) 
    ship.center_ship()
    # 暂停
    sleep(0.5) 
def update_aliens(ai_settings, stats, screen, ship, aliens,bullets): 
    --snip--
    # 检测外星人和飞船碰撞
    if pygame.sprite.spritecollideany(ship, aliens):
        ship_hit(ai_settings, stats, screen, ship, aliens,bullets)
python 复制代码
def center_ship(self):
    """让飞船在屏幕上居中"""
    self.center = self.screen_rect.centerx

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

python 复制代码
def check_aliens_bottom(ai_settings, stats, screen, ship, aliens,bullets):
    """检查是否有外星人到达了屏幕底端"""
    screen_rect = screen.get_rect()
    for alien in aliens.sprites():
        if alien.rect.bottom >= screen_rect.bottom: 
            # 像飞船被撞到一样进行处理
            ship_hit(ai_settings, stats, screen, ship, aliens,bullets)
            break
def update_aliens(ai_settings, stats, screen, ship, aliens,bullets):
    --snip--
    # 检查是否有外星人到达屏幕底端
    check_aliens_bottom(ai_settings, stats, screen, ship, aliens,bullets) 

4.游戏结束

下面在GameStats中添加一个作为标志的属性game_active,以便在玩家的飞船用完后结束游戏

python 复制代码
def __init__(self, settings):
    --snip--
    # 游戏刚启动时处于活动状态
    self.game_active = True

现在在ship_hit()中添加代码,在玩家的飞船都用完后将game_active设置为False

python 复制代码
def ship_hit(ai_settings, stats, screen, ship, aliens, bullets):
    """响应飞船被外星人撞到"""
    if stats.ships_left > 0:    
        # 将ships_left减1
        stats.ships_left -= 1
        --snip--
         #暂停一会儿
        sleep(0.5)
    else:
        stats.game_active = False

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

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

python 复制代码
# 开始游戏主循环
while True:
    gf.check_events(ai_settings, screen, ship, bullets)
    if stats.game_active:
        ship.update()
        gf.update_bullets(ai_settings, screen, ship, aliens,bullets)
        gf.update_aliens(ai_settings, stats, screen, ship,aliens, bullets)
    gf.update_screen(ai_settings, screen, ship, aliens,bullets)

八、记分

1.添加Play按钮

当前,这个游戏在玩家运行alien_invasion.py时就开始了。下面让游戏一开始处于非活动状态,并提示玩家单击Play按钮来开始游戏。为此,在game_stats.py中输入如下代码

python 复制代码
def __init__(self, ai_settings):
    """初始化统计信息"""
    self.ai_settings = ai_settings
    self.reset_stats()
    # 让游戏一开始处于非活动状态
    self.game_active = False
def reset_stats(self):
    --snip--

(1)创建Button类

我们创建一个Button类,用于创建带标签的实心矩形。将这个类保存为文件button.py

python 复制代码
import pygame.font

class Button():
    def __init__(self, ai_settings, screen, msg): 
        """初始化按钮的属性"""
        self.screen = screen
        self.screen_rect = screen.get_rect()
        # 设置按钮的尺寸和其他属性
        self.width, self.height = 200, 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能够将文本渲染到屏幕上。Pygame通过将你要显示的字符串渲染为图像来处理文本。我们调用prep_msg()来处理这样的渲染。

python 复制代码
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以及要渲染为图像的文本。调用font.render()将存储在msg中的文本转换为图像,然后将该图像存储在msg_image中。方法font.render()还接受一个布尔实参,该实参指定开启还是关闭反锯齿功能。余下的两个实参分别是文本颜色和背景色。我们启用了反锯齿功能,并将文本的背景色设置为按钮的颜色。

我们让文本图像在按钮上居中:根据文本图像创建一个rect,并将其center属性设置为按钮的center属性。最后,我们创建方法draw_button(),通过调用它可将这个按钮显示到屏幕上

python 复制代码
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对象,从而在屏幕上绘制文本图像。

(2)在屏幕上绘制按钮

python 复制代码
--snip--
from game_stats import GameStats
from button import Button
--snip--
def run_game():
    --snip--
    pygame.display.set_caption("Alien Invasion")
    # 创建Play按钮
    play_button = Button(ai_settings, screen, "Play") 
    --snip--
    # 开始游戏主循环
    while True:
        --snip--
        gf.update_screen(ai_settings, screen, stats, ship, aliens,bullets, play_button)

run_game()

接下来,修改update_screen(),以便在游戏处于非活动状态时显示Play按钮.

python 复制代码
def update_screen(ai_settings, screen, stats, ship, aliens,bullets,play_button):
    """更新屏幕上的图像,并切换到新屏幕"""
    --snip--
    # 如果游戏处于非活动状态,就绘制Play按钮
    if not stats.game_active:
        play_button.draw_button()
    # 让最近绘制的屏幕可见
    pygame.display.flip()

(3)开始游戏

为在玩家单击Play按钮时开始新游戏,需在game_functions.py中添加如下代码,以监视与这个按钮相关的鼠标事件

python 复制代码
def check_events(ai_settings, screen, stats, play_button, ship,bullets):
    """响应按键和鼠标事件"""
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            --snip--
        elif event.type == pygame.MOUSEBUTTONDOWN: 
            mouse_x, mouse_y = pygame.mouse.get_pos() 
            check_play_button(stats, play_button, mouse_x,mouse_y)

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

在alien_invasion.py中调用check_events(),需要传递另外两个实参------stats和play_button

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

(4)重置游戏

为在玩家每次单击Play按钮时都重置游戏,需要重置统计信息、删除现有的外星人和子弹、创建一群新的外星人,并让飞船居中,如下所示

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

check_events()的定义需要修改,调用check_play_button()的代码亦如此

python 复制代码
def check_events(ai_settings, screen, stats, play_button, ship,aliens,bullets):
    """响应按键和鼠标事件"""
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
        --snip--
    elif event.type == pygame.MOUSEBUTTONDOWN:
        mouse_x, mouse_y = pygame.mouse.get_pos()
        check_play_button(ai_settings, screen, stats,play_button, ship, aliens, bullets, mouse_x, mouse_y)

下面来修改alien_invasion.py中调用check_events()的代码,以将实参aliens传递给它

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

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

当前,Play按钮存在一个问题,那就是即便Play按钮不可见,玩家单击其原来所在的区域时,游戏依然会作出响应。为修复这个问题,可让游戏仅在game_active为False时才开始.

python 复制代码
def check_play_button(ai_settings, screen, stats, play_button,ship, aliens,bullets,mouse_x, mouse_y):
    """玩家单击Play按钮时开始新游戏"""
    button_clicked = play_button.rect.collidepoint(mouse_x,mouse_y) 
    if button_clicked and not stats.game_active: 
        #重置游戏统计信息
        --snip--

标志button_clicked的值为True或False,仅当玩家单击了Play按钮且游戏当前处于非活动状态时,游戏才重新开始。

(6)隐藏光标

我们在游戏处于活动状态时让光标不可见

python 复制代码
def check_play_button(ai_settings, screen, stats, play_button,ship, aliens,bullets, mouse_x, mouse_y):
    """在玩家单击Play按钮时开始新游戏"""
    button_clicked = play_button.rect.collidepoint(mouse_x,mouse_y)
    if button_clicked and not stats.game_active:
        # 隐藏光标
        pygame.mouse.set_visible(False)
        --snip--

通过向set_visible()传递False,让Pygame在光标位于游戏窗口内时将其隐藏起来。游戏结束后,我们将重新显示光标,让玩家能够单击Play按钮来开始新游戏。

python 复制代码
def ship_hit(ai_settings, screen, stats, ship, aliens, bullets):
    """响应飞船被外星人撞到"""
    if stats.ships_left > 0:
        --snip--
    else:
        stats.game_active = False
        pygame.mouse.set_visible(True)

2.提高等级

(1)修改速度设置

我们首先重新组织Settings类,将游戏设置划分成静态的和动态的两组。对于随着游戏进行而变化的设置,我们还确保它们在开始新游戏时被重置。

python 复制代码
def __init__(self):
    """初始化游戏的静态设置"""
    # 屏幕设置
    self.screen_width = 1200
    self.screen_height = 800
    self.bg_color = (230, 230, 230)
    # 飞船设置
    self.ship_limit = 3
    # 子弹设置
    self.bullet_width = 3
    self.bullet_height = 15
    self.bullet_color = 60, 60, 60
    self.bullets_allowed = 3
    # 外星人设置
    self.fleet_drop_speed = 10
    # 以什么样的速度加快游戏节奏
    self.speedup_scale = 1.1 
    self.initialize_dynamic_settings() 

initialize_dynamic_settings()的代码如下

python 复制代码
def initialize_dynamic_settings(self):
    """初始化随游戏进行而变化的设置"""
    self.ship_speed_factor = 1.5
    self.bullet_speed_factor = 3
    self.alien_speed_factor = 1
    # fleet_direction为1表示向右;为-1表示向左
    self.fleet_direction = 1

每当玩家提高一个等级时,我们都使用increase_speed()来提高飞船、子弹和外星人的速度

python 复制代码
def increase_speed(self):
    """提高速度设置"""
    self.ship_speed_factor *= self.speedup_scale
    self.bullet_speed_factor *= self.speedup_scale
    self.alien_speed_factor *= self.speedup_scale

在check_bullet_alien_collisions()中,我们在整群外星人都被消灭后调用increase_speed()来加快游戏的节奏,再创建一群新的外星人

python 复制代码
def check_bullet_alien_collisions(ai_settings, screen, ship,aliens, bullets):
    --snip--
    if len(aliens) == 0:
        # 删除现有的子弹,加快游戏节奏,并创建一群新的外星人
        bullets.empty()
        ai_settings.increase_speed()
        create_fleet(ai_settings, screen, ship, aliens)

(2)重置速度

python 复制代码
def check_play_button(ai_settings, screen, stats, play_button,ship, aliens,bullets, mouse_x, mouse_y):
    """在玩家单击Play按钮时开始新游戏"""
    button_clicked = play_button.rect.collidepoint(mouse_x,mouse_y)
    if button_clicked and not stats.game_active:
        # 重置游戏设置
        ai_settings.initialize_dynamic_settings()
        # 隐藏光标
        pygame.mouse.set_visible(False)
        --snip--

3.记分

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

python 复制代码
class GameStats():
    --snip--
    def reset_stats(self):
        """初始化随游戏进行可能变化的统计信息"""
        self.ships_left = self.ai_settings.ship_limit
        self.score = 0

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

(1)显示得分

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

python 复制代码
import pygame.font

class Scoreboard():
    """显示得分信息的类"""
    def __init__(self, ai_settings, screen, stats): 
        """初始化显示得分涉及的属性"""
        self.screen = screen
        self.screen_rect = screen.get_rect()
        self.ai_settings = ai_settings
        self.stats = stats
        # 显示得分信息时使用的字体设置
        self.text_color = (30, 30, 30) 
        self.font = pygame.font.SysFont(None, 48) 
        # 准备初始得分图像
        self.prep_score() 

为将要显示的文本转换为图像,我们调用了prep_score()

python 复制代码
def prep_score(self):
    """将得分转换为一幅渲染的图像"""
    score_str = str(self.stats.score) 
    self.score_image = self.font.render(score_str, True,self.text_color,self.ai_settings.bg_color)
    # 将得分放在屏幕右上角
    self.score_rect = self.score_image.get_rect() 
    self.score_rect.right = self.screen_rect.right - 20 
    self.score_rect.top = 20 

最后,我们创建方法show_score(),用于显示渲染好的得分图像

python 复制代码
def show_score(self):
    """在屏幕上显示得分"""
    self.screen.blit(self.score_image, self.score_rect)

(2)创建记分牌

为显示得分,我们在alien_invasion.py中创建一个Scoreboard实例

python 复制代码
--snip--
from game_stats import GameStats
from scoreboard import Scoreboard
--snip--
def run_game():
    --snip--
    # 创建存储游戏统计信息的实例,并创建记分牌
    stats = GameStats(ai_settings)
    sb = Scoreboard(ai_settings, screen, stats) 
    --snip--
    # 开始游戏主循环
    while True:
        --snip--
        gf.update_screen(ai_settings, screen, stats, sb, ship,aliens, bullets, play_button)

run_game()

为显示得分,将update_screen()修改成下面这样

python 复制代码
def update_screen(ai_settings, screen, stats, sb, ship, aliens,bullets,play_button):
    --snip--
    # 显示得分
    sb.show_score()
    # 如果游戏处于非活动状态,就显示Play按钮
    if not stats.game_active:
        play_button.draw_button()
    # 让最近绘制的屏幕可见
    pygame.display.flip()

(3)在外星人被消灭时更新得分

python 复制代码
def initialize_dynamic_settings(self):
    --snip--
    # 记分
    self.alien_points = 50

在check_bullet_alien_collisions()中,每当有外星人被击落时,都更新得分

python 复制代码
def check_bullet_alien_collisions(ai_settings, screen, stats, sb,ship,aliens, bullets):
    """响应子弹和外星人发生碰撞"""
    # 删除发生碰撞的子弹和外星人
    collisions = pygame.sprite.groupcollide(bullets, aliens, True,True)
    if collisions:
        stats.score += ai_settings.alien_points 
        sb.prep_score()
        --snip--

我们需要修改update_bullets(),确保在函数之间传递合适的实参

python 复制代码
def update_bullets(ai_settings, screen, stats, sb, ship, aliens,bullets):
    """更新子弹的位置,并删除已消失的子弹"""
    --snip--
    check_bullet_alien_collisions(ai_settings, screen, stats, sb,ship, aliens, bullets)

我们还需要修改主while循环中调用update_bullets()的代码

python 复制代码
# 开始游戏主循环
while True:
    gf.check_events(ai_settings, screen, stats, play_button,ship,aliens, bullets)
    if stats.game_active:
        ship.update()
        gf.update_bullets(ai_settings, screen, stats, sb,ship, aliens,bullets)
        --snip--

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

我们的代码可能遗漏了一些被消灭的外星人。例如,如果在一次循环中有两颗子弹射中了外星人,或者因子弹更宽而同时击中了多个外星人,玩家将只能得到一个被消灭的外星人的点数。

在check_bullet_alien_collisions()中,与外星人碰撞的子弹都是字典collisions中的一个键;而与每颗子弹相关的值都是一个列表,其中包含该子弹撞到的外星人。我们遍历字典collisions,确保将消灭的每个外星人的点数都记入得分

python 复制代码
def check_bullet_alien_collisions(ai_settings, screen, stats, sb,ship,aliens, bullets):
    --snip--
    if collisions:
        for aliens in collisions.values(): 
            stats.score += ai_settings.alien_points * len(aliens)
            sb.prep_score()
            --snip--

(5)提高点数

玩家每提高一个等级,游戏都变得更难,因此处于较高的等级时,外星人的点数应更高。

python 复制代码
class Settings():
    """存储游戏《外星人入侵》的所有设置的类"""
    def __init__(self):
        --snip--
        # 加快游戏节奏的速度
        self.speedup_scale = 1.1
        # 外星人点数的提高速度
        self.score_scale = 1.5 
        self.initialize_dynamic_settings()
    def increase_speed(self):
        """提高速度设置和外星人点数"""
        self.ship_speed_factor *= self.speedup_scale
        self.bullet_speed_factor *= self.speedup_scale
        self.alien_speed_factor *= self.speedup_scale
        self.alien_points = int(self.alien_points *self.score_scale) 

(6)将得分圆整

将得分显示为10的整数倍,还将设置得分的格式,在大数字中添加用逗号表示的千位分隔符。我们在Scoreboard中执行这种修改

python 复制代码
def prep_score(self):
    """将得分转换为渲染的图像"""
    rounded_score = int(round(self.stats.score, -1)) 
    score_str = "{:,}".format(rounded_score) 
    self.score_image = self.font.render(score_str, True,self.text_color, self.ai_settings.bg_color)
    --snip--

函数round()通常让小数精确到小数点后多少位,其中小数位数是由第二个实参指定的。然而,如果将第二个实参指定为负数,round()将圆整到最近的10、100、1000等整数倍。使用了一个字符串格式设置指令,它让Python将数值转换为字符串时在其中插入逗号。

(7)最高得分

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

python 复制代码
def __init__(self, ai_settings):
    --snip--
    # 在任何情况下都不应重置最高得分
    self.high_score = 0

下面来修改Scoreboard以显示最高得分。先来修改方法__init__()

python 复制代码
def __init__(self, ai_settings, screen, stats):
    --snip--
    # 准备包含最高得分和当前得分的图像
    self.prep_score()
    self.prep_high_score() 

方法prep_high_score()的代码如下

python 复制代码
def prep_high_score(self):
    """将最高得分转换为渲染的图像"""
    high_score = int(round(self.stats.high_score, -1)) 
    high_score_str = "{:,}".format(high_score) 
    self.high_score_image = self.font.render(high_score_str,True, self.text_color, self.ai_settings.bg_color)
    #将最高得分放在屏幕顶部中央
    self.high_score_rect = self.high_score_image.get_rect()
    self.high_score_rect.centerx = self.screen_rect.centerx 
    self.high_score_rect.top = self.score_rect.top 
python 复制代码
def show_score(self):
    """在屏幕上显示当前得分和最高得分"""
    self.screen.blit(self.score_image, self.score_rect)
    self.screen.blit(self.high_score_image,self.high_score_rect)

为检查是否诞生了新的最高得分,我们在game_functions.py中添加一个新函数check_high_score()

python 复制代码
def check_high_score(stats, sb):
    """检查是否诞生了新的最高得分"""
    if stats.score > stats.high_score: 
        stats.high_score = stats.score
        sb.prep_high_score()

在check_bullet_alien_collisions()中,每当有外星人被消灭,都需要在更新得分后调用check_high_score()

python 复制代码
def check_bullet_alien_collisions(ai_settings, screen, stats, sb,ship,aliens, bullets):
    --snip--
    if collisions:
        for aliens in collisions.values():
            stats.score += ai_settings.alien_points * len(aliens)
            sb.prep_score()
        check_high_score(stats, sb)
    --snip--

(8)显示等级

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

python 复制代码
def reset_stats(self):
    """初始化随游戏进行可能变化的统计信息"""
    self.ships_left = self.ai_settings.ship_limit
    self.score = 0
    self.level = 1

为让Scoreboard能够在当前得分下方显示当前等级,我们在__init__()中调用了一个新方法prep_level()

python 复制代码
def __init__(self, ai_settings, screen, stats):
    --snip--
    # 准备包含得分的初始图像
    self.prep_score()
    self.prep_high_score()
    self.prep_level()

prep_level()的代码如下

python 复制代码
def prep_level(self):
    """将等级转换为渲染的图像"""
    self.level_image = self.font.render(str(self.stats.level),True, self.text_color, self.ai_settings.bg_color)
    # 将等级放在得分下方
    self.level_rect = self.level_image.get_rect()
    self.level_rect.right = self.score_rect.right 
    self.level_rect.top = self.score_rect.bottom + 10

我们还需要更新show_score()

python 复制代码
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()中提高等级,并更新等级图像

python 复制代码
def check_bullet_alien_collisions(ai_settings, screen, stats, sb,ship,aliens, bullets):
    --snip--
    if len(aliens) == 0:
        # 如果整群外星人都被消灭,就提高一个等级
        bullets.empty()
        ai_settings.increase_speed()
        # 提高等级
        stats.level += 1 
        sb.prep_level() 
        create_fleet(ai_settings, screen, ship, aliens)

为确保开始新游戏时更新记分和等级图像,在按钮Play被单击时触发重置

python 复制代码
def check_play_button(ai_settings, screen, stats, sb, play_button,ship,aliens, bullets, mouse_x, mouse_y):
    """在玩家单击Play按钮时开始新游戏"""
    button_clicked = play_button.rect.collidepoint(mouse_x,mouse_y)
    if button_clicked and not stats.game_active:
        --snip--
        # 重置游戏统计信息
        stats.reset_stats()
        stats.game_active = True
        # 重置记分牌图像
        sb.prep_score() 
        sb.prep_high_score()
        sb.prep_level()
        # 清空外星人列表和子弹列表
        aliens.empty()
        bullets.empty()
        --snip--

在check_events()中,现在需要向check_play_button()传递sb,让它能够访问记分牌对象

python 复制代码
def check_events(ai_settings, screen, stats, sb, play_button,ship, aliens,bullets):
    """响应按键和鼠标事件"""
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            --snip--
        elif event.type == pygame.MOUSEBUTTONDOWN:
            mouse_x, mouse_y = pygame.mouse.get_pos()
            check_play_button(ai_settings, screen, stats, sb,play_button, ship, aliens, bullets, mouse_x, mouse_y)

最后,更新alien_invasion.py中调用check_events()的代码,也向它传递sb

python 复制代码
# 开始游戏主循环
while True:
    gf.check_events(ai_settings, screen, stats, sb,play_button, ship,aliens, bullets)
    --snip--

(9)显示余下的飞船数

首先,需要让Ship继承Sprite,以便能够创建飞船编组

python 复制代码
import pygame
from pygame.sprite import Sprite

class Ship(Sprite): 
    def __init__(self, ai_settings, screen):
        """初始化飞船,并设置其起始位置"""
        super(Ship, self).__init__() 
        --snip--

接下来,需要修改Scoreboard,在其中创建一个可供显示的飞船编组。下面是其中的import语句和方法__init__()

python 复制代码
import pygame.font
from pygame.sprite import Group
from ship import Ship

class Scoreboard():
    """报告得分信息的类"""
    def __init__(self, ai_settings, screen, stats):
        --snip--
        self.prep_level()
        self.prep_ships()
        --snip--

prep_ships()的代码如下

python 复制代码
def prep_ships(self):
    """显示还余下多少艘飞船"""
    self.ships = Group() 
    for ship_number in range(self.stats.ships_left): 
        ship = Ship(self.ai_settings, self.screen)
        ship.rect.x = 10 + ship_number * ship.rect.width 
        ship.rect.y = 10 
        self.ships.add(ship)

现在需要在屏幕上绘制飞船了

python 复制代码
def show_score(self):
    --snip--
    self.screen.blit(self.level_image, self.level_rect)
    # 绘制飞船
    self.ships.draw(self.screen)

为在游戏开始时让玩家知道他有多少艘飞船,我们在开始新游戏时调用prep_ships()。这是在game_functions.py的check_play_button()中进行的

python 复制代码
def check_play_button(ai_settings, screen, stats, sb, play_button,ship,aliens, bullets, mouse_x, mouse_y):
    """在玩家单击Play按钮时开始新游戏"""
    button_clicked = play_button.rect.collidepoint(mouse_x,mouse_y)
    if button_clicked and not stats.game_active:
        --snip--
        # 重置记分牌图像
        sb.prep_score()
        sb.prep_high_score()
        sb.prep_level()
        sb.prep_ships()
        --snip--

我们还在飞船被外星人撞到时调用prep_ships(),从而在玩家损失一艘飞船时更新飞船图像

python 复制代码
def update_aliens(ai_settings, screen, stats, sb, ship, aliens,bullets): 
    --snip--
    # 检测外星人和飞船之间的碰撞
    if pygame.sprite.spritecollideany(ship, aliens):
        ship_hit(ai_settings, screen, stats, sb, ship, aliens,bullets) 
        # 检查是否有外星人抵达屏幕底端
        check_aliens_bottom(ai_settings, screen, stats, sb, ship,aliens, bullets) 

def ship_hit(ai_settings, screen, stats, sb, ship, aliens,bullets): 
    """响应被外星人撞到的飞船"""
    if stats.ships_left > 0:
        # 将ships_left减1
        stats.ships_left -= 1
        # 更新记分牌
        sb.prep_ships() 
        # 清空外星人列表和子弹列表
        --snip--

在check_aliens_bottom()中需要调用ship_hit(),因此对这个函数进行更新

python 复制代码
def check_aliens_bottom(ai_settings, screen, stats, sb, ship,aliens,bullets):
    """检查是否有外星人抵达屏幕底端"""
    screen_rect = screen.get_rect()
    for alien in aliens.sprites():
        if alien.rect.bottom >= screen_rect.bottom:
            # 像飞船被外星人撞到一样处理
            ship_hit(ai_settings, screen, stats, sb, ship, aliens,bullets)
            break

最后,在alien_invasion.py中修改调用update_aliens()的代码,向它传递实参sb

python 复制代码
# 开始游戏主循环
while True:
    --snip--
    if stats.game_active:
        ship.update()
        gf.update_bullets(ai_settings, screen, stats, sb,ship, aliens,bullets)
        gf.update_aliens(ai_settings, screen, stats, sb, ship,aliens,bullets)
        --snip--

九、源码来源

Python编程:从入门到实践 (ituring.com.cn)星途辛某人/pydemo1 - 码云 - 开源中国 (gitee.com)GitHub - 11xy11/pydemo1

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