添加Play按钮

首先在Setting中添加如下代码

1
2
3
4
5
6
7
# 按钮设置
self.button_color = (0, 255, 0)
self.button_width = 200
self.button_height = 50
self.button_text_color = (255, 255, 255)
self.button_font = 48
self.play_button_text = "Play"

将按钮的相关属性加入到设置相关的类中,然后创建button.py文件,添加如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import pygame.font

class Button:
def __init__(self, game, msg):
self.game = game
self.font = pygame.font.SysFont(None, self.game.settings.button_font)

self.rect = pygame.Rect(0, 0, self.game.settings.button_width, self.game.settings.button_height)
self.rect.center = self.game.screen.get_rect().center

self._pre_msg(msg)

def _pre_msg(self, msg):
"""将msg渲染为图像"""
self.msg_image = self.font.render(msg, True, self.game.settings.button_text_color, self.game.settings.button_color)
self.msg_image_rect = self.msg_image.get_rect()
self.msg_image_rect.center = self.rect.center

def draw(self):
"""绘制按钮"""
self.game.screen.fill(self.game.settings.button_color, self.rect)
self.game.screen.blit(self.msg_image, self.msg_image_rect)

__init__()函数用于按钮的初始化,如按钮的大小、字体、位置等,_pre_msg()函数用于将字符串消息转换为图像并填充到矩形中,draw()函数用于绘制按钮

接下来我们在主程序中将game_active设为False,从而为了显示按钮,然后添加按钮属性self.play_button = Button(self, self.settings.play_button_text),接着在更新屏幕的地方判断如果游戏处于非活跃状态就显示按钮

1
2
if not self.game_active:
self.play_button.draw()

现在运行游戏

image-20250706105928604

开始游戏

此时点击按钮并没有做出反应,因为我们还没有添加鼠标点击响应的事件,首先在_check_events中添加如下代码

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

上述代码用于检测是否有鼠标按下,如果有则获取鼠标点击的位置,然后将变量传递给_check_play_button,现在我们添加该函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def _check_play_button(self, mouse_pos):
"""响应Play按钮的点击事件"""
if self.play_button.rect.collidepoint(mouse_pos):
# 重置游戏状态
self.status.reset_status()
self.game_active = True

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

# 创建新的外星人舰队
self._create_fleet()

# 重置飞船位置
self.ship.center_ship()

一旦有点击事件的发生,我们就判断点击位置是否位于按钮中,如果是,则重置游戏状态并清空所有子弹和外星人,创建新的外星人和飞船,从而表明我们开启了新游戏,运行如下

20250706-111254

游戏优化

当前游戏中还存在Bug,如果我们点击按钮所在区域,即便是按钮已经消失但仍会做出响应并重置游戏,因为我们的鼠标点击事件还会判断其点击位置与按钮位置重合,因此我们需要对判断做出优化

1
if self.play_button.rect.collidepoint(mouse_pos) and not self.game_active:

_check_play_button函数中的if判断语句改为上述代码,从而防止其在游戏运行时仍会执行重置代码

另外,玩游戏时我们应该将光标隐藏,在游戏结束时显示光标

_check_play_button的末尾添加pygame.mouse.set_visible(False),游戏开始将光标隐藏,在_ship_hitelse块的末尾添加pygame.mouse.set_visible(True) ,游戏结束时将光标显示

提升趣味性

Settings中添加如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    # 游戏速度设置
self.speed_scale = 1.1
self.init_settings()

def init_settings(self):
"""初始化游戏的动态设置"""
self.ship_speed = 30
self.bullet_speed = 10
self.alien_speed = 10
self.alien_drop_speed = 10
self.alien_direction = 1

def increase_speed(self):
"""提高游戏速度设置"""
self.ship_speed *= self.speed_scale
self.bullet_speed *= self.speed_scale
self.alien_speed *= self.speed_scale
self.alien_drop_speed *= self.speed_scale

speed_scale用于控制游戏增长的速度,init_settings用于初始化设置,increase_speed用于将相关参数增长从而提高游戏难度

_check_bullet_alien_collisions中,如果外星人全部被击落,我们就调用self.settings.increase_speed()增加游戏速度。当然每次开启新游戏时,应当将所有设置还原,在_check_play_buttonif语块的第一行添加self.settings.init_settings(),这将使得新游戏的属性为初始化值

记录分数

GameStatus中添加属性self.score = 0用于重置分数,在Settings中添加分数相关设置

1
2
3
# 游戏分数设置
self.score_text_color = (30, 30, 30)
self.score_font = 48

然后创建gamescore.py文件添加如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import pygame.font

class GameScore:
def __init__(self, game):
self.game = game
self.font = pygame.font.SysFont(None, self.game.settings.score_font)
self.prep_score()

def prep_score(self):
"""将得分渲染为图像"""
score_str = str(self.game.status.score)
self.score_image = self.font.render(score_str, True, self.game.settings.score_text_color, self.game.settings.bg_color)

self.score_rect = self.score_image.get_rect()
self.score_rect.right = self.game.screen.get_rect().right - 20
self.score_rect.top = 20

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

在主程序中添加成员self.score = GameScore(self),随后在更新屏幕函数中显示分数面板self.score.show_score(),此时运行游戏就能显示分数面板了

更新得分

Settingsinit_settings中添加self.alien_points = 50,用于表示每个外星人击落的初始得分,然后在主程序的_check_bullet_alien_collisions中添加

1
2
3
4
if collisions:
for aliens in collisions.values():
self.status.score += self.settings.alien_points * len(aliens)
self.score.prep_score()

