07.外星人

在屏幕上边缘附近添加一个外星人,然后生成一群外星人。我们让这群外星人向两边和下面移动,并删除被子弹击中的外星人。最后,我们将显示玩家拥有的飞船数量,并在玩家的飞船用完后结束游戏。

本章将学习如何检测游戏对象之间的碰撞,如子弹和外星人之间的碰撞。检测碰撞有助于你定义游戏元素之间的交互:可以将角色限定在迷宫墙壁之内或在两个角色之间传球。

我们将时不时地查看游戏开发计划,以确保编程工作不偏离轨道。编写代码前,先来回顾一下这个项目,并更新开发计划。

1、回顾项目

开发较大的项目时,进入每个开发阶段前回顾一下开发计划,搞清楚接下来要通过编写代码来完成哪些任务都是不错的主意。本章涉及以下内容:

  • 研究既有代码,确定实现新功能前是否要进行重构。
  • 在屏幕左上角添加一个外星人,并指定合适的边距。
  • 根据第一个外星人的边距和屏幕尺寸计算屏幕上可容纳多少个外星人。我们将编写一个循环来创建一系列外星人,这些外星人填满了屏幕的上半部分。
  • 让外星人群向两边和下方移动,直到外星人被全部击落,有外星人撞到飞船,或有外星人抵达屏幕底端。如果整群外星人都被击落,我们将再创建一群外星人。如果有外星人撞到了飞船或抵达屏幕底端,我们将销毁飞船并再创建一群外星人。
  • 限制玩家可用的飞船数量,配给的飞船用完后,游戏结束。

我们将在实现功能的同时完善这个计划,但就目前而言,该计划已足够详尽。

在给项目添加新功能前,还应审核既有代码。每进入一个新阶段,通常项目都会更复杂,因此最好对混乱或低效的代码进行清理。

我们在开发的同时一直不断地重构,因此当前需要做的清理工作不多,但每次为测试新功能而运行这个游戏时,都必须使用鼠标来关闭它,这太讨厌了。下面来添加一个结束游戏的快捷键Q:game_functions.py

def check_keydown_events(event, ai_settings, screen, ship, bullets):
    """响应按键"""
    if event.key == pygame.K_RIGHT:
        --snip--
    elif event.key == pygame.K_q:
        sys.exit()
2、创建第一个外星人
a. 创建Alien类

在屏幕上放置外星人与放置飞船类似。每个外星人的行为都由Alien类控制,我们将像创建Ship类那样创建这个类。出于简化考虑,我们也使用位图来表示外星人。请务必将你选择的图像文件保存到文件夹images中。

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)

除位置不同外,这个类的大部分代码都与Ship类相似。每个外星人最初都位于屏幕左上角附近,我们将每个外星人的左边距都设置为外星人的宽度,并将上边距设置为外星人的高度。

b. 创建Alien实例

在alien_invasion.py中创建一个Alien实例:

from alien import Alien

def run_game():
	# 创建一个外星人
	alien = Alien(ai_settings, screen)
    
    # 开始游戏的主循环
	while True:
		# 调用绘制函数
		gf.update_screen(ai_settings, screen, ship, alien, bullets)
  • 导入了新创建的Alien类,并在进入主while循环前创建了一个Alien实例。
  • 我们没有修改外星人的位置,因此该while循环没有任何新东西,但我们修改了对update_screen()的调用,传递了一个外星人实例。
c. 让外星人出现在屏幕上

为让外星人出现在屏幕上,我们在update_screen()中调用其方法blitme():

def update_screen(ai_settings, screen, ship, alien bullets):
	""" 更新屏幕上的图像,并切换到新屏幕 """
	--snip--
	alien.blitme()
	
	# 让最近绘制的屏幕可见
	pygame.display.flip()
3、创建一群外星人

要绘制一群外星人,需要确定一行能容纳多少个外星人以及要绘制多少行外星人。我们将首先计算外星人之间的水平间距,并创建一行外星人,再确定可用的垂直空间,并创建整群外星人。

a. 确定一行可以容纳多少个外星人

为确定一行可容纳多少个外星人,我们来看看可用的水平空间有多大。屏幕宽度存储在ai_settings.screen_width中,但需要在屏幕两边都留下一定的边距,把它设置为外星人的宽度。由于有两个边距,因此可用于放置外星人的水平空间为屏幕宽度减去外星人宽度的两倍:

