Python学习之路12-外星人

《Python编程:从入门到实践》笔记。
本章主要是对上一篇的继续,添加“外星人”,“外星人”与飞船的交互。

1. 回顾项目

开发较大的项目时,进入每个开发阶段前回顾一下开发计划,搞清楚接下来要通过代码实现哪些功能至关重要。本篇将设计一下内容:

  • 研究即有代码,确定实现新功能前是否需要重构代码
  • 在屏幕左上角添加一个外星人,并指定合适的边距
  • 根据第一个外星人的边距和屏幕尺寸计算屏幕上可容纳多少个外星人。编写一个循环来填满屏幕的上半部分
  • 让外星舰队向两边和下方移动,直到外星人被全部击落,或有外星人撞到飞船,或有外星人抵达屏幕底部。如果所有外星人都被击落,再创建一批外星人。如果有外星人撞到飞船或到达屏幕底部,则销毁飞船并再创建一群外星人。
  • 限制玩家可用的飞机数,消耗完则游戏结束

希望各位上一篇的代码没有删掉。在开始新的代码前,我们先在前面的check_keydown_events()函数中添加“通过快捷键Q结束游戏”的代码:

1
2
3
4
def check_keydown_event(event, ship, ai_settings, screen, bullets):
-- snip --
elif event.key == pygame.K_q:
sys.exit()

2. 创建外星人

首先我们需要编写一个外星人Alien类。新建alien.py模块,在其中加入如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import pygame
from pygame.sprite import Sprite

class Alien(Sprite):
"""表示单个外星人的类"""
def __init__(self, ai_settings, screen):
"""初始化外星人并设置其起始位置"""
super(Alien, self).__init__()
self.screen = screen
self.ai_settings = ai_settings

# 加载外星人图像,并设置其rect属性
self.image = pygame.image.load("images/alien.bmp")
self.rect = self.image.get_rect()

# 每个外星人最初都在屏幕左上角附近
self.rect.x = self.rect.width
self.rect.y = self.rect.height

# 存储外星人的准确位置
self.x = float(self.rect.x)

def blitme(self):
"""在指定位置绘制外星人"""
self.screen.blit(self.image, self.rect)

它和Bullet类一样继承自Sprite类。现在开始创建多行外星人。

2.1 修改game_functions.py模块

首先在game_functions.py模块中添加create_fleet()函数用于创建外星舰队:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def create_fleet(ai_settings, screen, ship, aliens):
"""创建外星舰队"""
alien = Alien(ai_settings, screen)
# 计算每行能放多少个
number_aliens_x = get_number_aliens_x(ai_settings, alien.rect.width)
# 计算能放多少行
number_rows = get_number_rows(ai_settings, ship.rect.height,
alien.rect.height)

# 嵌套循环创建外星舰队
for row_number in range(number_rows):
for alien_number in range(number_aliens_x):
# 创建外星人并将其加入舰队
create_alien(ai_settings, screen, aliens, alien_number, row_number)

然后我们依次补充下面三个函数(注意各个函数的参数),这三个函数也位于game_functions.py中:

get_number_aliens_x(): 计算一行能放多少个外星人

1
2
3
4
5
6
7
def get_number_aliens_x(ai_settings, alien_width):
"""计算每行可容纳多少个外星人"""
# 左右两侧留出一个外星人的宽度
available_space_x = ai_settings.screen_width - 2 * alien_width
# 列间距为一个外星人宽度
number_aliens_x = int(available_space_x / (2 * alien_width))
return number_aliens_x

get_number_rows(): 计算能放多少行外星人

1
2
3
4
5
6
7
def get_number_rows(ai_settings, ship_height, alien_height):
"""计算屏幕可容纳多少行外星人"""
# 可用高度 = 窗口高度 - 上方一个外星人高度 - 下方一个飞船高度 - 两个外星人高度作为缓冲空间
available_space_y = (ai_settings.screen_height - 3 * alien_height - ship_height)
# 行距为一个外星人高度
number_rows = int(available_space_y / (2 * alien_height))
return number_rows

create_alien(): 创建外星人

1
2
3
4
5
6
7
8
def create_alien(ai_settings, screen, aliens, alien_number, row_number):
"""创建一个外星人并将其放在当前行"""
alien = Alien(ai_settings, screen)
# 下面就是根据上面的公式计算每一个外星人在窗口中的位置(这是左上角的坐标)
alien.x = alien.rect.width * (1 + 2 * alien_number)
alien.rect.x = alien.x
alien.rect.y = alien.rect.height * (1 + 2 * row_number)
aliens.add(alien)

现在我们还需要修改update_screen()函数:

1
2
3
4
5
def update_screen(ai_settings, screen, ship, bullets, aliens):
-- snip --
# 绘制外星人,放在绘制子弹的代码后面,让外星人的图像覆盖掉子弹的图像
aliens.draw(screen)
-- snip --

注意,该函数增加了一个参数aliens,这是个Group对象,所以代码中的draw()方法也跟前一篇中的bullets.update()方法一样,一行代码更新所有对象。

2.2 修改alien_invasion.py模块

在主程序中添加创建外星人的代码:

