Python应用实例(一)外星人入侵(七)
外星人入侵(七)
1.射杀外星人
我们创建了飞船和外星人群,但子弹击中外星人时将穿过外星人,因为还没有检查碰撞。在游戏编程中,碰撞指的是游戏元素重叠在一起。要让子弹能够击落外星人,我们将使用sprite.groupcollide()检测两个编组的成员之间的碰撞。
1.1 检测子弹与外星人的碰撞
子弹击中外星人时,我们需要马上知道,以便碰撞发生后让子弹立即消失。为此,我们将在更新子弹的位置后立即检测碰撞。
函数sprite.groupcollide()将一个编组中每个元素的rect同另一个编组中每个元素的rect进行比较。在这里,是将每颗子弹的rect同每个外星人的rect进行比较,并返回一个字典,其中包含发生了碰撞的子弹和外星人。在这个字典中,每个键都是一颗子弹,而关联的值是被该子弹击中的外星人。
在方法_update_bullets()末尾,添加如下检查子弹和外星人碰撞的代码:alien_invasion.py
def _update_bullets(self):
"""更新子弹的位置,并删除消失的子弹。"""
--snip--
# 检查是否有子弹击中了外星人。
# 如果是,就删除相应的子弹和外星人。
collisions = pygame.sprite.groupcollide(
self.bullets, self.aliens, True, True)
这些新增的代码将self.bullets中所有的子弹都与self.aliens中所有的外星人进行比较,看它们是否重叠在一起。每当有子弹和外星人的rect重叠时,groupcollide()就在它返回的字典中添加一个键值对。两个实参True让Pygame删除发生碰撞的子弹和外星人。(要模拟能够飞行到屏幕顶端、消灭击中的每个外星人的高能子弹,可将第一个布尔实参设置为False,并保留第二个布尔参数为True。这样被击中的外星人将消失,但所有的子弹都始终有效,直到抵达屏幕顶端后消失。)
如果此时运行这个游戏,被击中的外星人将消失。如图13-5所示,有些外星人被射杀了。
1.2 为测试创建大子弹
只需运行这个游戏就可测试很多功能,但有些功能在正常情况下测试起来比较烦琐。例如,要测试代码能否正确处理外星人编组为空的情形,需要花很长时间将屏幕上的外星人全部射杀。
测试有些功能时,可以修改游戏的某些设置,以便能够专注于游戏的特定方面。例如,可以缩小屏幕以减少需要射杀的外星人数量,也可以提高子弹的速度,以便能够在单位时间内发射大量子弹。
测试这个游戏时,我喜欢做的一项修改是,增大子弹的尺寸并使其在击中外星人后依然有效,如图13-6所示。请尝试将bullet_width设置为300乃至3000,看看将所有外星人全部射杀有多快!
这样的修改可提高测试效率,还可能激发出如何赋予玩家更大威力的思想火花。(完成测试后,别忘了将设置恢复正常。)
1.3 生成新的外星人群
这个游戏的一个重要特点是,外星人无穷无尽:一群外星人被消灭后,又会出现另一群外星人。
要在一群外星人被消灭后再显示一群外星人,首先需要检查编组aliens是否为空。如果是,就调用_create_fleet()。我们将在_update_bullets()末尾执行这项任务,因为外星人都是在这里被消灭的:
alien_invasion.py
def _update_bullets(self):
--snip--
❶ if not self.aliens:
# 删除现有的子弹并新建一群外星人。
❷ self.bullets.empty()
self._create_fleet()
在❶处,检查编组aliens是否为空。空编组相当于False,因此这是一种检查编组是否为空的简单方式。如果编组aliens为空,就使用方法empty()删除编组中余下的所有精灵,从而删除现有的所有子弹(见❷)。我们还调用了_create_fleet(),在屏幕上重新显示一群外星人。
现在,当前这群外星人被消灭干净后,将立刻出现一群新的外星人。
1.4 提高子弹的速度
如果现在尝试在游戏中射杀外星人,可能会发现子弹的速度不太合适(有点快或有点慢),游戏感不好。当前,可通过修改设置让这款游戏更有意思、更好玩。
要修改子弹的速度,可调整settings.py中bullet_speed的值。在我的系统中,我把bullet_speed的值调整到1.5,让子弹的速度快些:
# 子弹设置
self.bullet_speed = 1.5
self.bullet_width = 3
--snip--
这项设置的最佳值取决于你使用的系统的速度,请找出适合自己的值。你也可以调整其他设置。
1.5 重构_update_bullets()
下面来重构_update_bullets(),使其不再执行那么多任务。为此,将处理子弹和外星人碰撞的代码移到一个独立的方法中:
alien_invasion.py
def _update_bullets(self):
--snip--
# 删除消失的子弹。
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.bullets.empty()
self._create_fleet()
我们创建了一个新方法_check_bullet_alien_collisions(),用于检测子弹和外星人之间的碰撞,并在整群外星人被消灭干净时采取相应的措施。这能避免_update_bullets()过长,简化了后续开发工作。
2.结束游戏
如果玩家根本不会输,游戏还有什么趣味和挑战性可言?如果玩家没能在足够短的时间内将整群外星人消灭干净,导致有外星人撞到了飞船或抵达屏幕底端,飞船将被摧毁。与此同时,限制玩家可使用的飞船数,在玩家用光所有的飞船后,游戏将结束。
2.1 检测外星人和飞船碰撞
首先检查外星人和飞船之间的碰撞,以便在外星人撞上飞船时做出合适的响应。为此,在AlienInvasion中更新每个外星人的位置后,立即检测外星人和飞船之间的碰撞:
alien_invasion.py
def _update_aliens(self):
--snip--
self.aliens.update()
# 检测外星人和飞船之间的碰撞。
❶ if pygame.sprite.spritecollideany(self.ship, self.aliens):
❷ print("Ship hit!!!")
函数spritecollideany()接受两个实参:一个精灵和一个编组。它检查编组是否有成员与精灵发生了碰撞,并在找到与精灵发生碰撞的成员后停止遍历编组。在这里,它遍历编组aliens,并返回找到的第一个与飞船发生碰撞的外星人。
如果没有发生碰撞,spritecollideany()将返回None,因此❶处的if代码块不会执行。如果找到了与飞船发生碰撞的外星人,它就返回这个外星人,因此if代码块将执行:打印“Ship hit!!!”(见❷)。有外星人撞到飞船时,需要执行很多任务:删除余下的外星人和子弹,让飞船重新居中,以及创建一群新的外星人。编写完成这些任务的代码之前,需要确定检测外星人和飞船碰撞的方法是否可行。为此,最简单的方式就是调用函数print()。
现在如果运行这个游戏,则每当有外星人撞到飞船时,终端窗口都将显示“Ship hit!!!”。测试这项功能时,请将alien_drop_speed设置为较大的值,如50或100,这样外星人将更快地撞到飞船。
2.2 响应外星人和飞船碰撞
现在需要确定当外星人与飞船发生碰撞时该做些什么。我们不销毁Ship实例并创建新的,而是通过跟踪游戏的统计信息来记录飞船被撞了多少次(跟踪统计信息还有助于记分)。
下面来编写一个用于跟踪游戏统计信息的新类GameStats,并将其保存为文件game_stats.py:
class GameStats:
"""跟踪游戏的统计信息。"""
def __init__(self, ai_game):
"""初始化统计信息。"""
self.settings = ai_game.settings
self.reset_stats()
def reset_stats(self):
"""初始化在游戏运行期间可能变化的统计信息。"""
self.ships_left = self.settings.ship_limit
在游戏运行期间,只创建一个GameStats实例,但每当玩家开始新游戏时,需要重置一些统计信息。为此,在方法reset_stats()中初始化大部分统计信息,而不是在__init__()中直接初始化。我们在__init__()中调用这个方法,这样创建GameStats实例时将妥善地设置这些统计信息,在玩家开始新游戏时也能调用reset_stats()。
当前,只有一项统计信息ships_left,其值在游戏运行期间不断变化。一开始玩家拥有的飞船数存储在settings.py的ship_limit中:
settings.py
# 飞船设置
self.ship_speed = 1.5
self.ship_limit = 3
还需对alien_invasion.py做些修改,以创建一个GameStats实例。首先,更新这个文件开头的import语句:
alien_invasion.py
import sys
from time import sleep
import pygame
from settings import Settings
from game_stats import GameStats
from ship import Ship
--snip--
从Python标准库的模块time中导入函数sleep(),以便在飞船被外星人撞到后让游戏暂停片刻。我们还导入了GameStats。
接下来,在__init__()中创建一个GameStats实例:
alien_invasion.py
def __init__(self):
--snip--
self.screen = pygame.display.set_mode(
(self.settings.screen_width, self.settings.screen_height))
pygame.display.set_caption("Alien Invasion")
# 创建一个用于存储游戏统计信息的实例。
self.stats = GameStats(self)
self.ship = Ship(self)
--snip--
在创建游戏窗口后、定义诸如飞船等其他游戏元素前,创建一个GameStats实例。
有外星人撞到飞船时,将余下的飞船数减1,创建一群新的外星人,并将飞船重新放到屏幕底端的中央。另外,让游戏暂停片刻,让玩家在新外星人群出现前注意到发生了碰撞并将重新创建外星人群。
下面将实现这些功能的大部分代码放到新方法_ship_hit()中。在_update_aliens()中,将在有外星人撞到飞船时调用这个方法:
alien_invasion.py
def _ship_hit(self):
"""响应飞船被外星人撞到。"""
# 将ships_left减1。
❶ self.stats.ships_left -= 1
# 清空余下的外星人和子弹。
❷ self.aliens.empty()
self.bullets.empty()
# 创建一群新的外星人,并将飞船放到屏幕底端的中央。
❸ self._create_fleet()
self.ship.center_ship()
# 暂停。
❹ sleep(0.5)
新方法_ship_hit()在飞船被外星人撞到时做出响应。在这个方法中,将余下的飞船数减1(见❶),再清空编组aliens和bullets(见❷)。
接下来,创建一群新的外星人,并将飞船居中(见❸)。(稍后将在Ship类中添加方法center_ship()。)最后,在更新所有元素后(但在将修改显示到屏幕前)暂停,让玩家知道飞船被撞到了(见❹)。这里的函数调用sleep()让游戏暂停半秒钟,让玩家能够看到外星人撞到了飞船。函数sleep()执行完毕后,将接着执行方法_update_screen(),将新的外星人群绘制到屏幕上。
在_update_aliens()中,当有外星人撞到飞船时,不调用函数print(),而调用_ship_hit():
ship.py
def center_ship(self):
"""让飞船在屏幕底端居中。"""
self.rect.midbottom = self.screen_rect.midbottom
self.x = float(self.rect.x)
这里像__init__()中那样让飞船在屏幕底端居中。让飞船在屏幕底端居中后,重置用于跟踪飞船确切位置的属性self.x。
注意 我们根本没有创建多艘飞船。在整个游戏运行期间,只创建了一个飞船实例,并在该飞船被撞到时将其居中。统计信息ships_left指出玩家是否用完了所有的飞船。
请运行这个游戏,射杀几个外星人,并让一个外星人撞到飞船。游戏暂停片刻后,将出现一群新的外星人,而飞船将在屏幕底端居中。
2.3 有外星人到达屏幕底端
如果有外星人到达屏幕底端,我们将像有外星人撞到飞船那样做出响应。为检测这种情况,在alien_invasion.py中添加一个新方法:a
lien_invasion.py
def _check_aliens_bottom(self):
"""检查是否有外星人到达了屏幕底端。"""
screen_rect = self.screen.get_rect()
for alien in self.aliens.sprites():
❶ if alien.rect.bottom >= screen_rect.bottom:
# 像飞船被撞到一样处理。
self._ship_hit()
break
方法_check_aliens_bottom()检查是否有外星人到达了屏幕底端。到达屏幕底端后,外星人的属性rect.bottom大于或等于屏幕的属性rect.bottom(见❶)。如果有外星人到达屏幕底端,就调用_ship_hit()。只要检测到一个外星人到达屏幕底端,就无须检查其他外星人了,因此在调用_ship_hit()后退出循环。
我们在_update_aliens()中调用_check_aliens_bottom():
alien_invasion.py
def _update_aliens(self):
--snip--
# 检查是否有外星人撞到飞船。
if pygame.sprite.spritecollideany(self.ship, self.aliens):
self._ship_hit()
# 检查是否有外星人到达了屏幕底端。
self._check_aliens_bottom()
在更新所有外星人的位置并检测是否有外星人和飞船发生碰撞后调用_check_aliens_bottom()。现在,每当有外星人撞到飞船或抵达屏幕底端时,都将出现一群新的外星人。
2.4 游戏结束
现在这个游戏看起来更完整了,但它永远都不会结束,只是ships_left不断变成越来越小的负数。下面在GameStats中添加一个作为标志的属性game_active,以便在玩家的飞船用完后结束游戏。首先,在GameStats类的方法__init__()末尾设置这个标志:
game_stats.py
def __init__(self, ai_game):
--snip--
# 游戏刚启动时处于活动状态。
self.game_active = True
接下来在_ship_hit()中添加代码,在玩家的飞船用完后将game_active设置为False:
alien_invasion.py
def _ship_hit(self):
"""响应飞船被外星人撞到。"""
if self.stats.ships_left > 0:
# 将ships_left减1。
self.stats.ships_left -= 1
--snip--
# 暂停。
sleep(0.5)
else:
self.stats.game_active = False
_ship_hit()的大部分代码没有变。我们将原来的代码都移到了一个if语句块中,它检查玩家是否至少还有一艘飞船。如果是,就创建一群新的外星人,暂停片刻,再接着往下执行。如果玩家没有了飞船,就将game_active设置为False。
3.确定应运行游戏的哪些部分
我们需要确定游戏的哪些部分在任何情况下都应运行,哪些部分仅在游戏处于活动状态时才运行:
alien_invasion.py
def run_game(self):
"""开始游戏主循环。"""
while True:
self._check_events()
if self.stats.game_active:
self.ship.update()
self._update_bullets()
self._update_aliens()
self._update_screen()
在主循环中,在任何情况下都需要调用_check_events(),即便游戏处于非活动状态。例如,我们需要知道玩家是否按了Q键以退出游戏,或者是否单击了关闭窗口的按钮。我们还需要不断更新屏幕,以便在等待玩家是否选择开始新游戏时修改屏幕。其他的函数仅在游戏处于活动状态时才需要调用,因为游戏处于非活动状态时,不用更新游戏元素的位置。
现在运行这个游戏,它将在飞船用完后停止不动。