available_space_x = ai_settings.screen_width – (2 * alien_width)

我们还需要在外星人之间留出一定的空间,即外星人宽度。因此,显示一个外星人所需的水平空间为外星人宽度的两倍:一个宽度用于放置外星人,另一个宽度为外星人右边的空白区域。为确定一行可容纳多少个外星人,我们将可用空间除以外星人宽度的两倍:

number_aliens_x = available_space_x / (2 * alien_width)

注意:令人欣慰的是,在程序中执行计算时,一开始你无需确定公式是正确的,而可以尝试直接运行程序,看看结果是否符合预期。即便是在最糟糕的情况下,也只是屏幕上显示的外星人太多或太少。你可以根据在屏幕上看到的情况调整计算公式。

b. 创建多个外星人

为创建一行外星人,首先在alien_invasion.py中创建一个名为aliens的空编组,用于存储全部外星人,再调用game_functions.py中创建外星人群的函数:

import pygame


from settings import Settings
from ship import Ship
from pygame.sprite import Group
import game_functions as gf


def run_game():
	--snip--
	
	# 创建一个外星人编组
	aliens = Group()
	
	
	# 创建外星人群
	gf.create_fleet(ai_settings, screen, aliens)
	
	# 开始游戏的主循环
	while True:
		--snip--
		
		# 调用绘制函数
		gf.update_screen(ai_settings, screen, ship, aliens, bullets)

run_game()
  • 由于我们不再在alien_invasion.py中直接创建外星人,因此无需在这个文件中导入Alien类。
  • 创建了一个空编组,用于存储所有的外星人。
  • 接下来,调用稍后将编写的函数create_fleet(),并将ai_settings、对象screen和空编组aliens传递给它。

修改对update_screen()的调用,让它能够访问外星人编组,我们还需要修改update_screen():

def update_screen(ai_settings, screen, ship, aliens, bullets):
	""" 更新屏幕上的图像,并切换到新屏幕 """
	# 每次循环时都重绘屏幕
	screen.fill(ai_settings.bg_color)
	# 在飞船和外星人后面重绘所有子弹
	for bullet in bullets.sprites():
		bullet.draw_bullet()
	ship.blitme()
	aliens.draw(screen)
	
	# 让最近绘制的屏幕可见
	pygame.display.flip()
c. 创建外星人群

现在可以创建外星人群了。下面是新函数create_fleet(),我们将它放在game_functions.py的末尾。我们还需要导入Alien类,因此务必在文件game_functions.py开头添加相应的import语句:

from alien import Alien

def create_fleet(ai_settings, screen, aliens):
	"""创建外星人群"""
	# 创建一个外星人并计算一行可以容纳多少个外星人
	# 外星人间距为外星人的宽度
	alien = Alien(ai_settings, screen)
	alien_width = alien.rect.width
	available_space_x = ai_settings.screen_width - 2 * alien_width
	number_aliens_x = int(available_space_x / (2 * alien_width))
	
	# 创建第一行外星人
	for alien_number in range(number_aliens_x):
		alien = Alien(ai_settings, screen)
		# 外星人所在的位置 = 左边空闲宽度 + 外星人预设宽度(外星人宽度的2倍) * 是第几个
		alien.x = alien_width + 2 * alien_width * alien_number
		alien.rect.x = alien.x
		aliens.add(alien)
  • 为放置外星人,我们需要知道外星人的宽度和高度,因此在执行计算前,我们先创建一个外星人。这个外星人不是外星人群的成员,因此没有将它加入到编组aliens中。
  • 从外星人的rect属性中获取外星人宽度,并将这个值存储到alien_width中,以免反复访问属性rect。
  • 计算可用于放置外星人的水平空间,以及其中可容纳多少个外星人。
    • 使用了int()来确保计算得到的外星人数量为整数,因为我们不希望某个外星人只显示一部分,而且函数range()也需要一个整数。
    • 函数int()将小数部分丢弃,相当于向下圆整(这大有裨益,因为我们宁愿每行都多出一点点空间,也不希望每行的外星人之间过于拥挤)。
  • 编写了一个循环,它从零数到要创建的外星人数。在这个循环的主体中,我们创建一个新的外星人,并通过设置x坐标将其加入当前行。
  • 将每个外星人都往右推一个外星人的宽度。接下来,我们将外星人宽度乘以2,得到每个外星人占据的空间(其中包括其右边的空白区域),再据此计算当前外星人在当前行的位置。最后,我们将每个新创建的外星人都添加到编组aliens中。
  • 这行外星人在屏幕上稍微偏向了左边,这实际上是有好处的,因为我们将让外星人群往右移,触及屏幕边缘后稍微往下移,然后往左移,以此类推。