1
2
3
4
5
6
7
8
9
10
def run_game():
-- snip --
gf.create_fleet(ai_settings, screen, ship, aliens)

while True:
-- snip --
# 比之前代码多传入了一个aliens参数
gf.update_screen(ai_settings, screen, ship, bullets, aliens)

-- snip --

现在我们执行程序将会得到如下结果:

3. 让外星舰队动起来

我们将让外星舰队在窗体中向右移动,撞到屏幕边缘后下以一定距离下降,再沿反方向移动,直到外星人被消灭,或外星人撞上飞船,或有外星人到达窗体底部。

3.1 补充settings.py模块

1
2
3
4
5
6
7
class Settings:
def __init__(self):
-- snip --
self.fleet_drop_speed = 10
# 外星舰队方向标志:1向右,-1向左
# 也可以用如left, right之类的标志,但这样会很麻烦
self.fleet_direction = 1

3.2 修改alien.py模块

我们需要在Alien类中添加两个方法,一个用于检测窗体边缘,一个用于更新Alien对象:

1
2
3
4
5
6
7
8
9
10
11
12
class Alien(Sprite):
-- snip --
def check_edges(self):
"""如果外星人位于屏幕边缘则返回True"""
screen_rect = self.screen.get_rect()
return self.rect.right >= screen_rect.right or self.rect.left <= 0

def update(self):
"""向右移动外星人"""
# 以后这样的方式会用的很多
self.x += self.ai_settings.alien_speed_factor * self.ai_settings.fleet_direction
self.rect.x = self.x

如果使用文本值来控制方向,那就需要添加if-else语句来检测舰队移动方向。鉴于只有两个可能的方向,这里使用-11来表示,这样更容易改变外星人对象的坐标。

3.3 修改game_functions.py模块

首先,我们在该模块中添加一个更新外星舰队的函数update_aliens()

1
2
3
4
def update_aliens(ai_settings, aliens):
"""检查是否有外星人位于屏幕边缘,并更新外星舰队中所有外星人的位置"""
check_fleet_edges(ai_settings, aliens)
aliens.update() # “一键更新”

check_fleet_edges()函数用于检测舰队是否碰到了窗体边缘,代码如下:

1
2
3
4
5
6
7
def check_fleet_edges(ai_settings, aliens):
"""有外星人到达边缘时采取相应的措施"""
# 检测舰队中每一个外星人是否碰到了窗体边缘
for alien in aliens.sprites():
if alien.check_edges():
change_fleet_direction(ai_settings, aliens)
break

change_fleet_direction()函数用于改变舰队的移动方向,以及让舰队向下移动,代码如下:

1
2
3
4
5
def change_fleet_direction(ai_settings, aliens):
"""将外星舰队下移,并改变它们的方向"""
for alien in aliens.sprites():
alien.rect.y += ai_settings.fleet_drop_speed
ai_settings.fleet_direction *= -1

上面三个函数就是在game_functions.py中的所有变动。

3.4 修改alien_invasion.py模块

在该模块中我们只需要在while循环中添加一行代码:

1
2
3
4
5
6
7
8
# 开始游戏的主循环
while True:
gf.check_events(ai_settings, screen, ship, bullets)
ship.update()
gf.update_bullets(bullets)
# 添加对外星舰队的修改
gf.update_aliens(ai_settings, aliens)
gf.update_screen(ai_settings, screen, ship, bullets, aliens)

最后运行主程序,得到如下效果:

截了一张静态图,实际是动态的。

4. 击杀外星人

对于当前的程序,如果发射子弹,子弹将穿过外星人,而不是击杀,下面我们继续完善该项目,使其能击杀外星人。而要实现这一点,关键就是要检测到子弹图像与外星人图像是否重叠,重叠了则表示击中。

4.1 修改game_functions.py

为何检测子弹与卫星人的碰撞,我们需要修改update_bullets()函数,这里我们增加了update_bullets()的参数,还调用了一个新函数:

1
2
3
def update_bullets(bullets, aliens, ship, screen, ai_settings):
-- snip --
check_bullet_alien_collisions(ai_settings, screen, ship, aliens, bullets)

函数check_bullet_alien_collisions()用于检测子弹与外星人的碰撞,当外星人被消灭光时,清空现有子弹,并生成新的外星舰队,它的代码如下:

1
2
3
4
5
6
7
8
9
10
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)

sprite.groupcollide()方法用于检测对象之间的碰撞,它将bullets中的每个子弹的rectaliens中的每个外星人的rect进行比较,并返回一个字典。该字典以第一个参数中的对象为键,以第二个参数中的键为值,在这里,以bullets中发生了碰撞的bullet为键,它的值为与之碰撞的alien(不是aliens)!第三个参数表示是否删除第一个参数中发生了碰撞的对象,而四个参数表示是否删除第二个参数中发生了碰撞的对象。

4.2 修改alien_invasion.py

只需要修改调用update_bullets()函数的那行代码即可,增加几个参数:

1
gf.update_bullets(bullets, aliens, ship, screen, ai_settings)

基础功能基本完成。

4.3 测试技巧补充

