评分
让我们来实现一个计分系统,实时跟踪游戏得分,并显示高分、关卡和剩余船只数量。
得分是一个游戏统计数据,因此我们将在 GameStats
中添加一个 score
属性:
class GameStats:
--snip--
def reset_stats(self):
"""Initialize statistics that can change during the game."""
self.ships_left = self.ai_settings.ship_limit
self.score = 0
为了在每次新游戏开始时重置分数,我们在 reset_stats()
而不是 __init__()
中初始化分数。
显示分数
要在屏幕上显示分数,我们首先要创建一个新类 Scoreboard
。目前,该类将只显示当前分数。最终,我们还将用它来报告高分、等级和剩余船只数。下面是类的第一部分;将其保存为 scoreboard.py:
import pygame.font
class Scoreboard:
"""A class to report scoring information."""
def __init__(self, ai_game): (1)
"""Initialize scorekeeping attributes."""
self.screen = ai_game.screen
self.screen_rect = self.screen.get_rect()
self.settings = ai_game.settings
self.stats = ai_game.stats
# Font settings for scoring information.
self.text_color = (30, 30, 30) (2)
self.font = pygame.font.SysFont(None, 48) (3)
# Prepare the initial score image.
self.prep_score() (4)
因为 Scoreboard
会将文本写入屏幕,所以我们首先要导入 pygame.font
模块。接下来,我们给 __init__()
提供 ai_game
参数,这样它就可以访问设置(settings)、屏幕(screen)和统计(stats)对象,它需要这些对象来报告我们要跟踪的值❶。然后,我们设置文本颜色❷ 并实例化一个字体对象❸。
为了把要显示的文本转换成图像,我们调用了 prep_score()
❹,我们在这里定义了它:
def prep_score(self):
"""Turn the score into a rendered image."""
score_str = str(self.stats.score) (1)
self.score_image = self.font.render(score_str, True, self.text_color, self.settings.bg_color) (2)
# Display the score at the top right of the screen.
self.score_rect = self.score_image.get_rect() (3)
self.score_rect.right = self.screen_rect.right - 20 (4)
self.score_rect.top = 20 (5)
在 prep_score()
中,我们将数值 stats.score
转换成字符串 ❶,然后将该字符串传递给 render()
,由 render()
创建图像 ❷。为了在屏幕上清晰显示分数,我们将屏幕的背景颜色和文本颜色传递给 render()
。
我们将把分数显示在屏幕的右上角,并随着分数的增加和数字宽度的增加而向左扩展。为确保分数始终与屏幕右侧对齐,我们创建了一个名为 score_rect
❸的矩形,并将其右侧边缘设置为距离屏幕右侧边缘 20 像素 ❹。然后,我们将顶部边缘设置为距离屏幕顶部❺向下 20 个像素。
然后,我们创建 show_score()
方法来显示渲染的分数图像:
def show_score(self):
"""Draw score to the screen."""
self.screen.blit(self.score_image, self.score_rect)
此方法会在屏幕上 score_rect
指定的位置绘制分数图像。
制作记分牌
为了显示得分,我们将在 AlienInvasion
中创建一个 Scoreboard
实例。首先,让我们更新导入语句:
--snip--
from game_stats import GameStats
from scoreboard import Scoreboard
--snip--
接下来,我们在 __init__()
中创建一个 Scoreboard
实例:
def __init__(self):
--snip--
pygame.display.set_caption("Alien Invasion")
# Create an instance to store game statistics,
# and create a scoreboard.
self.stats = GameStats(self)
self.sb = Scoreboard(self)
--snip--
然后我们在 _update_screen()
中在屏幕上绘制记分牌:
def _update_screen(self):
--snip--
self.aliens.draw(self.screen)
# Draw the score information.
self.sb.show_score()
# Draw the play button if the game is inactive.
--snip--
在绘制 "播放" 按钮之前,我们会调用 show_score()
。
现在运行 "异形入侵" 时,屏幕右上方应该会出现一个 0。(此时,我们只想在进一步开发计分系统之前确保分数出现在正确的位置)。图 14-2 显示了游戏开始前的分数。
接下来,我们将为每个外星人分配分值!