d. 重构create_fleet()
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
	

def create_alien(ai_settings, screen, aliens, alien_number):
	"""创建一个外星人并将其放在当前行"""
	alien = Alien(ai_settings, screen)
	alien_width = alien.rect.width
	# 外星人所在的位置 = 左边空闲宽度 + 外星人预设宽度(外星人宽度的2倍) * 是第几个
	alien.x = alien_width + 2 * alien_width * alien_number
	alien.rect.x = alien.x
	aliens.add(alien)
	

def create_fleet(ai_settings, screen, aliens):
	"""创建外星人群"""
	# 创建一个外星人并计算一行可以容纳多少个外星人
	alien = Alien(ai_settings, screen)
	number_aliens_x = get_number_aliens_x(ai_settings, alien.rect.width)
	
	# 创建第一行外星人
	for alien_number in range(number_aliens_x):
		create_alien(ai_settings, screen, aliens, alien_number)
  • 函数get_number_aliens_x()的代码都来自create_fleet(),且未做任何修改。
  • 函数create_alien()的代码也都来自create_fleet(),且未做任何修改,只是使用刚创建的外星人来获取外星人宽度。
  • 我们将计算可用水平空间的代码替换为对get_number_aliens_x()的调用,并删除了引用alien_width的代码行,因为现在这是在create_alien()中处理的。
  • 调用create_alien()。通过这样的重构,添加新行进而创建整群外星人将更容易。
e. 添加行

要创建外星人群,需要计算屏幕可容纳多少行,并对创建一行外星人的循环重复相应的次数。

为计算可容纳的行数,我们这样计算可用垂直空间:将屏幕高度减去第一行外星人的上边距(外星人高度)、飞船的高度以及最初外星人高度加上外星人间距(外星人高度的两倍):

available_space_y = ai_settings.screen_height – 3 * alien_height – ship_height

这将在飞船上方留出一定的空白区域,给玩家留出射杀外星人的时间。

每行下方都要留出一定的空白区域,并将其设置为外星人的高度。为计算可容纳的行数,我们将可用垂直空间除以外星人高度的两倍(同样,如果这样的计算不对,我们马上就能发现,继而将间距调整为合理的值)。

number_rows = available_space_y / (2 * alien_height)

在create_fleet()的定义中,还新增了一个用于存储ship对象的形参,因此在alien_invasion.py中调用create_fleet()时,需要传递实参ship:

# 创建外星人群
gf.create_fleet(ai_settings, screen, ship, aliens)

知道可容纳多少行后,便可重复执行创建一行外星人的代码:

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


def create_alien(ai_settings, screen, aliens, alien_number, row_number):
	"""创建一个外星人并将其放在当前行"""
	alien = Alien(ai_settings, screen)
	alien_width = alien.rect.width
	# 外星人所在的位置 = 左边空闲宽度 + 外星人预设宽度(外星人宽度的2倍) * 是第几个
	alien.x = alien_width + 2 * alien_width * alien_number
	alien.rect.x = alien.x
	alien.rect.y = alien.rect.height + 2 * alien.rect.height * row_number
	aliens.add(alien)

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)
  • 为计算屏幕可容纳多少行外星人,我们在函数get_number_rows()中实现了前面计算available_space_y和number_rows的公式。这里使用了int(),因为我们不想创建不完整的外星人行。
  • 为创建多行,我们使用两个嵌套在一起的循环:一个外部循环和一个内部循环。其中的内部循环创建一行外星人,而外部循环从零数到要创建的外星人行数。
  • 为嵌套循环,我们编写了一个新的for循环,并缩进了要重复执行的代码。
    • 我们调用create_alien()时,传递了一个表示行号的实参,将每行都沿屏幕依次向下放置。
    • create_alien()的定义需要一个用于存储行号的形参。在create_alien()中,我们修改外星人的y坐标,并在第一行外星人上方留出与外星人等高的空白区域。相邻外星人行的y坐标相差外星人高度的两倍,因此我们将外星人高度乘以2,再乘以行号。
    • 第一行的行号为0,因此第一行的垂直位置不变,而其他行沿屏幕依次向下放置。
4、让外星人移动

