创建Alien类

创建一个alien.py文件,并把书中源码的alien.bmp图片拷贝到我们的imgs文件夹中,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import pygame
from pygame.sprite import Sprite

class Alien(Sprite):
def __init__(self, game):
super().__init__()
self.game = game

# 加载外星人图片
self.image = pygame.image.load('imgs/alien.bmp')
self.rect = self.image.get_rect()

# 设置外星人初始位置
self.rect.x = self.rect.width
self.rect.y = self.rect.height

# 外星人当前的x轴位置
self.x = float(self.rect.x)

创建Alien实例

由于外星人通常是成批次出现,因此我们也要通过Sprite管理,如self.aliens = pygame.sprite.Group(),这里我们单独创建一个函数用于外星人的生成

1
2
3
4
def _create_fleet(self):
"""创建外星人舰队"""
alinen = Alien(self)
self.aliens.add(alinen)

然后在更新屏幕的函数中添加如下代码用于外星人在屏幕上的显示

1
self.aliens.draw(self.screen)

运行

image-20250705143755517

此时在屏幕的左上角已经出现了一个外星人

创建Alien舰队

上述我们在_create_fleet中只生成了一个外星人,下面进行修改来生成多行外星人,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def _create_fleet(self):
"""创建外星人舰队"""
alinen = Alien(self)
alien_width = alinen.rect.width
alien_height = alinen.rect.height

current_x, current_y = alien_width, alien_height
# 计算屏幕可以容纳多少个外星人
while current_y < (self.settings.screen_height - 3 * alien_height):
while current_x < (self.settings.screen_width - 2 * alien_width):
self._create_alien(current_x, current_y)
current_x += 2 * alien_width

# 重置当前x位置,移动到下一行
current_y += 2 * alien_height
current_x = alien_width

def _create_alien(self, current_x, current_y):
"""创建一个外星人"""
alien = Alien(self)
alien.x = current_x
alien.rect.x = current_x
alien.rect.y = current_y
self.aliens.add(alien)

这里我们先生成一个实例,然后获取到它的宽度和高度并赋值为当前的x轴位置和y轴位置,内层循环用来生成一行外星人,从距离屏幕一个外星人的宽度开始,每隔一个外星人宽度创建一个实例,直到屏幕宽度不满足两个外星人的宽度为止,外层循环首先更新y轴位置,将其下移两个外星人的高度,然后初始化当前的x轴位置,再次创建一行,直到屏幕高度不满足三个外星人的高度位置为止,运行如下

image-20250705150208156

此时我们已经创建了一个外星人的舰队

移动舰队

Setting中添加如下代码

1
2
3
4
# 外星人设置
self.alien_speed = 1
self.alien_drop_speed = 10
self.alien_direction = 1 # 1表示向右移动,-1表示向左移动

分别表示外星人的水平速度、下降速度、水平方向

然后在Alien中添加更新位置的函数

1
2
3
4
def update(self):
"""更新外星人位置"""
self.x += self.game.settings.alien_speed * self.game.settings.alien_direction
self.rect.x = self.x

通过alien_direction来决定水平方向的移动,但这样只会让舰队朝着一个方向不停移动直到超出屏幕范围,在舰队即将到达屏幕边缘时,我们应该改变它的方向,让它朝着相反的方向移动,并下降一段距离。先添加边缘检测函数,如下

1
2
3
4
5
6
def check_edges(self):
"""检查外星人是否到达屏幕边缘"""
screen_rect = self.game.screen.get_rect()
if self.rect.right >= screen_rect.right or self.rect.left <= 0:
return True
return False

先获取屏幕大小,然后根据外星人所在的位置判断是否超出了屏幕边界,超了就返回True,否则返回就False

随后我们在主程序添加检测函数,并在到达边缘时使其下降一段距离

1
2
3
4
5
6
7
8
9
10
11
12
def _check_fleet_edges(self):
"""检查外星人是否到达屏幕边缘"""
for alien in self.aliens.sprites():
if alien.check_edges():
self._change_fleet_direction()
break

def _change_fleet_direction(self):
"""改变外星人舰队的方向"""
for alien in self.aliens.sprites():
alien.rect.y += self.settings.alien_drop_speed
self.settings.alien_direction *= -1

对于每一个外星人我们都检测其是否超出了屏幕范围,然后改变所有外星人其移动方向,并下降一段距离,此时我们在更新外星人位置的函数中调用检测边界函数,运行结果如下

20250705-153147

射击外星人

此时我们发射子弹,子弹并不能击落外星人,而是会直接穿过外星人,因为我们还没有做子弹和外星人的碰撞检测,下面我们来逐步实现这一过程

检测子弹和外星人的碰撞