外星人被击落时更新分数
为了在屏幕上写入实时分数,每当有外星人被击中,我们就更新 stats.score
的值,然后调用 prep_score()
更新分数图像。但首先,我们要确定玩家每次击落外星人后会得到多少分:
def initialize_dynamic_settings(self):
--snip--
# Scoring settings
self.alien_points = 50
随着游戏的进行,我们将增加每个外星人的点数值。为了确保每次新游戏开始时都能重置该点数值,我们在 initialize_dynamic_settings()
中设置了该值。
每击落一个外星人,我们就在 _check_bullet_alien_collisions()
中更新分数:
def _check_bullet_alien_collisions(self):
"""Respond to bullet-alien collisions."""
# Remove any bullets and aliens that have collided.
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 会返回一个碰撞字典。我们检查字典是否存在,如果存在,外星人的值就会添加到分数中。然后我们调用 prep_score()
为更新后的分数创建一个新图像。
现在,当你玩 "异形入侵" 游戏时,你应该可以获得很多分数了!
重置分数
现在,我们只有在外星人被击中后才会准备新的分数,这在游戏的大部分时间里都有效。但当我们开始新游戏时,在第一个外星人被击中之前,我们仍然会看到旧游戏中的分数。
我们可以通过在开始新游戏时预置分数来解决这个问题:
def _check_play_button(self, mouse_pos):
--snip--
if button_clicked and not self.game_active:
--snip--
# Reset the game statistics.
self.stats.reset_stats()
self.sb.prep_score()
--snip--
开始新游戏时,我们会在重置游戏统计数据后调用 prep_score()
。这样,记分牌上的分数就会变成 0。
确保获得所有命中
按照目前的写法,我们的代码可能会漏掉某些外星人的得分。例如,如果两颗子弹在同一循环中与外星人相撞,或者如果我们制作了一颗超宽子弹来击中多个外星人,那么玩家只能在击中其中一个外星人时获得分数。为了解决这个问题,让我们改进子弹与外星人碰撞的检测方式。
在 _check_bullet_alien_collisions()
中,任何与外星人相撞的子弹都会成为碰撞字典中的一个键。与每颗子弹相关的值是与之发生碰撞的外星人列表。我们会循环查看碰撞字典中的值,以确保每击中一个外星人都能获得分数:
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 像素,并验证您用超宽子弹击中的每个外星人都能获得分数;然后将子弹宽度恢复到正常值。
增加积分值
因为每次玩家到达一个新的关卡,游戏的难度都会增加,所以后面关卡中的外星人应该能获得更多的分数。为了实现这一功能,我们将添加代码,以便在游戏速度提高时增加分值:
class Settings:
"""A class to store all settings for Alien Invasion."""
def __init__(self):
--snip--
# How quickly the game speeds up
self.speedup_scale = 1.1
# How quickly the alien point values increase
self.score_scale = 1.5 (1)
self.initialize_dynamic_settings()
def initialize_dynamic_settings(self):
--snip--
def increase_speed(self):
"""Increase speed settings and alien point values."""
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) (2)
我们定义了积分增加的速度,称之为 score_scale
❶。速度的小幅增加(1.1)会使游戏迅速变得更具挑战性。但是,要想看到更明显的得分差异,我们需要将外星分值改变得更大(1.5)。现在,当我们提高游戏速度时,我们也增加了每次击中的点值❷。我们使用 int()
函数来增加整数点值。
要查看每个外星人的数值,请在设置中的 increase_speed()
方法中添加 print()
调用:
def increase_speed(self):
--snip--
self.alien_points = int(self.alien_points * self.score_scale)
print(self.alien_points)
每达到一个新等级,终端中就会出现新的点值。
在确认点数值正在增加后,请务必删除 |
四舍五入
大多数街机射击游戏都以 10 的倍数来报告分数,因此我们的分数也应如此。此外,让我们在分数格式中加入大数字的逗号分隔符。我们将在记分板中进行这一更改:
def prep_score(self):
"""Turn the score into a rendered image."""
rounded_score = round(self.stats.score, -1)
score_str = f"{rounded_score:,}"
self.score_image = self.font.render(score_str, True, self.text_color, self.settings.bg_color)
--snip--
round()
函数通常将浮点数舍入到第二个参数设置的小数位数。然而,当您传递一个负数作为第二个参数时,round()
会将数值舍入到最接近的 10、100、1,000,依此类推。这段代码告诉 Python 将 stats.score
的值四舍五入到最接近的 10,并将其赋值给 rounded_score
。
然后,我们在分数的 f-string 中使用格式指定符。格式指定符是一个特殊的字符序列,用于修改变量值的显示方式。在本例中,序列 : 告诉 Python 在所提供数值的适当位置插入逗号。这样,字符串就会变成 1,000,000 而不是 1000000。
现在,当您运行游戏时,即使您获得了很多分数,也会看到格式整齐、四舍五入的分数,如图 14-3 所示。