来让外星人群在屏幕上向右移动,撞到屏幕边缘后下移一定的距离,再沿相反的方向移动。我们将不断地移动所有的外星人,直到所有外星人都被消灭,有外星人撞上飞船,或有外星人抵达屏幕底端。下面先来让外星人向右移动。

a. 向右移动外星人

为移动外星人,我们将使用alien.py中的方法update(),且对外星人群中的每个外星人都调用它。首先,添加一个控制外星人速度的设置:settings.py

 def __init__(self):
        --snip--
        # 外星人设置
        self.alien_speed_factor = 1

然后,使用这个设置来实现update():alien.py

def update(self):
    """向右移动外星人"""
    self.x += self.ai_settings.alien_speed_factor
    self.rect.x = self.x
  • 每次更新外星人位置时,都将它向右移动,移动量为alien_speed_factor的值。
  • 我们使用属性self.x跟踪每个外星人的准确位置,这个属性可存储小数值。然后,我们使用self.x的值来更新外星人的rect的位置。

在主while循环中已调用了更新飞船和子弹的方法,但现在还需更新每个外星人的位置:alien_invasion.py

# 开始游戏的主循环
while True:
    # 更新子弹的位置 , 删除已消失的子弹
    gf.update_bullets(bullets)
    # 更新外星人群位置
    gf.update_aliens(aliens)
  • 在更新子弹后再更新外星人的位置,因为稍后要检查是否有子弹撞到了外星人。

最后,在文件game_functions.py末尾添加新函数update_aliens():

def update_aliens(aliens):
	aliens.update()
  • 我们对编组aliens调用方法update(),这将自动对每个外星人调用方法update()。
  • 如果你现在运行这个游戏,会看到外星人群向右移,并逐渐在屏幕右边缘消失。
b. 创建表示外星人移动方向的设置

下面来创建让外星人撞到屏幕右边缘后向下移动、再向左移动的设置。实现这种行为的代码如下:settings.py

def __init__(self):
    # 外星人设置
    self.alien_speed_factor = 1
    self.fleet_drop_speed = 10
    # fleet_direction为 1 表示向右移动,为 -1 表示向左移动
    self.fleet_direction = 1
  • 设置fleet_drop_speed指定了有外星人撞到屏幕边缘时,外星人群向下移动的速度。将这个速度与水平速度分开是有好处的,这样你就可以分别调整这两种速度了。
  • 要实现fleet_direction设置,可以将其设置为文本值,如'left'或'right',但这样就必须编写if-elif语句来检查外星人群的移动方向。
    • 鉴于只有两个可能的方向,我们使用值1和-1来表示它们,并在外星人群改变方向时在这两个值之间切换。
    • 另外,鉴于向右移动时需要增大每个外星人的x坐标,而向左移动时需要减小每个外星人的x坐标,使用数字来表示方向更合理。
c. 检查外星人是否撞到了屏幕边缘

现在需要编写一个方法来检查是否有外星人撞到了屏幕边缘,还需修改update(),以让每个外星人都沿正确的方向移动:

def check_edges(self):
		"""如果外星人位于屏幕边缘,就返回True"""
		screen_rect = self.screen.get_rect()
		if self.rect.right >= screen_rect.right:
			return True
		elif self.rect.left <= 0:
			return True
		
	
def update(self):
    """向左或向右移动外星人"""
    self.x += (self.ai_settings.alien_speed_factor *
                self.ai_settings.fleet_direction)
    self.rect.x = self.x
  • 我们可对任何外星人调用新方法check_edges(),看看它是否位于屏幕左边缘或右边缘。
    • 如果外星人的rect的right属性大于或等于屏幕的rect的right属性,就说明外星人位于屏幕右边缘。
    • 如果外星人的rect的left属性小于或等于0,就说明外星人位于屏幕左边缘。
  • 我们修改了方法update(),将移动量设置为外星人速度和fleet_direction的乘积,让外星人向左或向右移。
    • 如果fleet_direction为1,就将外星人当前的 x 坐标增大alien_speed_factor,从而将外星人向右移;
    • 如果fleet_direction为-1,就将外星人当前的 x 坐标减去alien_speed_factor,从而将外星人向左移。
d. 向下移动外星人群并改变移动方向

有外星人到达屏幕边缘时,需要将整群外星人下移,并改变它们的移动方向。