对于上述代码,我们可能需要测试当消灭完外星人后,新的舰队是否能被正确创建等,如果我们以现在游戏的设定,即子弹速度为1,子弹宽度为3,那测试起来将会很痛苦。此时,我们可以修改修改游戏的参数,比如将子弹宽度修改为300,子弹速度修改为3,这样就相当于对游戏进行了快进,此时代码的运行效果如下:

不过最后记得将参数修改回去。

5. 结束游戏

接下来我们实现外星人碰到飞船,外星人抵达窗体底部,飞船数用光导致游戏结束的代码。

5.1 创建GameStats类

首先我们创建一个用于存储游戏信息的GameStats类,存放在game_stats.py文件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
class GameStats:
"""跟踪游戏的统计信息"""
def __init__(self, ai_settings):
"""初始化统计信息"""
# 用于控制游戏启动与否
self.game_active = True
self.ai_settings = ai_settings
self.reset_stats()

def reset_stats(self):
"""初始化在游戏运行期间可能变化的统计信息"""
# 重置飞船数
self.ships_left = self.ai_settings.ship_limit

5.2 修改settings.py

从上述代码可以看出,我们需要在settings.py中添加一项表示“飞船数”的信息:

1
2
3
4
5
6
7
8
class Settings:
def __init__(self):
"""初始化游戏的设置"""
# 屏幕设置
-- snip --
# 设置飞船数量限制
self.ship_limit = 3
-- snip --

5.3 响应飞船与外星人的碰撞,修改game_functions.py

我们在更新每个外星人的位置后立即检测外星人和飞船之间的碰撞,随后再检查外星人是否到达了窗体底部。修改update_aliens()函数,使用sprite中的spritecollideany()方法来检测碰撞:将第二参数中的每一个元素与第一个参数比较,检测是否碰撞,返回第二个参数中第一个发生碰撞的对象,如果没有发生碰撞则返回None:

1
2
3
4
5
6
7
8
# 增加了参数和碰撞检测
def update_aliens(ai_settings, aliens, ship, screen, bullets, stats):
-- snip --
# 检测外星人和飞船之间的碰撞
if pygame.sprite.spritecollideany(ship, aliens):
ship_hit(ai_settings, stats, screen, ship, aliens, bullets)

check_aliens_bottom(ai_settings, stats, screen, ship, aliens, bullets)

为此我们需要增加两个函数:

ship_hit():当外星人与飞船发生碰撞时,调用次函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
-- snip --
from time import sleep

def ship_hit(ai_settings, stats, screen, ship, aliens, bullets):
"""响应被外星人撞到的飞船"""
# 将ship_left减1
if stats.ships_left > 0:
stats.ships_left -= 1

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

# 创建一群新的外星人,并将飞船恢复到初始位置
create_fleet(ai_settings, screen, ship, aliens)
ship.center_ship()

# 暂停
sleep(0.5)
else:
stats.game_active = False

从上面的代码还可以看出,我们还需要在Ship类中添加一个center_ship()方法:

1
2
3
def center_ship(self):
"""让飞船在屏幕上居中"""
self.center = self.screen_rect.centerx

check_aliens_bottom(): 当飞船到达窗体底部时调用次函数

1
2
3
4
5
6
7
8
def check_aliens_bottom(ai_settings, stats, screen, ship, aliens, bullets):
"""检测是否有外星人到达了屏幕底部"""
screen_rect = screen.get_rect()
for alien in aliens.sprites():
if alien.rect.bottom >= screen_rect.bottom:
# 和飞船被碰撞是的代码没啥区别,故调用同一个函数
ship_hit(ai_settings, stats, screen, ship, aliens, bullets)
break

5.4 修改主程序alien_invasion.py

修改游戏的循环部分:

1
2
3
4
5
6
7
8
9
10
11
# 开始游戏的主循环
while True:
gf.check_events(ai_settings, screen, ship, bullets)

# 决定程序运行时该执行的部分
if stats.game_active:
ship.update()
gf.update_bullets(bullets, aliens, ship, screen, ai_settings)
gf.update_aliens(ai_settings, aliens, ship, screen, bullets, stats)

gf.update_screen(ai_settings, screen, ship, bullets, aliens)

在主循环中,任何情况下都需要调用check_events(),即使游戏处于非活动状态;还需要不断更新屏幕,以便在等待玩家是否选择重新开始游戏时能够修改屏幕;其他函数仅在游戏处于活动状态时太需要调用。

6. 小结

本篇讲述了:

  • 如何在游戏中添加大量相同的元素;
  • 如何用嵌套循环来创建元素网格;
  • 如何控制对象在屏幕上移动的方向以及响应事件;
  • 如何检测和响应元素碰撞;
  • 如何在游戏中跟踪统计信息;
  • 如何使用标志game_active来判断游戏是否结束。

下一篇中,同时也是本项目的最后一篇,我们将:

  • 添加一个Play按钮让玩家能够开始游戏,以及游戏结束后再开始;
  • 每当玩家消灭一群外星人后,加快游戏节奏;
  • 添加一个分数系统。
VPointer wechat
欢迎大家关注我的微信公众号"代码港"~~
您的慷慨将鼓励我继续创作~~