高分数
每个玩家都想打破游戏的最高分,因此让我们来跟踪和报告高分,让玩家有所追求。我们将在 GameStats
中存储高分:
def __init__(self, ai_game):
--snip--
# High score should never be reset.
self.high_score = 0
因为高分不应该被重置,所以我们在 __init__()
而不是 reset_stats()
中初始化 high_score
。
接下来,我们将修改 Scoreboard
以显示高分。让我们从 __init__()
方法开始:
def __init__(self, ai_game):
--snip--
# Prepare the initial score images.
self.prep_score()
self.prep_high_score() (1)
高分将与分数分开显示,因此我们需要一个新方法 prep_high_score()
来准备高分图像❶。
下面是 prep_high_score()
方法:
def prep_high_score(self):
"""Turn the high score into a rendered image."""
high_score = round(self.stats.high_score, -1)
high_score_str = f"{high_score:,}"
self.high_score_image = self.font.render(high_score_str, True,self.text_color, self.settings.bg_color)
# Center the high score at the top of the screen.
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
属性设置为与分数图片的顶部相匹配❹。
现在,show_score()
方法会在屏幕右上方绘制当前分数,在屏幕中央上方绘制高分:
def show_score(self):
"""Draw score to the screen."""
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):
"""Check to see if there's a new high score."""
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()
:
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--
当碰撞字典存在时,我们会调用 check_high_score()
,并在更新所有被击中的外星人的分数后调用 check_high_score()
。
第一次玩 "外星入侵" 时,您的分数将是最高分,因此会显示为当前分数和最高分。但当你开始第二局游戏时,你的高分应该显示在中间,而当前分数应该显示在右边,如图 14-4 所示。

显示级别
要在游戏中显示玩家的等级,我们首先需要在 GameStats
中设置一个属性,代表当前等级。要在每个新游戏开始时重置等级,请在 reset_stats()
中对其进行初始化:
def reset_stats(self):
"""Initialize statistics that can change during the game."""
self.ships_left = self.settings.ship_limit
self.score = 0
self.level = 1
为了让 Scoreboard
显示当前级别,我们在 __init__()
中调用了一个新方法 prep_level()
:
def __init__(self, ai_game):
--snip--
self.prep_high_score()
self.prep_level()
下面是 prep_level()
:
def prep_level(self):
"""Turn the level into a rendered image."""
level_str = str(self.stats.level)
self.level_image = self.font.render(level_str, True, self.text_color, self.settings.bg_color) (1)
# Position the level below the score.
self.level_rect = self.level_image.get_rect()
self.level_rect.right = self.score_rect.right (2)
self.level_rect.top = self.score_rect.bottom + 10 (3)
prep_level()
方法根据存储在 stats.level
❶ 中的值创建图像,并设置图像的右属性与分数的右属性❷ 匹配。然后将顶部属性设置为分数图像底部下方 10 个像素,以便在分数和级别❸ 之间留出空间。
我们还需要更新 show_score()
:
def show_score(self):
"""Draw scores and level to the screen."""
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()
中递增 stats.level
,并更新关卡图像:
def _check_bullet_alien_collisions(self):
--snip--
if not self.aliens:
# Destroy existing bullets and create new fleet.
self.bullets.empty()
self._create_fleet()
self.settings.increase_speed()
# Increase level.
self.stats.level += 1
self.sb.prep_level()
如果舰队被摧毁,我们会递增 stats.level
的值并调用 prep_level()
,以确保正确显示新的关卡。
为确保在新游戏开始时正确更新关卡图像,我们还会在玩家点击 "播放" 按钮时调用 prep_level()
:
def _check_play_button(self, mouse_pos):
--snip--
if button_clicked and not self.game_active:
--snip--
self.sb.prep_score()
self.sb.prep_level()
--snip--
我们在调用 prep_score()
之后立即调用 prep_level()
。
如图 14-5 所示,现在你将看到自己完成了多少关卡。