我们需要对game_functions.py做重大修改,因为我们要在这里检查是否有外星人到达了左边缘或右边缘。为此,我们编写函数check_fleet_edges()和change_fleet_direction(),并对update_aliens()进行修改:

def check_fleet_edges(ai_settings, aliens):
	"""有外星人到达边缘时采取响应的措施"""
	for alien in aliens.sprites():
		if alien.check_edges():
			change_fleet_direction(ai_settings, aliens)
			break
			
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
							
def update_aliens(ai_settings, aliens):
	"""
	检查是否有外星人位于屏幕边缘,并更新整群外星人的位置
	"""
	check_fleet_edges(ai_settings, aliens)
	aliens.update()
  • check_fleet_edges()中,我们遍历外星人群,并对其中的每个外星人调用check_edges()。如果check_edges()返回True,我们就知道相应的外星人位于屏幕边缘,需要改变外星人群的方向,因此我们调用change_fleet_direction()并退出循环。
  • change_fleet_direction()中,我们遍历所有外星人,将每个外星人下移fleet_drop_speed设置的值;然后,将fleet_direction的值修改为其当前值与-1的乘积。
  • 修改函数update_aliens(),在其中通过调用check_fleet_edges()来确定是否有外星人位于屏幕边缘。现在,函数update_aliens()包含形参ai_settings,因此我们调用它时指定了与ai_settings对应的实参:
# 开始游戏的主循环
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, aliens, bullets)

现在运行这个游戏,外星人群将在屏幕上来回移动,并在抵达屏幕边缘后向下移动。现在可以开始射杀外星人,检查是否有外星人撞到飞船,或抵达了屏幕底端。

5、射杀外星人

创建了飞船和外星人群,但子弹击中外星人时,将穿过外星人,因为我们还没有检查碰撞。在游戏编程中,碰撞指的是游戏元素重叠在一起。要让子弹能够击落外星人,我们将使用sprite.groupcollide()检测两个编组的成员之间的碰撞。

a. 检测子弹与外星人的碰撞

子弹击中外星人时,我们要马上知道,以便碰撞发生后让外星人立即消失。为此,我们将在更新子弹的位置后立即检测碰撞。

方法sprite.groupcollide()将每颗子弹的rect同每个外星人的rect进行比较,并返回一个字典,其中包含发生了碰撞的子弹和外星人。在这个字典中,每个键都是一颗子弹,而相应的值都是被击中的外星人。在函数update_bullets()中,使用下面的代码来检查碰撞:

def update_bullets(aliens, bullets):
	"""更新子弹的位置,并删除已消失的子弹"""
	# 更新子弹的位置
	bullets.update()
	
	# 删除已消失的子弹
	for bullet in bullets.copy():
		if bullet.rect.bottom <= 0:
			bullets.remove(bullet)
	
	# 检查是否有子弹集中了外星人
	# 如果是这样,就删除响应的子弹和外星人
	collisions = pygame.sprite.groupcollide(bullets, aliens, True, True)
  • 新增的这行代码遍历编组bullets中的每颗子弹,再遍历编组aliens中的每个外星人。每当有子弹和外星人的rect重叠时,groupcollide()就在它返回的字典中添加一个键-值对。
  • 两个实参True告诉Pygame删除发生碰撞的子弹和外星人。

调用update_bullets()时,传递了实参aliens:

# 开始游戏的主循环
while True:
    --snip--
    # 更新子弹的位置 , 删除已消失的子弹
    gf.update_bullets(aliens, bullets)
    --snip--
b. 为测试创建大子弹

只需通过运行这个游戏就可以测试其很多功能,但有些功能在正常情况下测试起来比较烦琐。例如,要测试代码能否正确地处理外星人编组为空的情形,需要花很长时间将屏幕上的外星人都击落。

测试有些功能时,可以修改游戏的某些设置,以便专注于游戏的特定方面。例如,可以缩小屏幕以减少需要击落的外星人数量,也可以提高子弹的速度,以便能够在单位时间内发射大量子弹。

测试这个游戏时,我喜欢做的一项修改是增大子弹的尺寸,使其在击中外星人后依然有效,如图13-6所示。请尝试将bullet_width设置为300,看看将所有外星人都射杀有多快!

类似这样的修改可提高测试效率,还可能激发出如何赋予玩家更大威力的思想火花。(完成测试后,别忘了将设置恢复正常。)

c. 生成新的外星人群

这个游戏的一个重要特点是外星人无穷无尽,一个外星人群被消灭后,又会出现一群外星人。

