本章将结束《外星人入侵》这个游戏的开发。我们会添加一个play按钮,用于根据需要启动游戏以及重启游戏,还会修改这个预习,使其根据玩家等级提高难度,并实现一个计分系统。
14.1 添加play按钮
本节将添加一个play按钮,它在游戏开始前出现,并在游戏结束后再次出现,让玩家能够开始新游戏。

下面让游戏一开始处于非活动状态,并提示玩家单机play按钮来开始游戏。
def run_game():
    pygame.init()
    ai_settings = Text_settings()
    stats = GameStats(ai_settings)
    screen = pygame.display.set_mode(
        (ai_settings.screen_width, ai_settings.screen_height))

    pygame.display.set_caption("My Game")
    ship = Ship(ai_settings, screen)
    bullets = Group()
    aliens = Group()
    test_game_function.create_fleet(ai_settings, screen, ship, aliens)
    
    #让游戏一开始处于非活动状态
    stats.game_active = False
14.1.1 创建button类(胭惜雨:button原意是纽扣、扣子,互联网语境下表示按钮)
由于pygame没有内置创建按钮的方法,我们将编写一个button类,用于创建带标签的实心矩形。

import pygame.font


class Button:
    def __init__(self, screen, msg):
        self.screen = screen
        self.screen_rect = self.screen.get_rect()

        # 设置按钮的尺寸和其他属性
        self.width, self.height = 200, 50
        self.button_color = (255,153,0)
        self.text_color = (0, 0, 0)
        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能够将文本渲染到屏幕上。方法__inint__()接受参数self、对象ai_game和msg,其中msg是要在按钮中显示的文本。设置按钮的尺寸,再通过设置button_color,让按钮的rect对象变为对应的颜色,并通过设置text_color让文本为黑色。

在font.SysFont处制定用什么字体来渲染文本。实参None让pygame使用默认字体,而48指定了文本的自豪。为让按钮在屏幕上居中,创建一个表示按钮的rect对象,并将其center属性设置为屏幕的center属性。

pygame处理文本的方式是,将要显示的字符串渲染为图像。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()还接受一个布尔实参,该实参指定开启还是关闭反锯齿功能(反锯齿让文本的边缘更平滑)。余下两个实参分别是文本颜色和背景色。

最后,创建方法draw_button(),用于将这个按钮显示到屏幕上:

    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便创建好了。
14.1.2 在屏幕上绘制按钮
首先,在主文本上更新import语句:
from test_button import Button

其次,在AlienInvasion类的方法__init__()中创建它。可将这些代码放到方法__init__()的末尾:
def run_game():
    pygame.init()
    ai_settings = Text_settings()
    stats = GameStats(ai_settings)
    screen = pygame.display.set_mode(
        (ai_settings.screen_width, ai_settings.screen_height))

    # 创建play按钮
    play_button = Button(screen,"play game")

这些代码创建一个标签为play的Button实例,但没有将它显示到屏幕上。为显示该按钮,在_update_screen()中对齐调用方法draw_button():
def update_screen(ai_settings, screen, ship, bullets, aliens,stats,play_button):
    """更新屏幕上的图像,并切换到新屏幕"""
    screen.fill(ai_settings.bg_color)
    # 重绘所有子弹
    for bullet in bullets.sprites():
        bullet.draw_bullet()
    aliens.draw(screen)
    # 如果游戏处于非活动状态,就绘制play按钮
    if not stats.game_active:
        play_button.draw_button()
    ship.blitme()
    pygame.display.flip()

为让play按钮位于其他所有屏幕元素上面,在绘制其他所有游戏元素后再绘制这个按钮,然后切换到新屏幕。将这些代码放在一个if代码块中,让按钮仅在游戏处于非活动状态时才出现。
14.1.3 开始游戏
为在玩家单机Play按钮时开始新游戏,在_check_events()末尾添加如下elif代码块,以见识与该按钮相关的鼠标事件:
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)
        
        elif event.type == pygame.MOUSEBUTTONDOWN:
            mouse_pos = pygame.mouse.get_pos()
            check_play_button(mouse_pos)

无论玩家单机屏幕的什么地方,pygame都将检测到一个MOUSEBUTTONDOWN事件,但我们只想让这个游戏在玩家用鼠标单击play时做出响应。为此,使用了pygame.mouse.get_pos(),它返回一个元组,其中包含玩家单击时鼠标的x坐标和y坐标。我们将这些值传递给新方法check_play_button()。