要实现这个功能非常简单,我们只需要利用pygame.sprite.groupcollide(self.bullets, self.aliens, True, True)这个函数即可,前两个参数分别表示子弹群组和外星人群组,后面两个参数表示碰撞后是否消失,True表明碰撞后子弹和外星人均消失。但这样还有个缺陷,就是我们把外星人全部击落之后,屏幕将没有外星人了,我们应该在打完所有外星人之后重新生成新的外星人舰队,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def _update_bullets(self):
"""更新子弹位置"""
self.bullets.update()
for bullet in self.bullets.copy():
if bullet.rect.bottom <= 0:
self.bullets.remove(bullet)

self._check_bullet_alien_collisions()

def _check_bullet_alien_collisions(self):
"""检查子弹与外星人碰撞"""
collisions = pygame.sprite.groupcollide(self.bullets, self.aliens, True, True)
if not self.aliens:
# 如果没有外星人了,重新创建外星人舰队
self._create_fleet()
self.bullets.empty()

我们在更新子弹的后面加入了调用检测碰撞的函数,并在所有外星人消失后重新生成新的外星人舰队,同时清除所有子弹,从而使我们继续游戏

检测飞船和外星人的碰撞

如果按照上述的逻辑,那么这个游戏将没有终止的时候,为此应该加入游戏结束的一些条件,比如我们的飞船和外星人发生了碰撞,在更新外星人的函数中添加代码如下

1
2
3
# 检测外星人和飞船的碰撞
if pygame.sprite.spritecollideany(self.ship, self.aliens):
print("飞船被外星人撞了!")

spritecollideany(self.ship, self.aliens)函数接收一个精灵和一个编组,用于检测飞船是否有和外星人碰撞,如果发生碰撞,将打印”飞船被外星人撞了!”。通常来讲,我们不应当在飞船撞毁之后直接结束游戏,而是应该给予玩家多次机会,下面我们创建一个game_status.py文件,用于记录游戏信息,内容如下

1
2
3
4
5
6
7
8
9
10
class GameStatus:
def __init__(self, game):
"""初始化游戏状态"""
self.ships_left = None
self.game = game
self.reset_status()

def reset_status(self):
"""重置游戏状态"""
self.ships_left = self.game.settings.ship_limit

同时在Settings中添加self.ship_limit = 3,表明飞船最多只有三次,ships_left则用来记录剩余的飞船数量

下面我们对主程序进行修改,添加一个用于游戏状态表示的实例:self.status = GameStatus(self),然后创建撞击后的响应函数,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def _ship_hit(self):
"""响应飞船被外星人撞击的事件"""
if self.status.ships_left > 0:
# 减少剩余飞船数量
self.status.ships_left -= 1
# 清空子弹和外星人
self.bullets.empty()
self.aliens.empty()
# 重新创建外星人舰队
self._create_fleet()
# 重置飞船位置
self.ship.center_ship()
# 暂停一段时间
sleep(1)
else:
print("游戏结束!")

如果被撞击并且飞船数量大于0,则减少一个飞船数量并清空子弹和外星人,从而生成新的外星人和飞船。删除碰撞函数里print()函数,替换为_ship_hit(),并在Ship中添加如下函数

1
2
3
4
def center_ship(self):
"""将飞船放在屏幕底部中央"""
self.rect.midbottom = self.game.screen.get_rect().midbottom
self.x = float(self.rect.x)

使新生成的飞船位于屏幕底部中心

到达屏幕底部边缘

如果外星人到达屏幕底部,我们同样应该跟碰撞一样响应,添加屏幕下边缘检测函数

1
2
3
4
5
6
def _check_alien_bottom(self):
"""检查外星人是否到达屏幕底部"""
for alien in self.aliens.sprites():
if alien.rect.bottom >= self.settings.screen_height:
self._ship_hit()
break

检查每个外星人的底部是否超过了屏幕的高度,如果超过则调用_ship_hit()函数。在更新外星人的最后添加self._check_alien_bottom(),此时当外星人到达屏幕底部游戏也将重置

GameOver

细心一点可能会注意到,当我们死亡超过三次时,游戏仍然没有结束,事实上它永远也不会结束,因为我们在_ship_hit()函数中,当ships_left小于0时,只是调用了print("游戏结束!"),其他什么也没有做,因此ships_left只会越来越小,我们应该为其添加一个游戏标识,如果标识为False则终止某些部分的运行,从而实现游戏的结束。在主程序添加self.game_active = True,并将print("游戏结束!")替换为self.game_active = False,同时修改主循环如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 游戏主循环
while True:
# 监听事件
self._check_events()

if self.game_active:
# 更新飞船位置
self.ship.update()
# 更新子弹位置
self._update_bullets()
# 更新外星人位置
self._update_aliens()

# 更新屏幕
self._update_screen()
# 控制游戏帧率
self.clock.tick(self.settings.FPS)