要在外星人群被消灭后又显示一群外星人,首先需要检查编组aliens是否为空。如果为空,就调用create_fleet()。我们将在update_bullets()中执行这种检查,因为外星人都是在这里被消灭的:

def update_bullets(ai_settings, screen, ship, aliens, bullets):
	--snip--
	
	# 检查是否有子弹集中了外星人
	# 如果是这样,就删除响应的子弹和外星人
	collisions = pygame.sprite.groupcollide(bullets, aliens, True, True)
	
	if len(aliens) == 0:
		# 删除现有的子弹并创建一群外星人
		bullets.empty()
		create_fleet(ai_settings, screen, ship, aliens)
  • 检查编组aliens是否为空。如果是,就使用方法empty()删除编组中余下的所有精灵,从而删除现有的所有子弹。
  • 调用了create_fleet(),再次在屏幕上显示一群外星人。

update_bullets()的定义包含额外的形参ai_settings、screen和shi,因此我们需要更新alien_invasion.py中对update_bullets()的调用:

while True:
    # 更新子弹的位置 , 删除已消失的子弹
    gf.update_bullets(ai_settings, screen, ship, aliens, bullets)

现在,当前外星人群消灭干净后,将立刻出现一个新的外星人群。

d. 重构update_bullets()函数

下面来重构update_bullets(),使其不再完成那么多任务。我们将把处理子弹和外星人碰撞的代码移到一个独立的函数中:

def update_bullets(ai_settings, screen, ship, aliens, bullets):
	"""更新子弹的位置,并删除已消失的子弹"""
	--snip--
			
	check_bullet_alien_collisions(ai_settings, screen, ship, aliens, bullets)
	
	
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)
  • 创建了一个新函数——check_bullet_alien_collisions(),以检测子弹和外星人之间的碰撞,以及在整群外星人都被消灭干净时采取相应的措施。这避免了update_bullets()太长,简化了后续的开发工作。
6、结束游戏

如果玩家根本不会输,游戏还有什么趣味和挑战性可言?如果玩家没能在足够短的时间内将整群外星人都消灭干净,且有外星人撞到了飞船,飞船将被摧毁。与此同时,我们还限制了可供玩家使用的飞船数,而有外星人抵达屏幕底端时,飞船也将被摧毁。玩家用光了飞船后,游戏便结束。

a. 检测外形人和飞船碰撞

我们首先检查外星人和飞船之间的碰撞,以便外星人撞上飞船时我们能够作出合适的响应。我们在更新每个外星人的位置后立即检测外星人和飞船之间的碰撞。

def update_aliens(ai_settings, ship, aliens):
	"""
	检查是否有外星人位于屏幕边缘,并更新整群外星人的位置
	"""
	check_fleet_edges(ai_settings, aliens)
	aliens.update()
	
	# 检测外星人和飞船之间的碰撞
	if pygame.sprite.spritecollideany(ship, aliens):
		print("Ship hit!!!")
  • spritecollideany()接受两个实参:一个精灵和一个编组。
    • 它检查编组是否有成员与精灵发生了碰撞,并在找到与精灵发生了碰撞的成员后就停止遍历编组。
    • 在这里,它遍历编组aliens,并返回它找到的第一个与飞船发生了碰撞的外星人。
    • 如果没有发生碰撞,spritecollideany()将返回None,if代码块不会执行。
    • 如果找到了与飞船发生碰撞的外星人,它就返回这个外星人,因此if代码块将执行:打印“Ship hit!!!”。
    • 有外星人撞到飞船时,需要执行的任务很多:需要删除余下的所有外星人和子弹,让飞船重新居中,以及创建一群新的外星人。编写完成这些任务的代码前,需要确定检测外星人和飞船碰撞的方法是否可行。而为确定这一点,最简单的方式是编写一条print语句。

现在,我们需要将ship传递给update_aliens():

# 开始游戏的主循环
while True:
    --snip--
    # 更新外星人群位置
    gf.update_aliens(ai_settings, ship, aliens)

运行这个游戏,则每当有外星人撞到飞船时,终端窗口都将显示“Ship hit!!!”。测试这项功能时,请将alien_drop_speed设置为较大的值,如50或100,这样外星人将更快地撞到飞船。

b. 响应外星人和飞船的碰撞