方法check_play_button()的代码如下,将它放在check_events()后面,同时check_events()也要进行修改:

def check_events(stats,play_button,aliens,bullets,ai_settings,screen,ship):
    """响应按键和鼠标事件"""
    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)

        elif event.type == pygame.MOUSEBUTTONDOWN:
            mouse_pos = pygame.mouse.get_pos()
            check_play_button(stats,play_button,aliens,bullets,ai_settings,screen,ship,mouse_pos)

def check_play_button(stats,play_button,aliens,bullets,ai_settings,screen,ship,mouse_pos):
    """玩家单击play时开始游戏"""
    if play_button.rect.collidepoint(mouse_pos):
        # 重置游戏统计信息
        stats.reset_stats()
        stats.game_active = True

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

        # 创建一群新的外星人并让飞船居中
        create_fleet(ai_settings, screen, ship, aliens)
        ship.center_ship()

这里使用了rect的方法collidepoint()检查鼠标单机位置是否在Play按钮的rect内。如果是,就将game_active设置为True,让游戏开始!

至此,现在应该能够开始这个游戏了。游戏结束时,应该game_active设置为False,并重新显示Play按钮。
14.1.5  将Play按钮切换到非活动状态
当前存在一个问题:即便play按钮不可见,玩家单击其所在的区域时,游戏依然会做出响应。游戏开始后,如果玩家不小心单击了Play按钮所在的区域,游戏将重新开始。

为修复这个问题,可让游戏仅在game_active为False时才开始:
def check_play_button(
        stats,
        play_button,
        aliens,
        bullets,
        ai_settings,
        screen,
        ship,
        mouse_pos):
    """玩家单击play时开始游戏"""
    button_clicked = play_button.rect.collidepoint(mouse_pos)
    
    if button_clicked and not stats.game_active:
        # 重置游戏统计信息
        stats.reset_stats()
        stats.game_active = True

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

        # 创建一群新的外星人并让飞船居中
        create_fleet(ai_settings, screen, ship, aliens)
        ship.center_ship()

标志button_clicked的值为True或False。仅当玩家单机了play按钮且游戏当前处于非活动状态时,游戏才重新开始。
14.1.6 隐藏鼠标光标
为让玩家开始游戏,要让鼠标光标可见,但是游戏开始后,光标只会添乱。为修复这种问题,需要在游戏处于活动状态时使其不可见。可在方法check_play_button()末尾的if代码块中完成这项任务:
    if button_clicked and not stats.game_active:
        # 重置游戏统计信息
        stats.reset_stats()
        stats.game_active = True

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

        # 创建一群新的外星人并让飞船居中
        create_fleet(ai_settings, screen, ship, aliens)
        ship.center_ship()
        
        #隐藏光标
        pygame.mouse.set_visible(False)

通过想set_visible()传递False,让pygame在光标位于游戏窗口内将其隐藏起来。

游戏结束后,将重新显示光标,让玩家能够单击Play按钮来开始新游戏。相关的代码如下:
def ship_hit(ai_settings, stats, screen, ship, aliens, bullets):
    """响应被外星人撞到的飞船"""
    if stats.ships_left > 0:
        # 将ship_left 减1
        stats.ships_left -= 1

        # 清空外星人列表和子弹列表
        aliens.empty()
        bullets.empty()

        # 创建一群新的外星人,并将飞船放到屏幕低端中央
        create_fleet(ai_settings, screen, ship, aliens)
        ship.center_ship()

        # 暂停
        sleep(0.5)

    else:
        print("给你机会你不中用啊!")
        stats.game_active = False
        pygame.mouse.set_visible(True)

在ship_hit()中,在游戏进入非活动状态后,立即让光标可见。关注这样的细节让游戏显得更专业,也让玩家能够专注于玩游戏而不是费力理解用户界面。
14.2 提高等级
将整群外星人小妹干净后,玩家将提高一个等级,但游戏的难度没变。下面来增加一点趣味性,每当玩家将屏幕上的外星人消灭干净后,都将加快游戏的节奏。

14.2.1 修改速度设置
import pygame
from pygame.sprite import Group

