Python应用实例(一)外星人入侵(十)

1.记分

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

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

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

1.1 显示得分

为在屏幕上显示得分,首先创建一个新类Scoreboard。当前,这个类只显示当前得分,但后面也将使用它来显示最高得分、等级和余下的飞船数。下面是这个类的前半部分,被保存为文件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.2 创建记分牌

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

alien_invasion.py

--snip--
from game_stats import GameStats
from scoreboard import Scoreboard
--snip--

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

    def __init__(self):
        --snip--
        pygame.display.set_caption("Alien Invasion")

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

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

    def _update_screen(self):
        --snip--
        self.aliens.draw(self.screen)

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

        # 如果游戏处于非活动状态,就显示Play按钮。
        --snip--

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

在这里插入图片描述

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

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

settings.py

    def initialize_dynamic_settings(self):
        --snip--

        # 记分
        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()
        --snip--

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

1.4 重置得分

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

alien_invasion.py

    def _check_play_button(self, mouse_pos):
        --snip--
        if button_clicked and not self.stats.game_active:
            --snip--
            # 重置游戏统计信息。
            self.stats.reset_stats()
            self.stats.game_active = True
            self.sb.prep_score()
            --snip--

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

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

当前的代码可能会遗漏一些被消灭的外星人。例如,如果在一次循环中,有两颗子弹击中了外星人,或者因子弹较宽而同时击中了多个外星人,玩家将只能得到一个外星人的分数。为修复这种问题,我们来调整检测子弹和外星人碰撞的方式。

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

alien_invasion.py

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

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

1.6 提高分数

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

settings.py

  class Settings:
      """存储游戏《外星人入侵》的所有设置的类。"""

      def __init__(self):
          --snip--
          # 加快游戏节奏的速度。
          self.speedup_scale = 1.1
          # 外星人分数的提高速度。
❶         self.score_scale = 1.5

          self.initialize_dynamic_settings()

      def initialize_dynamic_settings(self):
          --snip--

      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):
        --snip--
        self.alien_points = int(self.alien_points * self.score_scale)
        print(self.alien_points)

现在每当提高一个等级时,你都将在终端窗口看到新的分数值。

注意 确认分数在不断增加后,一定要删除调用函数print()的代码,否则可能影响游戏的性能,分散玩家的注意力。

1.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)
          --snip--

函数round()通常让小数精确到小数点后某一位,其中小数位数是由第二个实参指定的。然而,如果将第二个实参指定为负数,round()将舍入到最近的10的整数倍,如10、100、1000等。❶处的代码让Python将stats.score的值舍入到最近的10的整数倍,并将结果存储到rounded_score中。

在❷处,使用一个字符串格式设置指令,让Python将数值转换为字符串时在其中插入逗号。例如,输出为1,000,000而不是1000000。如果现在运行这个游戏,看到的得分将是10的整数倍,即便得分很高亦如此,如图所示。

在这里插入图片描述

1.8 最高得分

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

    def __init__(self, ai_game):
        --snip--
        # 任何情况下都不应重置最高得分。
        self.high_score = 0

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

      def __init__(self, ai_game):
          --snip--
          # 准备包含最高得分和当前得分的图像。
          self.prep_score()
❶         self.prep_high_score()

最高得分将与当前得分分开显示,因此需要编写一个新方法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

    def _check_bullet_alien_collisions(self):
        --snip--
        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()
        --snip--

如果字典collisions存在,就根据消灭了多少外星人更新得分,再调用check_high_score()。

第一次玩这个游戏时,当前得分就是最高得分,因此两个地方显示的都是当前得分。但再次开始该游戏时,最高得分会出现在中央,而当前得分则出现在右边,如图所示。

在这里插入图片描述

1.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

    def __init__(self, ai_game):
        --snip--
        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

    def _check_bullet_alien_collisions(self):
        --snip--
        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):
        --snip--
         if button_clicked and not self.stats.game_active:
             --snip--
             self.sb.prep_score()
             self.sb.prep_level()
             --snip--

这里在调用prep_score()后立即调用prep_level()。现在可以知道到了多少级,如图所示。

在这里插入图片描述

注意 在一些经典游戏中,得分带有标签,如Score、High Score和Level。这里没有显示这些标签,游戏开始后,每个数的含义将一目了然。要包含这些标签,只需在Scoreboard中调用font.render()前,将它们添加到得分字符串中。

1.10 显示余下的飞船数

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

  import pygame
  from pygame.sprite import Sprite

❶ class Ship(Sprite):
      """管理飞船的类。"""

      def __init__(self, ai_game):
          """初始化飞船并设置其起始位置。"""super().__init__()
          --snip--

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

import pygame.font
from pygame.sprite import Group

from ship import Ship

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

scoreboard.py

    def __init__(self, ai_game):
        """初始化记录得分的属性。"""
        self.ai_game = ai_game
        self.screen = ai_game.screen
        --snip--
        self.prep_level()
        self.prep_ships()

我们将游戏实例赋给一个属性,因为创建飞船时需要用到它。在调用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):
        --snip--
     if button_clicked and not self.stats.game_active:
         --snip--
         self.sb.prep_score()
         self.sb.prep_level()
         self.sb.prep_ships()
         --snip--

还要在飞船被外星人撞到时调用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()
            --snip--

这里在将ships_left的值减1后调用prep_ships()。这样每次损失飞船后,显示的飞船数都是正确的。图显示了完整的记分系统,它在屏幕左上角指出还余下多少艘飞船。

在这里插入图片描述

posted @ 2023-07-15 21:53  小幽余生不加糖  阅读(15)  评论(0编辑  收藏  举报  来源