现在需要确定外星人与飞船发生碰撞时,该做些什么。我们不销毁ship实例并创建一个新的ship实例,而是通过跟踪游戏的统计信息来记录飞船被撞了多少次(跟踪统计信息还有助于记分)。

下面来编写一个用于跟踪游戏统计信息的新类——GameStats,并将其保存为文件game_stats.py:

class GameStats():
	"""跟踪游戏的统计信息"""
	
	def __init__(self, ai_settings):
		"""初始化统计信息"""
		self.ai_settings = ai_settings
		self.reset_stats()
		
	def reset_stats(self):
		"""初始化在游戏运行期间可能变化的统计信息"""
		self.ships_left = self.ai_settings.ship_limit
  • 在这个游戏运行期间,我们只创建一个GameStats实例,但每当玩家开始新游戏时,需要重置一些统计信息。
  • 为此,我们在方法reset_stats()中初始化大部分统计信息,而不是在__init__()中直接初始化它们。我们在__init__()中调用这个方法,这样创建GameStats实例时将妥善地设置这些统计信息,同时在玩家开始新游戏时也能调用reset_stats()。
  • 当前只有一项统计信息——ships_left,其值在游戏运行期间将不断变化。

一开始玩家拥有的飞船数存储在settings.py的ship_limit中:

class Settings():
	"""存储《外星人入侵》的所有设置的类"""
	
	def __init__(self):
		"""初始化游戏的设置"""
		--snip--
		# 飞船的设置
		self.ship_speed_factor = 1.5
		self.ship_limit = 3
        --snip--

我们还需对alien_invasion.py做些修改,以创建一个GameStats实例:

--snip--
from settings import Settings
from game_stats import GameStats
--snip--


def run_game():
	--snip--
	
	# 创建一个用于存储游戏统计信息的实例
	stats = GameStats(ai_settings)
	--snip--
	
	# 开始游戏的主循环
	while True:
		--snip--
		# 更新子弹的位置 , 删除已消失的子弹
		gf.update_bullets(ai_settings, screen, ship, aliens, bullets)
		# 更新外星人群位置
		gf.update_aliens(ai_settings, stats, screen, ship, aliens, bullets)
		--snip--

run_game()
  • 导入了新类GameStats,创建了一个名为stats的实例,再调用update_aliens()并添加了实参stats、screen和ship。
  • 在有外星人撞到飞船时,我们将使用这些实参来跟踪玩家还有多少艘飞船,以及创建一群新的外星人。
    • 有外星人撞到飞船时,我们将余下的飞船数减1,创建一群新的外星人,并将飞船重新放置到屏幕底端中央
    • 还将让游戏暂停一段时间,让玩家在新外星人群出现前注意到发生了碰撞,并将重新创建外星人群。

下面将实现这些功能的大部分代码放到pygame_function.py的函数ship_hit()中:

--snip--
from time import sleep
--snip--


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

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

	# 创建一群新的外星人,并将飞船放到屏幕底端中央
	create_fleet(ai_settings, screen, ship, aliens)
	ship.center_ship()

	# 暂停
	sleep(0.5)		
			
							
def update_aliens(ai_settings, stats, screen, ship, aliens, bullets):
	"""
	检查是否有外星人位于屏幕边缘,并更新整群外星人的位置
	"""
	check_fleet_edges(ai_settings, aliens)
	aliens.update()
	
	# 检测外星人和飞船之间的碰撞
	if pygame.sprite.spritecollideany(ship, aliens):
		ship_hit(ai_settings, stats, screen, ship, aliens, bullets)
  • 首先从模块time中导入了函数sleep(),以便使用它来让游戏暂停。
  • 新函数ship_hit()在飞船被外星人撞到时作出响应。在这个函数内部:
    • 将余下的飞船数减1,然后清空编组aliens和bullets。
    • 接下来,我们创建一群新的外星人,并将飞船居中,稍后将在Ship类中添加方法center_ship()。
    • 最后,我们更新所有元素后(但在将修改显示到屏幕前)暂停,让玩家知道其飞船被撞到了。屏幕将暂时停止变化,让玩家能够看到外星人撞到了飞船。
  • 函数sleep()执行完毕后,将接着执行函数update_screen(),将新的外星人群绘制到屏幕上。
  • 我们还更新了update_aliens()的定义,使其包含形参stats、screen和bullets,让它能够在调用ship_hit()时传递这些值。

下面是新方法center_ship(),请将其添加到ship.py的末尾:

def center_ship(self):
    """让飞船在屏幕上居中"""
    self.center = self.screen_rect.centerx
  • 为让飞船居中,我们将飞船的属性center设置为屏幕中心的x坐标,而该坐标是通过属性screen_rect获得的。
  • 注意:我们根本没有创建多艘飞船,在整个游戏运行期间,我们都只创建了一个飞船实例,并在该飞船被撞到时将其居中。统计信息ships_left让我们知道飞船是否用完。

请运行这个游戏,射杀几个外星人,并让一个外星人撞到飞船。游戏暂停后,将出现一群新的外星人,而飞船将在屏幕底端居中。

c. 有外星人到达屏幕底端

如果有外星人到达屏幕底端,我们将像有外星人撞到飞船那样作出响应。请在game_function.py中添加一个执行这项任务的新函数,并将其命名为check_aliens_bottom():


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
						
							
def update_aliens(ai_settings, stats, screen, ship, aliens, bullets):
	"""
	检查是否有外星人位于屏幕边缘,并更新整群外星人的位置
	"""
	check_fleet_edges(ai_settings, aliens)
	aliens.update()
	
	# 检测外星人和飞船之间的碰撞
	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)
		
  • 函数check_aliens_bottom()检查是否有外星人到达了屏幕底端。到达屏幕底端后,外星人的属性rect.bottom的值大于或等于屏幕的属性rect.bottom的值。
    • 如果有外星人到达屏幕底端,我们就调用ship_hit();只要检测到一个外星人到达屏幕底端,就无需检查其他外星人,因此我们在调用ship_hit()后退出循环。
    • 在更新所有外星人的位置并检测是否有外星人和飞船发生碰撞后调用check_aliens_bottom()。现在,每当有外星人撞到飞船或抵达屏幕底端时,都将出现一群新的外星人。
d. 结束游戏

现在这个游戏看起来更完整了,但它永远都不会结束,只是ships_left不断变成更小的负数。下面在GameStats中添加一个作为标志的属性game_active,以便在玩家的飞船用完后结束游戏:

class GameStats():
	"""跟踪游戏的统计信息"""
	
	def __init__(self, ai_settings):
		--snip--
		# 游戏刚启动时处于活动状态
		self.game_active = True

现在在game_function.py中的ship_hit()中添加代码,在玩家的飞船都用完后将game_active设置为False:

def ship_hit(ai_settings, stats, screen, ship, aliens, bullets):
	"""响应被外星人撞到的飞船"""
	if stats.ships_left > 0:
		# 将ships_left减1
		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_hit()的大部分代码都没变。我们将原来的所有代码都移到了一个if语句块中,这条if语句检查玩家是否至少还有一艘飞船。如果是这样,就创建一群新的外星人,暂停一会儿,再接着往下执行。如果玩家没有飞船了,就将game_active设置为False。
7、确定应运行游戏的哪些部分

在alien_invasion.py中,我们需要确定游戏的哪些部分在任何情况下都应运行,哪些部分仅在游戏处于活动状态时才运行:

# 开始游戏的主循环
while True:
    # 调用事件监听函数
    gf.check_events(ai_settings, screen, ship, bullets)
    
    if stats.game_active:
        # 更新飞船位置
        ship.update()
        
        # 更新子弹的位置 , 删除已消失的子弹
        gf.update_bullets(ai_settings, screen, ship, aliens, bullets)
        # 更新外星人群位置
        gf.update_aliens(ai_settings, stats, screen, ship, 
                                                aliens, bullets)
        
    # 调用绘制函数
    gf.update_screen(ai_settings, screen, ship, aliens, bullets)
  • 在主循环中,在任何情况下都需要调用check_events(),即便游戏处于非活动状态时亦如此。
    • 例如,我们需要知道玩家是否按了Q键以退出游戏,或单击关闭窗口的按钮。
    • 我们还需要不断更新屏幕,以便在等待玩家是否选择开始新游戏时能够修改屏幕。
    • 其他的函数仅在游戏处于活动状态时才需要调用,因为游戏处于非活动状态时,我们不用更新游戏元素的位置。

现在,你运行这个游戏时,它将在飞船用完后停止不动。

在与这个项目相关的最后一章中,我们将添加一个Play按钮,让玩家能够开始游戏,以及游戏结束后再玩。每当玩家消灭一群外星人后,我们都将加快游戏的节奏,并添加一个记分系统,得到一个极具可玩性的游戏!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值