class Text_settings():
    """所有设置"""

    def __init__(self):
        """初始化游戏的静态设置"""
        self.bg_color = (230, 230, 230)
        self.screen_width = 1200
        self.screen_height = 800
        self.screen = pygame.display.set_mode((self.screen_width, self.screen_height))
        self.ship_speed_factor = 1
        self.bullet_speed_factor = 2
        self.bullet_width = 3
        self.bullet_height = 15
        self.bullet_color = (60, 60, 60)
        self.bullet_allowed = 100
        self.alien_speed = float(0.5)
        self.fleet_drop_speed = 50
        #fleet_direction 为1 表示向右移,为-1表示向左移
        self.fleet_direction = 1
        self.ship_limit = 3
        
        # 加快游戏节奏的速度
        self.speedup_scale = 1.1
        self.initialize_dynamic_settings()
        
    def initialize_dynamic_settings(self):
        """初始化随游戏进行变化的设置"""
        self.ship_speed_factor = 1.5
        self.bullet_speed_factor = 3.0
        self.alien_speed = 1.0
        
        #fleet_direction 为1 表示向右移,为-1表示向左移
        self.fleet_direction = 1

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

这个方法设置飞船、子弹和外星人的初始速度。随着游戏的进行,将提高这些速度。每当玩家开始新游戏时,都将重置这些速度。在这个方法中,还设置了fleet_speed的值,因为外星人移动的速度越快,到达屏幕底端所需的时间越短。

为在玩家等级提高时提高飞船、子弹和外星人的速度,编写一个名叫increase_speed()的新方法:
    def increase_speed(self):
        """提高速度设置"""
        self.ship_speed_factor *= self.speedup_scale
        self.bullet_speed_factor *= self.speedup_scale
        self.alien_speed *= self.speedup_scale

为提高这些游戏元素的速度,将每个速度设置都乘以speedup_scale的值。

在check_bullet_alien_collisions()中,在整群外星人都被消灭后调用increase_speed()来加快游戏的节奏:
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)
        ai_settings.increase_speed()

通过修改速度设置ship_speed、alien_speed和bullet_speed的值,足以加快整个游戏的节奏!
14.2.2 重置速度
每当玩家开始新游戏时,都需要将发生了变化的设置重置为初始值,否则新游戏开始时,速度设置将为前一次提高后的值:
def check_play_button(
        stats,
        play_button,
        aliens,
        bullets,
        ai_settings,
        screen,
        ship,
        mouse_pos):
    """玩家单击play时开始游戏"""
    button_clicked = play_button.rect.collidepoint(mouse_pos)

    if button_clicked and not stats.game_active:
        # 重置游戏统计信息
        ai_settings.initialize_dynamic_settings()
        stats.reset_stats()
        stats.game_active = True
14.3 记分
下面来实现一个记分系统,以实时跟踪玩家的得分,并显示最高得分、等级和余下的飞船数。

得分是游戏的一向统计信息,因此在GameStats中添加一个score属性:
    def reset_stats(self):
        """初始化在游戏运行期间可能变化的统计信息"""
        self.ships_left = self.ai_settings.ship_limit
        self.score = 0

为在每次开始游戏时都重置得分,我们在reset_stats()而不是__init__()中初始化score。
14.3.1 显示得分
为在屏幕上显示得分,首先创建一个新类Scoreboard。当前,这个类只显示当前的分,但后面也将使用它来显示最高得分、等级和余下的飞船数。下面是这个类的前半部分。
import pygame.font


class Scoreboard:
    """显示得分信息的类"""

    def __init__(self, screen, ai_settings, stats):
        """初始化显示得分涉及的属性"""
        self.screen = screen
        self.screen_rect = self.screen.get_rect()
        self.settings = ai_settings
        self.stats = stats

        # 显示得分信息是使用的字体设置
        self.text_color = (30, 30, 30)
        self.font = pygame.font.SysFont(None, 48)
        # 准备初始化得分图像
        self.prep_score()

由于Scoreboard在屏幕上显示文本,首先要导入模块pygame.font。接下来,在__init__()中包含形参screen,ai_settings,stats。然后,设置文本颜色,并实例化一个字体对象。

为将要显示的文本转换为图像,调用prep_score(),其定义如下:
    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(),用于显示渲染好的得分图像。
    def show_score(self):
        """在屏幕上显示得分"""
        self.screen.blit(self.score_image,self.score_rect)