表示每次击中一个外星人则对其进行加分,同时击落多个外星人也能同时记分,当然每次开始游戏我们也要重置分数,在_check_play_button中添加self.score.prep_score(),用新的分数来初始化记分面板

对于不同速度的外星人,我们也应该增加其击落分数,在Settings中添加self.score_scale = 1.5表示外星人的分数增长速度,然后在increase_speed中添加self.alien_points = int(self.alien_points * self.score_scale),更新新的外星人分数,最后再对分数进行取舍,在GameScoreprep_score中修改代码如下

1
2
3
4
5
6
7
8
9
def prep_score(self):
"""将得分渲染为图像"""
rounded_score = round(self.game.status.score, -1)
score_str = f"{rounded_score:,}" # 使用逗号分隔千位
self.score_image = self.font.render(score_str, True, self.game.settings.score_text_color, self.game.settings.bg_color)

self.score_rect = self.score_image.get_rect()
self.score_rect.right = self.game.screen.get_rect().right - 20
self.score_rect.top = 20

表示我们通过round函数将分数取为10的整数倍,这样每个外星人的分数都是10的倍数了

获取最高分

GameStatus添加新属性self.max_score = 0用来记录最高分,随后在GameScore中添加新函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def prep_high_score(self):
"""将最高得分渲染为图像"""
high_score = round(self.game.status.max_score, -1)
high_score_str = f"{high_score:,}"
self.high_score_image = self.font.render(high_score_str, True, self.game.settings.score_text_color, self.game.settings.bg_color)

self.high_score_rect = self.high_score_image.get_rect()
self.high_score_rect.centerx = self.game.screen.get_rect().centerx
self.high_score_rect.top = self.score_rect.top

def check_high_score(self):
"""检查是否达到了新的最高得分"""
if self.game.status.score > self.game.status.max_score:
self.game.status.max_score = self.game.status.score
self.prep_high_score()

这里将最高分渲染为图像并放置在屏幕正中央的上方,在__init__中调用prep_high_score,然后在show_score中将high_score_image显示出来,并在主程序中每当碰撞发生时就调用check_high_score判断当前分数是否超过了最高分,运行如下

image-20250706164524602

显示等级和飞船数

GameStatusreset_status中添加self.level = 1,在GameScore__init__添加函数self.prep_level(),代码如下

1
2
3
4
5
6
7
8
def prep_level(self):
"""将当前关卡渲染为图像"""
level_str = str(self.game.status.level)
self.level_image = self.font.render(level_str, True, self.game.settings.score_text_color, self.game.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中添加self.game.screen.blit(self.level_image, self.level_rect)来显示图像,在主程序中清除所有外星人之后提升等级,代码如下

1
2
3
4
5
6
7
8
9
if not self.aliens:
# 如果没有外星人了,重新创建外星人舰队
self._create_fleet()
self.bullets.empty()
self.settings.increase_speed()

# 提升关卡
self.status.level += 1
self.score.prep_level()

最后在_check_play_button中重置等级,调用self.score.prep_level()即可,运行如下

image-20250706233018229

接下来我们要显示剩余飞船数,这里我们对飞船也要使用Sprite,更改Ship类如下

1
2
3
class Ship(Sprite):
def __init__(self, game):
super().__init__()

Ship继承自Sprite,在GameScore中添加函数prep_ships如下

1
2
3
4
5
6
7
8
def prep_ships(self):
"""显示剩余飞船数量"""
self.ships = Group()
for ship_number in range(self.game.status.ships_left):
ship = Ship(self.game)
ship.rect.x = 10 + ship_number * ship.rect.width
ship.rect.y = 10
self.ships.add(ship)

这个函数用于根据剩余的飞船数量来创建飞船对象并调整位置,然后在初始化的函数中添加self.prep_ships()。下一步我们就要绘制飞船了,在show_score中添加self.ships.draw(self.game.screen)即可,然后在主程序的游戏开始和飞船被撞击的地方调用self.score.prep_ships()就能够正常加载和显示剩余飞船了,如下

image-20250706234409505

游戏Bug更改

到上述部分我们就完成了书中的所有内容,但经过我的测试,这款游戏存在一定的Bug,看图

image-20250706234509797

是不是很奇怪?游戏还没有开始怎么就出现了一条横线?其实这是因为我们通过循环来侦听不同事件的发生,如果在游戏开始之前点击空格按钮,那么就会执行发射子弹的函数。但此时游戏状态并没有被激活,因此这条横线会停在这里,而且由于我们的子弹比较大,我们的飞船图像并不能完全遮盖子弹,所以就出现了这种现象,但从逻辑上来讲,如果我们不停按空格,程序应该每次都会执行空格所对应的响应,所以这里我们对其进行修改

1
2
3
4
5
6
7
8
9
10
11
12
13
def _check_events(self):
"""响应按键和鼠标事件"""
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
elif event.type == pygame.KEYDOWN and self.game_active:
self._check_keydown_events(event)
elif event.type == pygame.KEYUP and self.game_active:
self._check_keyup_events(event)
elif event.type == pygame.MOUSEBUTTONDOWN:
mouse_pos = pygame.mouse.get_pos()
self._check_play_button(mouse_pos)

上述代码主要增加了当键盘响应时游戏是否处于激活状态,如果游戏未激活则不响应任何按键,此时我们无论按键盘上的哪里,都不会再次出现上述的情况了,至此,该项目就完全结束了。源码访问点击这里