在一些经典游戏中,分数都有标签,如得分、最高分和等级。我们省略了这些标签,因为只要玩过游戏,每个数字的含义就会一目了然。要包含这些标签,请在 |
显示船舶数量
最后,让我们来显示玩家所剩的飞船数量,不过这次我们要使用图形。为此,我们将在屏幕左上角绘制船只,以表示还剩下多少艘船,就像许多经典街机游戏那样。
首先,我们需要让 Ship
继承自 Sprite
,这样就可以创建一组船只:
import pygame
from pygame.sprite import Sprite
class Ship(Sprite): (1)
"""A class to manage the ship."""
def __init__(self, ai_game):
"""Initialize the ship and set its starting position."""
super().__init__() (2)
--snip--
在这里,我们导入 Sprite
,确保 Ship
继承自 Sprite
❶,并在 __init__()
❷的开头调用 super()
。
接下来,我们需要修改 Scoreboard
来创建一组可以显示的飞船。下面是 Scoreboard
的导入语句:
import pygame.font
from pygame.sprite import Group
from ship import Ship
因为我们要制作一组飞船,所以要导入 Group
和 Ship
类。
下面是 __init__()
:
def __init__(self, ai_game):
"""Initialize scorekeeping attributes."""
self.ai_game = ai_game
self.screen = ai_game.screen
--snip--
self.prep_level()
self.prep_ships()
我们将游戏实例分配给一个属性,因为我们需要它来创建一些飞船。我们在调用 prep_level()
之后调用 prep_ships()
。
下面是 prep_ships()
:
def prep_ships(self):
"""Show how many ships are left."""
self.ships = Group() (1)
for ship_number in range(self.stats.ships_left): (2)
ship = Ship(self.ai_game)
ship.rect.x = 10 + ship_number * ship.rect.width (3)
ship.rect.y = 10 (4)
self.ships.add(ship) (5)
prep_ships()
方法会创建一个空组 self.ships
,用于保存飞船实例 ❶。为了填充这个组,我们会为玩家离开的每一艘飞船运行一次循环❷。在这个循环中,我们会创建一艘新的飞船,并设置每艘飞船的 x 坐标值,这样飞船就会相邻出现,并在飞船组 ❸ 的左侧留出 10 个像素的边距。我们将 Y 坐标值设置为从屏幕顶部向下 10 个像素,这样飞船就会出现在屏幕的左上角 ❹。然后,我们将每艘新船只添加到船只组 ❺。
现在我们需要将船只绘制到屏幕上:
def show_score(self):
"""Draw scores, level, and ships to the screen."""
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()
中这样做:
def _check_play_button(self, mouse_pos):
--snip--
if button_clicked and not self.game_active:
--snip--
self.sb.prep_level()
self.sb.prep_ships()
--snip--
我们还会在船只被击中时调用 prep_ships()
,以便在玩家失去船只时更新船只图像的显示:
def _ship_hit(self):
"""Respond to ship being hit by alien."""
if self.stats.ships_left > 0:
# Decrement ships_left, and update scoreboard.
self.stats.ships_left -= 1
self.sb.prep_ships()
--snip--
我们在减小 ships_left
的值后调用 prep_ships()
,因此每次摧毁船只时都会显示正确的剩余船只数。
图 14-6 显示了完整的计分系统,剩余船只显示在屏幕左上方。