14.3.2 创建记分牌
为显示得分,在AlienInvasion中创建一个Scoreboard实例。先来更新import语句:
from test_scoreboard import Scoreboard

接下来,在方法__init__()中创建一个Scoreboard实例:
    sb = Scoreboard(screen,ai_settings,stats)def run_game():
    pygame.init()
    ai_settings = Text_settings()
    # 创建存储游戏统计信息的实例,并创建记分牌
    stats = GameStats(ai_settings)
    sb = Scoreboard(screen,ai_settings,stats)

然后,在update_screen中奖记分牌绘制到屏幕上:
    def update_screen(
        ai_settings,
        screen,
        ship,
        bullets,
        aliens,
        stats,
        play_button,sb):
    """更新屏幕上的图像,并切换到新屏幕"""
    screen.fill(ai_settings.bg_color)
    # 重绘所有子弹
    for bullet in bullets.sprites():
        bullet.draw_bullet()
    aliens.draw(screen)
    # 显示得分
    sb.show_score()
14.3.3 在外星人被消灭时更新得分
为在屏幕上实时显示得分,每当有外星人被击中时,都将更新stats.score的值,在调用prep_score()更新得分图像。但在此之前,需要制定玩家每击落一个外星人将得到多少分:
    def initialize_dynamic_settings(self):
        """初始化随游戏进行变化的设置"""
        self.ship_speed_factor = 1.5
        self.bullet_speed_factor = 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()中,每当有外星人被击落时,都更新得分:

def update_bullet(ai_settings, screen, ship, aliens, bullets,stats):
    # 检查子弹位置,并删除多余子弹
    for bullet in bullets.copy():
        if bullet.rect.bottom <= 20:
            bullets.remove(bullet)
    check_bullet_alien_collisions(ai_settings, screen, ship, aliens, bullets,stats)


def check_bullet_alien_collisions(ai_settings, screen, ship, aliens, bullets,stats):
    """响应子弹和外星人碰撞"""
    # 检查是否有子弹击中了外星人
    # 删除发生碰撞的子弹和外星人
    collisions = pygame.sprite.groupcollide(bullets, aliens, True, True)
    if collisions:
        stats.score += ai_settings.alien_points
    if len(aliens) == 0:
        # 删除现有的子弹并新建一群外星人
        bullets.empty()
        create_fleet(ai_settings, screen, ship, aliens)
        ai_settings.increase_speed()

有子弹击中外星人时,Python将返回一个字典(collisions)。我们检查这个字典是否存在,如果存在,就将得分加上一个外星人的分数。接下来,调用prep_score()来创建一副包含最新得分的新图像。
14.3.4 重置得分
当前,仅在有外星人被射杀之后生成得分。这在大多数情况下可行,但从开始新游戏到有外星人被射杀之间,显示的是上一次的得分。

为修复这个问题,可在开始新游戏时生成得分:
def check_play_button(
        stats,
        play_button,
        aliens,
        bullets,
        ai_settings,
        screen,
        ship,
        mouse_pos,sb):
    """玩家单击play时开始游戏"""
    button_clicked = play_button.rect.collidepoint(mouse_pos)

    if button_clicked and not stats.game_active:
        # 重置游戏统计信息
        ai_settings.initialize_dynamic_settings()
        stats.reset_stats()
        stats.game_active = True
        sb.prep_score()

开始新游戏时,我们重置游戏统计信息再调用prep_score()。此时生成的记分牌上显示的得分为0.
14.3.5 将消灭的每个外星人都计入得分
在check_bullet_alien_collisions()中,与外星人碰撞的子弹都是字典collisions中的一个键,而与每颗子弹相关的值都是一个列表,其中包含该子弹集中的的外星人。我们遍历字典collisions,确保将消灭的每个外星人都计入得分:
def check_bullet_alien_collisions(ai_settings, screen, ship, aliens, bullets,stats,sb):
    """响应子弹和外星人碰撞"""
    # 检查是否有子弹击中了外星人
    # 删除发生碰撞的子弹和外星人
    collisions = pygame.sprite.groupcollide(bullets, aliens, True, True)
    if collisions:
        for aliens in collisions.values():
            stats.score += ai_settings.alien_points * len(aliens)
            sb.prep_score

如果字典collisions存在,就遍历所有值。别忘了,每个值都是一个列表,包含被同一颗子弹击中的所有外星人。
14.3.6 提高分数
鉴于玩家每提高一个等级,游戏都变得更难,因此处于较高的等级时,外星人的分数应更高。为实现这种功能,需要编写在游戏节奏加快时提高分数的代码:
        # 加快游戏节奏的速度
        self.speedup_scale = 1.1
        # 外星人分数的提高速度
        self.score_scale = 1.5
        self.initialize_dynamic_settings()

    def initialize_dynamic_settings(self):
        """初始化随游戏进行变化的设置"""
        self.ship_speed_factor = 1.5
        self.bullet_speed_factor = 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_factor *= self.speedup_scale
        self.bullet_speed_factor *= self.speedup_scale
        self.alien_speed *= self.speedup_scale
        alien_points = int(self.alien_points * self.score_scale)

我们定义了分数的提高速度,并称之为score_scale。较低的节奏加快速度让游戏很快变得极具挑战性,但未了让记分发生显著的变化,需要将分数的提高速度设置为更大的值。为让分数为整数,使用了函数int()。

为显示外星人的分数,在Settings的方法increase_speed()中调用函数print():
    def increase_speed(self):
        """提高速度设置和外星人分数"""
        self.ship_speed_factor *= self.speedup_scale
        self.bullet_speed_factor *= self.speedup_scale
        self.alien_speed *= self.speedup_scale
        alien_points = int(self.alien_points * self.score_scale)
        print(self.alien_points)

现在每当提高一个等级时,你都将在终端窗口看到新的分数值。
14.3.7 舍入得分
大多数阶级风格的设计游戏将得分显示为10的整数倍,下面让记分系统遵循这个原则。我们还将设置得分的格式,在大多数中用逗号标识千位分隔符。在Scoreboard中执行这种修改:
    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)

函数round()通常让小数精确到小数点后某一位。其中小数位数是由第二个实参指定的,然而如果将第二个实参指定为复数,round()将舍入到最近的10的整数倍,如10、100、1000等。

在score_str处,使用一个字符串格式设置指令,让Python将数值转换为字符串时在其中插入逗号。
14.3.8 最高得分
我们将最高得分存储在GameStats中:
class GameStats():
    """跟踪游戏的统计信息"""

    def __init__(self, ai_settings):
        self.ai_settings = ai_settings
        self.reset_stats()
        self.game_active = True
        # 任何情况下都不应重置最高得分
        self.high_score = 0

因为在任何情况下都不会重置最高得分,所以__init__()而不是reset_stats()中初始化 high_score。

下面来修改Scoreboard以显示最高得分。先来修改方法__init__():
    def __init__(self, screen,ai_settings,stats):
        """初始化显示得分涉及的属性"""
        self.screen = screen
        self.screen_rect = screen.get_rect()
        self.settings = ai_settings
        self.stats = stats

        # 显示得分信息是使用的字体设置
        self.text_color = (30, 30, 30)
        self.font = pygame.font.SysFont(None, 48)
        # 准备包含最高得分和当前得分的图像
        self.prep_score()
        self.prep_high_score()

最高得分疆域当前得分分开显示,因此需要编写一个新方法prep_high_score(),用于准备包含最高得分的图像。

方法prep_high_score()的代码如下:
    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_centerx = self.screen_rect.centerx
        self.high_score_rect.top = self.score_rect.top
       
将最高得分舍入到最近的10的整数倍,并添加用逗号标识的千分位分隔符。然后,根据最高得分生成一幅图像,并使其水平居中,并将其top属性设置为当前得分图像的top属性。

现在,方法show_score()需要在屏幕右上角显示当前得分,并在屏幕顶部中央显示最高得分:
    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():
    def check_high_score(self):
        """检查是否诞生了新的最高得分"""
        if self.stats.score > self.stats.high_score:
            self.stats.high_score = self.stats.score
            self.prep_score()

方法check_high_score比较当前分和最高分,如果当前分比记录的最高分高,就替换历史最高分。

在check_bullet_alien_collisions()中,每当有外星人被消灭时,都需要在更新得分后调用check_hiht_score():
def check_bullet_alien_collisions(ai_settings, screen, ship, aliens, bullets,stats,sb):
    """响应子弹和外星人碰撞"""
    # 检查是否有子弹击中了外星人
    # 删除发生碰撞的子弹和外星人
    collisions = pygame.sprite.groupcollide(bullets, aliens, True, True)
    if collisions:
        for aliens in collisions.values():
            stats.score += ai_settings.alien_points * len(aliens)
            sb.prep_score()
            sb.check_high_score()

如果字典collisons存在,就根据消灭了多少外星人更新的分,在调用check_high_score()。
14.3.9 显示等级
为在游戏中显示玩家的等级,需要在GameStats中添加一个表示当前等级的属性。为确保每次开始新游戏时都重置等级,在reset_stats()中初始化它:
    def reset_stats(self):
        """初始化在游戏运行期间可能变化的统计信息"""
        self.ships_left = self.ai_settings.ship_limit
        self.score = 0
        self.level = 1

为了让Scoreboard显示当前等级,在__init__()调用一个新方法prep_level():
    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():
    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()中提高等级并更新等级图像:
def check_bullet_alien_collisions(ai_settings, screen, ship, aliens, bullets,stats,sb):
    """响应子弹和外星人碰撞"""
    # 检查是否有子弹击中了外星人
    # 删除发生碰撞的子弹和外星人
    collisions = pygame.sprite.groupcollide(bullets, aliens, True, True)
    if collisions:
        for aliens in collisions.values():
            stats.score += ai_settings.alien_points * len(aliens)
            sb.prep_score()
            sb.check_high_score()
    if len(aliens) == 0:
        # 删除现有的子弹并新建一群外星人
        bullets.empty()
        create_fleet(ai_settings, screen, ship, aliens)
        ai_settings.increase_speed()
        
        # 提高等级
        stats.level += 1
        sb.prep_level()

如果整群外星人都被消灭,就将stats.level的值加1,并调用prep_level()确保正确地显示新等级。

为确保在开始新游戏时更新等级图像,还需在玩家单机按钮play时调用prep_level()。
    if button_clicked and not stats.game_active:
        # 重置游戏统计信息
        ai_settings.initialize_dynamic_settings()
        stats.reset_stats()
        stats.game_active = True
        sb.prep_score()
        sb.prep_level()
14.3.10 显示余下的飞船数
最后来显示玩家还有多少艘飞船,但是用图形而不是数字。为此,在屏幕左上角绘制飞船图像来支出还余下多少艘飞船,就像众多经典游戏那样。

首先,需要让Ship继承Sprite,以便创建飞船编组:
import pygame
from pygame.sprite import Sprite


class Ship(Sprite):
    def __init__(self, ai_settings, screen):
        super().__init__()

这里导入了Sprite,让Ship继承Sprite,并在__init__()的开头调用super()。

接下来,需要修改Scoreboard,以创建可供显示的飞船编组。下面是其中的import语句:
import pygame.font
from pygame.sprite import Group
from test_ship import Ship

鉴于需要创建飞船编组,导入Group和Ship类。

下面是方法__init__():
class Scoreboard:
    """显示得分信息的类"""

    def __init__(self, screen,ai_settings,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()
        self.prep_high_score()
        self.prep_level()
        self.prep_ships()

我们将游戏实例赋给一个属性,因为创建飞船时需要用到它。在调用prep_level()后调用了prep_ship().

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

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

现在需要在屏幕上绘制飞船了:
    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()。这是在check_play_button()中进行的:
    if button_clicked and not stats.game_active:
        # 重置游戏统计信息
        ai_settings.initialize_dynamic_settings()
        stats.reset_stats()
        stats.game_active = True
        sb.prep_score()
        sb.prep_level()
        sb.prep_ships()

还要在飞船被外星人撞到时调用prep_ships(),从而在玩家损失飞船时更新飞船图像:
def ship_hit(ai_settings, stats, screen, ship, aliens, bullets,sb):
    """响应被外星人撞到的飞船"""
    if stats.ships_left > 0:
        # 将ship_left 减1 并更新记分牌
        stats.ships_left -= 1
        sb.prep_ships()

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

历经了不知道多少天。总算是把第一个项目:外星人入侵搞定了,纸上得来终觉浅,绝知此事要躬行。别说是上手了,自己跟着练一遍都还觉得自己跟弱智的区别在于他不会觉得自己笨。

不容易啊。

下一篇内容我会把外星人入侵所有代码贴上来,不敢说都对,但起码能运行。

下一个项目再见~

胭惜雨

2021年02月26日

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据