外星人入侵-Python
外星人入侵-Python
武装飞船
开发一个名为《外星人入侵》的游戏吧!为此将使用 Pygame,这是一组功能强大而有趣的模块,可用于管理图形、动画乃至声音, 让你能够更轻松地开发复杂的游戏。通过使用Pygame来处理在屏幕上绘制图像 等任务,可将重点放在程序的高级逻辑上。
你将安装Pygame,再创建一艘能够根据用户输入左右移动和射击的飞船。在接下来的两章,你将创建一群作为射杀目标的外星人,并改进该游戏:限制可供玩家使用的飞船数,并且添加记分牌。
玩家控制一艘最初出现在屏幕底部中央的飞船。玩 家可以使用箭头键左右移动飞船,还可使用空格键射击。游戏开始时,一群外 星人出现在天空中,并向屏幕下方移动。玩家的任务是射杀这些外星人。玩家 将所有外星人都消灭干净后,将出现一群新的外星人,其移动速度更快。只要 有外星人撞到玩家的飞船或到达屏幕底部,玩家就损失一艘飞船。玩家损失三 艘飞船后,游戏结束。
一.第一阶段
创建一艘飞船,它可左右移动,并且能在用户按空格键时开火。设置好这种行为后,就可以创建外星人并提高游戏的可玩性了。
安装Pygame
在终端提示符下执行如下命令
$ python -m pip install --user pygame
1.1开始游戏项目
1.1.1创建Pygame窗口及响应用户输入
创建一个表示游戏的类,以创建空的Pygame窗口。
alien_invasion.py
//首先,导入模块sys 和pygame.模块pygame 包含开发游戏所需的功能。玩家退
出时,我们将使用模块sys 中的工具来退出游戏.
import sys
import pygame
class AlienInvasion:
"""管理游戏资源和行为的类"""
def __init__(self):
"""初始化游戏并创建游戏资源"""
pygame.init() #1
self.screen = pygame.display.set_mode((1200, 800)) #2
pygame.display.set_caption("Alien Invasion")
def run_game(self):
"""开始游戏的主循环"""
while True: #3
for event in pygame.event.get(): #4
if event.type == pygame.QUIT: #5
sys.exit()
#让最近绘制的屏幕可见。
pygame.display.flip() #6
if __name__ == '__main__':
ai = AlienInvasion()
ai.run_game()
为开发游戏《外星人入侵》,我们创建了一个表示它的类,名为AlienInvasion 。在这个类的方法__init__() 中,调用函数pygame.init() 来初始化背景设置,让Pygame能够正确地工作. (#1)
(#2) 调用 pygame.display.set_mode() 来创建一个显示窗口,游戏的所有图形元素都将在其中绘制。实参(1200, 800) 是一个元组,指定了游戏窗口的尺寸——宽1200 像素、高800像素(你可以根据自己的显示器尺寸调整这些值)。将这个显示窗口赋给属性self.screen ,让这个类中的所有方法都能够使用它。
赋给属性self.screen 的对象是一个
surface
。在Pygame中,surface是屏幕的一 部分,用于显示游戏元素。在这个游戏中,每个元素(如外星人或飞船)都是一个 surface。display.set_mode() 返回的surface表示整个游戏窗口。激活游戏的 动画循环后,每经过一次循环都将自动重绘这个surface,将用户输入触发的所有变化都反映出来。
这个游戏由方法run_game() 控制。该方法包含一个不断运行的while 循环 (#3) ,而这个循环包含一个事件循环以及管理屏幕更新的代码。 事件 是用户玩游戏 时执行的操作,如按键或移动鼠标。为程序响应事件,可编写一个 事件循环 ,以 侦听事件并根据发生的事件类型执行合适的任务。(#4)for 循环就是一个事件循环。
为访问Pygame检测到的事件,我们使用了函数pygame.event.get() 。这个函数 返回一个列表,其中包含它在上一次被调用后发生的所有事件。所有键盘和鼠标事 件都将导致这个for 循环运行。在这个循环中,我们将编写一系列if 语句来检测 并响应特定的事件。例如,当玩家单击游戏窗口的关闭按钮时,将检测到 pygame.QUIT 事件,进而调用sys.exit() 来退出游戏. (#5)
(#6) 调用了pygame.display.flip() ,命令Pygame让最近绘制的屏幕可见。在 这里,它在每次执行while 循环时都绘制一个空屏幕,并擦去旧屏幕,使得只有新 屏幕可见。我们移动游戏元素时,pygame.display.flip() 将不断更新屏幕, 以显示元素的新位置,并且在原来的位置隐藏元素,从而营造平滑移动的效果。
在这个文件末尾,创建一个游戏实例并调用run_game() 。这些代码放在一个if 代码块中,仅当直接运行该文件时,它们才会执行。如果此时运行 alien_invasion.py,将看到一个空的Pygame窗口。
1.1.2设置背景色
调用方法fill() 用这种背景色填充屏幕。方法fill() 用于处理 surface,只接受一个实参:一种颜色。
def __init__(self):
--snip--
pygame.display.set_caption("Alien Invasion")
#设置背景色。
self.bg_color = (230, 230, 230)
def run_game(self):
--snip--
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
# 每次循环时都重绘屏幕。
self.screen.fill(self.bg_color)
# 让最近绘制的屏幕可见。
pygame.display.flip()
1.1.3创建设置类
每次给游戏添加新功能时,通常也将引入一些新设置。下面来编写一个名为 settings 的模块,在其中包含一个名为Settings 的类,用于将所有设置都存储 在一个地方,以免在代码中到处添加设置。这样,每当需要访问设置时,只需使用 一个设置对象。另外,在项目增大时,这使得修改游戏的外观和行为更容易:要修 改游戏,只需修改(接下来将创建的)settings.py中的一些值,而无须查找散布在 项目中的各种设置。
settings.py
class Settings:
"""存储游戏《外星人入侵》中所有设置的类"""
def __init__(self):
"""初始化游戏的设置。"""
# 屏幕设置
self.screen_width = 1200
self.screen_height = 800
self.bg_color = (230, 230, 230)
创建Settings 实例并用它来访问设置,需要将alien_invasion.py修改
alien_invasion.py
import sys
import pygame
from settings import Settings
class AlienInvasion:
"""管理游戏资源和行为的类"""
def __init__(self):
"""初始化游戏并创建游戏资源"""
pygame.init()
self.settings = Settings()#1
self.screen = pygame.display.set_mode(
(self.settings.screen_width, self.settings.screen_height))#2
pygame.display.set_caption("Alien Invasion")
# 设置背景色。
self.bg_color = (230, 230, 230)
def run_game(self):
"""开始游戏的主循环"""
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
# 每次循环时都重绘屏幕。
self.screen.fill(self.settings.bg_color)#3
# 让最近绘制的屏幕可见。
pygame.display.flip()
if __name__ == '__main__':
ai = AlienInvasion()
ai.run_game()
在主程序文件中,导入Settings 类,调用pygame.init() ,再创建一个 Settings 实例并将其赋给self.settings (#1) 。创建屏幕时 (#2) ,使 用了self.settings 的属性screen_width 和screen_height 。接下来填充 屏幕时,也使用了self.settings 来访问背景色 (#3)
1.2添加飞船图像
为了在屏幕上绘制玩家的飞船,我们将加载一幅图像,再 使用Pygame方法blit() 绘制它。
1.2.1创建 Ship 类
我们创建一个名为ship 的 模块,其中包含Ship 类,负责管理飞船的大部分行为。
ship.py
import pygame
from settings import Settings
class Ship:
"""管理飞船的类"""
def __init__(self,ai_game):
"""初始化飞船并设置其初始位置"""
self.screen = ai_game.screen #1
self.screen_rect = ai_game.screen.get_rect() #2
#加载飞船图像并获取其外接矩形
self.image = pygame.image.load('images/ship.bmp') #3
self.rect = self.image.get_rect()
#将每艘新飞船放在屏幕底部中央
self.rect.midbottom = self.screen_rect.midbottom #4
def blitme(self): #5
"""在指定位置绘制飞船。"""
self.screen.blit(self.image, self.rect)
Pygame之所以高效,是因为它让你能够像处理 矩形(rect 对象) 一样处理所有的 游戏元素,即便其形状并非矩形。像处理矩形一样处理游戏元素之所以高效,是因 为矩形是简单的几何形状。例如,通过将游戏元素视为矩形,Pygame能够更快地判 断出它们是否发生了碰撞。这种做法的效果通常很好,游戏玩家几乎注意不到我们 处理的并不是游戏元素的实际形状。在这个类中,我们将把飞船和屏幕作为矩形进行处理。
定义这个类之前,导入了模块pygame 。Ship 的方法__init__() 接受两个参 数:引用self 和指向当前AlienInvasion 实例的引用。这让Ship 能够访问 AlienInvasion 中定义的所有游戏资源。 (#1) ,将屏幕赋给了Ship 的一个属性,以便在这个类的所有方法中轻松访问。 (#2) ,使用方法get_rect() 访问屏 幕的属性rect ,并将其赋给了self.screen_rect ,这让我们能够将飞船放到屏幕的正确位置。
调用pygame.image.load() 加载图像,并将飞船图像的位置传递给它 (#3) 。 该函数返回一个表示飞船的surface,而我们将这个surface赋给了self.image 。 加载图像后,使用get_rect() 获取相应surface的属性rect ,以便后面能够使 用它来指定飞船的位置。
处理rect 对象时,可使用矩形四角和中心的 坐标和 坐标。可通过设置这些值来 指定矩形的位置。要让游戏元素居中,可设置相应rect 对象的属性center 、 centerx 或centery ;要让游戏元素与屏幕边缘对齐,可使用属性top 、 bottom 、left 或right 。除此之外,还有一些组合属性,如midbottom 、 midtop 、midleft 和midright 。要调整游戏元素的水平或垂直位置,可使用 属性x 和y ,分别是相应矩形左上角的 坐标和 坐标。这些属性让你无须做游戏 开发人员原本需要手工完成的计算,因此会经常用到。
- 注意 在Pygame中,原点(0, 0)位于屏幕左上角,向右下方移动时,坐标值将 增大。在1200 × 800的屏幕上,原点位于左上角,而右下角的坐标为(1200, 800)。这些坐标对应的是游戏窗口,而不是物理屏幕。我们要将飞船放在屏幕底部的中央。为此,将self.rect.midbottom 设置为表 示屏幕的矩形的属性midbottom (#4)。Pygame使用这些rect 属性来放置飞船 图像,使其与屏幕下边缘对齐并水平居中。
在(#5)处,定义了方法blitme() ,它将图像绘制到self.rect 指定的位置。
1.2.2在屏幕上绘制飞船
下面更新alien_invasion.py,创建一艘飞船并调用其方法blitme() :
alien_invasion.py
import sys
import pygame
from settings import Settings
from ship import Ship
class AlienInvasion:
"""管理游戏资源和行为的类"""
def __init__(self):
"""初始化游戏并创建游戏资源"""
pygame.init()
self.settings = Settings()#1
self.screen = pygame.display.set_mode(
(self.settings.screen_width, self.settings.screen_height))
pygame.display.set_caption("Alien Invasion")
self.ship= Ship(self) #1
def run_game(self):
"""开始游戏的主循环"""
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
# 每次循环时都重绘屏幕。
self.screen.fill(self.settings.bg_color)
self.ship.blitme() #2
# 让最近绘制的屏幕可见。
pygame.display.flip()
if __name__ == '__main__':
ai = AlienInvasion()
ai.run_game()
导入Ship 类,并在创建屏幕后创建一个Ship 实例 (#1) 。调用Ship() 时,必 须提供一个参数:一个AlienInvasion 实例。在这里,self 指向的是当前 AlienInvasion 实例。这个参数让Ship 能够访问游戏资源,如对象screen 。 我们将这个Ship 实例赋给了self.ship 。 填充背景后,调用ship.blitme() 将飞船绘制到屏幕上,确保它出现在背景前面 (#2) 。
1.3重构:方法 _check_events() 和 __update_screen()
在大型项目中,经常需要在添加新代码前重构既有代码。重构旨在简化既有代码的 结构,使其更容易扩展。本节将把越来越长的方法run_game() 拆分成两个辅助方 法(helper method)。 辅助方法 在类中执行任务,但并非是通过实例调用的。在 Python中, 辅助方法的名称以单个下划线打头。
1.3.1 方法 _check_events()
我们将把管理事件的代码移到一个名为_check_events() 的方法中,以简化 run_game() 并隔离事件管理循环。通过隔离事件循环,可将事件管理与游戏的其 他方面(如更新屏幕)分离。
alien_invasion.py
def run_game(self):
"""开始游戏的主循环"""
while True:
self._check_events() #1
# 每次循环时都重绘屏幕。
self.screen.fill(self.settings.bg_color)
self.ship.blitme()
# 让最近绘制的屏幕可见。
pygame.display.flip()
def _check_events(self): #2
"""响应按键和鼠标事件"""
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
新增方法_check_events() (#2),并将检查玩家是否单击了关闭窗口按钮的 代码移到该方法中。 要调用当前类的方法,可使用句点表示法,并指定变量名self 和要调用的方法的 名称(#1)。我们在run_game() 的while 循环中调用这个新增的方法。
1.3.2 方法 _update_screen()
步简化run_game() ,将更新屏幕的代码移到一个名为 _update_screen() 的方法中:
alien_invasion.py
def run_game(self):
"""开始游戏的主循环"""
while True:
self._check_events()
self._update_screen()
def _check_events(self):
--snip--
def _update_screen(self):
"""更新屏幕上的图像,并切换到新屏幕"""
self.screen.fill(self.settings.bg_color)
self.ship.blitme()
# 让最近绘制的屏幕可见。
pygame.display.flip()
我们将绘制背景和飞船以及切换屏幕的代码移到了方法_update_screen() 中。 现在,run_game() 中的主循环简单多了,很容易看出在每次循环中都检测了新发 生的事件并更新了屏幕。
1.4驾驶飞船
让玩家能够左右移动飞船。我们将编写代码,在用户按左或右箭头键时做出 响应。我们将首先专注于向右移动,再使用同样的原理来控制向左移动。通过这样 做,你将学会如何控制屏幕图像的移动。
1.4.1响应按键
每当用户按键时,都将在Pygame中注册一个事件。事件都是通过方法 pygame.event.get() 获取的,因此需要在方法_check_events() 中指定要检 查哪些类型的事件。每次按键都被注册为一个KEYDOWN 事件。
Pygame检测到KEYDOWN 事件时,需要检查按下的是否是触发行动的键。例如,如果 玩家按下的是右箭头键,就增大飞船的rect.centerx 值,将飞船向右移动:
alien_invasion.py
def _check_events(self):
"""响应按键和鼠标事件"""
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
elif event.type == pygame.KEYDOWN: #1
if event.key == pygame.K_d: #2
self.ship.rect.x+= 1 #3
在方法_check_events() 中,为事件循环添加一个elif 代码块,以便在Pygame 检测到KEYDOWN 事件时做出响应 (#1) 。我们检查按下键(event.key )是否 是右箭头键(pygame.K_d ) (#2) 。如果是,就将 self.ship.rect.centerx 的值加1,从而将飞船向右移动 (#3)
如果现在运行alien_invasion.py,则每按右箭头键一次,飞船都将向右移动1像 素。这是一个开端,但并非控制飞船的高效方式。下面来改进控制方式,允许持续 移动。
1.4.2允许持续移动
玩家按住右箭头键不放时,我们希望飞船不断向右移动,直到玩家松开为止。我们 将让游戏检测pygame.KEYUP 事件,以便知道玩家何时松开右箭头键。然后,结合 使用 KEYDOWN 和KEYUP 事件 以及一个名为 moving_right 的标志 来实现持续移动。
当标志moving_right 为False 时,飞船不会移动。玩家按下右箭头键时,我们 将该标志设置为True ,在玩家松开时将该标志重新设置为False 。
飞船的属性都由Ship 类控制,因此要给这个类添加一个名为moving_right 的属 性和一个名为update() 的方法。方法update() 检查标志moving_right 的状 态。如果该标志为True ,就调整飞船的位置。我们将在while 循环中调用这个方 法,以调整飞船的位置。
ship.py
def __init__(self,ai_game):
"""初始化飞船并设置其初始位置"""
self.screen = ai_game.screen
self.screen_rect = ai_game.screen.get_rect()
#加载飞船图像并获取其外接矩形
self.image = pygame.image.load('images/ship.bmp')
self.rect = self.image.get_rect()
#将每艘新飞船放在屏幕底部中央
self.rect.midbottom = self.screen_rect.midbottom #1
#移动标志
self.moving_right = False
def update(self): #2
"""根据移动标志调整飞船的位置"""
if self.moving_right:
self.rect.x += 1
在方法__init__() 中,添加属性self.moving_right ,并将其初始值设置为 False (#1) 。接下来,添加方法update() ,在前述标志为True 时向右移动 飞船 (#2) 。方法update() 将通过Ship 实例来调用,因此不是辅助方法。 接下来,需要修改_check_events() ,使其在玩家按下右箭头键时将 moving_right 设置为True ,并在玩家松开时将moving_right 设置为False:
alien_invasion.py
def _check_events(self):
"""响应按键和鼠标事件"""
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_RIGHT:
self.ship.moving_right = True #1
elif event.type == pygame.KEYUP: #2
if event.key == pygame.K_RIGHT:
self.ship.moving_right = False
在 (#1) 处,修改游戏在玩家按下右箭头键时响应的方式:不直接调整飞船的位置,而只 是将moving_right 设置为True 。在 (#2) 处,添加一个新的elif 代码块,用于响 应KEYUP 事件:玩家松开右箭头键(K_RIGHT )时,将moving_right 设置为 False 。
最后,需要修改run_game() 中的while 循环,以便每次执行循环时都调用飞船 的方法update() :
alien_invasion.py
def run_game(self):
"""开始游戏主循环。"""
while True:
self._check_events()
self.ship.update()
self._update_screen()
飞船的位置将在检测到键盘事件后(但在更新屏幕前)更新。这样,玩家输入时, 飞船的位置将更新,从而确保使用更新后的位置将飞船绘制到屏幕上。
1.4.3左右移动
现在飞船能够持续向右移动了,添加向左移动的逻辑也很容易。我们将再次修改 Ship 类和方法_check_events()
ship.py
def __init__(self, ai_game):
--snip--
#
移动标志
self.moving_right = False
self.moving_left = False
def update(self):
"""根据移动标志调整飞船的位置。"""
if self.moving_right:
self.rect.x += 1
if self.moving_left:
self.rect.x -= 1
在方法__init__() 中,添加标志self.moving_left 。在方法update() 中,添加一个if 代码块而不是elif 代码块,这样如果玩家同时按下了左右箭头 键,将先增加再减少飞船的rect.x 值,即飞船的位置保持不变。如果使用一个 elif 代码块来处理向左移动的情况,右箭头键将始终处于优先地位。从向左移动 切换到向右移动时,玩家可能同时按住左右箭头键,此时前面的做法让移动更准确。
alien_invasion.py
def _check_events(self):
"""响应按键和鼠标事件。"""
for event in pygame.event.get():
--snip--
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_RIGHT:
self.ship.moving_right = True
elif event.key == pygame.K_LEFT:
self.ship.moving_left = True
elif event.type == pygame.KEYUP:
if event.key == pygame.K_RIGHT:
self.ship.moving_right = False
elif event.key == pygame.K_LEFT:
self.ship.moving_left = False
如果因玩家按下K_LEFT 键而触发了KEYDOWN 事件,就将moving_left 设置为 True 。如果因玩家松开K_LEFT 而触发了KEYUP 事件,就将moving_left 设置 为False 。这里之所以可以使用elif 代码块,是因为每个事件都只与一个键相关 联。如果玩家同时按下左右箭头键,将检测到两个不同的事件。
1.4.4调整飞船的速度
当前,每次执行while 循环时,飞船最多移动1像素,但可在Settings 类中添加 属性ship_speed ,用于控制飞船的速度。我们将根据这个属性决定飞船在每次循 环时最多移动多远。
class Settings:
"""存储游戏《外星人入侵》中所有设置的类。"""
def __init__(self):
--snip--
# 飞船设置
self.ship_speed = 1.5
通过将速度设置指定为小数值,可在后面加快游戏节奏时更细致地控制飞船的速 度。然而,rect 的x 等属性只能存储整数值,因此需要对Ship 类做些修改:
ship.py
class Ship:
"""管理飞船的类"""
def __init__(self,ai_game): #1
"""初始化飞船并设置其初始位置"""
self.screen = ai_game.screen
self.settings = ai_game.settings
self.screen_rect = ai_game.screen.get_rect()
#加载飞船图像并获取其外接矩形
self.image = pygame.image.load('images/ship.bmp')
self.rect = self.image.get_rect()
#将每艘新飞船放在屏幕底部中央
self.rect.midbottom = self.screen_rect.midbottom
#在飞船的属性中存储小数值
self.x = float(self.rect.x) #2
#移动标志
self.moving_right = False
self.moving_left = False
def update(self):
"""根据移动标志调整飞船的位置"""
#更新飞船而不是rect对象的x值。
if self.moving_right:
self.x += self.settings.ship_speed #3
if self.moving_left:
self.x -= self.settings.ship_speed
#根据self.x更新rect对象
self.rect.x = self.x #4
在 (#1) 处,给Ship 类添加属性settings ,以便能够在update() 中使用它。鉴于 现在调整飞船的位置时,将增减一个单位为像素的小数值,因此需要将位置赋给一 个能够存储小数值的变量。可使用小数来设置rect 的属性,但rect 将只存储这个 值的整数部分。为准确存储飞船的位置,定义一个可存储小数值的新属性self.x (#2) 。使用函数float() 将self.rect.x 的值转换为小数,并将结果赋给 self.x 。 现在在update() 中调整飞船的位置时,将self.x 的值增减 settings.ship_speed 的值 (#3) 。更新self.x 后,再根据它来更新控制飞 船位置的self.rect.x (#4) 。self.rect.x 只存储self.x 的整数部分, 但对显示飞船而言,这问题不大。 现在可以修改ship_speed 的值了。只要它的值大于1,飞船的移动速度就会比以 前更快。这有助于让飞船的反应速度足够快,以便射杀外星人,还让我们能够随着 游戏的进行加快游戏的节奏。
1.4.5限制飞船的活动范围
当前,如果玩家按住箭头键的时间足够长,飞船将飞到屏幕之外,消失得无影无踪。
ship.py
def update(self):
"""根据移动标志调整飞船的位置"""
#更新飞船而不是rect对象的x值。
if self.moving_right and self.rect.right < self.screen_rect.right: #1
self.x += self.settings.ship_speed
if self.moving_left and self.rect.left > 0: #2
self.x -= self.settings.ship_speed
#根据self.x更新rect对象
self.rect.x = self.x
在修改self.x 的值之前检查飞船的位置。self.rect.right 返回飞船 外接矩形右边缘的x坐标。如果这个值小于self.screen_rect.right 的值,就 说明飞船未触及屏幕右边缘 (#1) 。左边缘的情况与此类似:如果rect 左边缘的 x坐标大于零,就说明飞船未触及屏幕左边缘 (#2) 。这确保仅当飞船在屏幕内时,才调整self.x 的值。
1.3.6 重构 _check_events()
随着游戏的开发,方法_check_events() 将越来越长。因此将其部分代码放在两 个方法中,其中一个处理KEYDOWN 事件,另一个处理KEYUP 事件:
alien_invasion.py
def _check_events(self):
"""响应鼠标和按键事件。"""
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
elif event.type == pygame.KEYDOWN:
self._check_keydown_events(event)
elif event.type == pygame.KEYUP:
self._check_keyup_events(event)
def _check_keydown_events(self, event):
"""响应按键。"""
if event.key == pygame.K_RIGHT:
self.ship.moving_right = True
elif event.key == pygame.K_LEFT:
self.ship.moving_left = True
def _check_keyup_events(self, event):
"""响应松开。"""
if event.key == pygame.K_RIGHT:
self.ship.moving_right = False
elif event.key == pygame.K_LEFT:
self.ship.moving_left = False
我们创建了两个新的辅助方法:_check_keydown_events() 和 _check_keyup_events() 。它们都包含形参self 和event 。这两个方法的代 码是从_check_events() 中复制而来的,因此将方法_check_events() 中相 应的代码替换成了对这两个新方法的调用。现在,方法_check_events() 更简 单,代码结构也更清晰,在其中响应玩家输入时将更容易.
1.5简单回顾
1.5.1 alien_invasion.py
主文件alien_invasion.py包含AlienInvasion 类。这个类创建一系列贯穿整个游 戏都要用到的属性:赋给self.settings 的设置,赋给screen 中的主显示 surface,以及一个飞船实例。这个模块还包含游戏的主循环,即一个调用 _check_events() 、ship.update() 和_update_screen() 的while 循 环。
方法_check_events() 检测相关的事件(如按下和松开键盘),并通过调用方法 _check_keydown_events() 和_check_keyup_events() 处理这些事件。当 前,这些方法负责管理飞船的移动。AlienInvasion 类还包含方法 _update_screen() ,该方法在每次主循环中重绘屏幕。
要玩游戏《外星人入侵》,只需运行文件alien_invasion.py,其他文件 (settings.py和ship.py)包含的代码会被导入这个文件中。
1.5.2 settings.py
文件settings.py包含Settings 类,这个类只包含方法__init__() ,用于初始 化控制游戏外观和飞船速度的属性。
1.5.3 ship.py
文件ship.py包含Ship 类,这个类包含方法__init__() 、管理飞船位置的方法 update() 和在屏幕上绘制飞船的方法blitme() 。表示飞船的图像存储在文件夹 images下的文件ship.bmp中。
1.6射击
我们将编写在玩家按空格键时发射子弹(用小矩形表示)的 代码。子弹将在屏幕中向上飞行,抵达屏幕上边缘后消失。
1.6.1 添加子弹设置
首先,更新settings.py,在方法__init__() 末尾存储新类Bullet 所需的值:
settings.py
def __init__(self):
--snip--
# 子弹设置
self.bullet_speed = 1.0
self.bullet_width = 3
self.bullet_height = 15
self.bullet_color = (60, 60, 60)
1.6.2创建 Bullet 类
bullet.py
import pygame
from pygame.sprite import Sprite
class Bullet(Sprite):
"""一个对飞船发射的子弹进行管理的类"""
def __init__(self,ai_game):
"""在飞船所处的位置创建一个子弹对象"""
super().__init__()
self.screen=ai_game.screen
self.settings=ai_game.settings
self.color=self.settings.bullet_color
#在(0,0)处创建一个表示子弹的矩形,再设置正确的位置
self.rect=pygame.Rect(0,0,self.settings.bullet_width, #1
self.settings.bullet_height)
self.rect.midtop=ai_game.ship.rect.midtop #2
#存储用小数表示的子弹位置
self.y=float(self.rect.y) #3
Bullet 类继承了从模块pygame.sprite 导入的Sprite 类。通过 使用精灵 (sprite) ,可将游戏中相关的元素编组,进而同时操作编组中的所有元素。为创 建子弹实例,init() 需要当前的AlienInvasion 实例,我们还调用了 super() 来继承Sprite 。另外,我们还定义了用于存储屏幕以及设置对象和子弹 颜色的属性。
在 (#1) 处,创建子弹的属性rect 。子弹并非基于图像,因此必须使用 pygame.Rect() 类从头开始创建一个矩形。创建这个类的实例时,必须提供矩形 左上角的 坐标和 坐标,以及矩形的宽度和高度。我们在(0, 0)处创建这个矩 形,但下一行代码将其移到了正确的位置,因为子弹的初始位置取决于飞船当前的 位置。子弹的宽度和高度是从self.settings 中获取的。
在 (#2) 处,将子弹的rect.midtop 设置为飞船的rect.midtop 。这样子弹将从飞 船顶部出发,看起来像是从飞船中射出的。我们将子弹的 坐标存储为小数值,以 便能够微调子弹的速度 (#3) 。
bullet.py
def update(self):
"""向上移动子弹"""
self.y-=self.settings.bullet_speed #1
#更新表示子弹的rect的位置
self.rect.y=self.y #2
def draw_bullet(self):
"""在屏幕上绘制子弹"""
pygame.draw.rect(self.screen,self.color,self.rect) #3
方法update() 管理子弹的位置。发射出去后,子弹向上移动,意味着其 坐标将 不断减小。为更新子弹的位置,从self.y 中减去settings .bullet_speed 的值 (#1) 。接下来,将self.rect.y 设置为self.y 的值 (#2) 。
属性bullet_speed 让我们能够随着游戏的进行或根据需要提高子弹的速度,以调 整游戏的行为。子弹发射后,其 坐标始终不变,因此子弹将沿直线垂直向上飞 行。 需要绘制子弹时,我们调用draw_bullet() 。draw.rect() 函数使用存储在 self.color 中的颜色填充表示子弹的rect 占据的屏幕部分 (#3) 。
1.6.3将子弹存储到编组中
定义Bullet 类和必要的设置后,便可编写代码在玩家每次按空格键时都射出一发 子弹了。我们将在AlienInvasion 中创建一个编组(group),用于 存储所有有效 的子弹 ,以便管理发射出去的所有子弹。这个编组是pygame.sprite.Group 类 的一个实例。pygame.sprite.Group 类似于列表,但提供了有助于开发游戏的 额外功能。在主循环中,将使用 这个编组在屏幕上绘制子弹以及更新每颗子弹的位置。
首先,在__init__() 中创建用于存储子弹的编组:
alien_invasion.py
def __init__(self):
--snip--
self.ship = Ship(self)
self.bullets = pygame.sprite.Group()
然后在while 循环中更新子弹的位置:
alien_invasion.py
def run_game(self):
"""开始游戏主循环。"""
while True:
self._check_events()
self.ship.update()
self.bullets.update() #1
self._update_screen()
对编组调用update() 时 (#1) ,编组自动对其中的每个精灵调用update() 。 因此代码行bullets.update() 将为编组bullets 中的每颗子弹调用 bullet.update() 。
1.6.4开火
在AlienInvasion 中,需要修改_check_keydown_events() ,以便在玩家按 空格键时发射一颗子弹。无须修改_check_keyup_events() ,因为玩家松开空 格键时什么都不会发生。还需要修改_update_screen() ,确保在调用flip() 前在屏幕上重绘每颗子弹。
为发射子弹,需要做的工作不少,因此编写一个新方法_fire_bullet() 来完成 这项任务:
alien_invasion.py
--snip--
from ship import Ship
from bullet import Bullet #1
class AlienInvasion:
--snip--
def _check_keydown_events(self, event):
--snip--
elif event.key == pygame.K_q:
sys.exit()
elif event.key == pygame.K_SPACE: #2
self._fire_bullet()
def _check_keyup_events(self, event):
--snip--
def _fire_bullet(self):
"""创建一颗子弹,并将其加入编组bullets中。"""
new_bullet = Bullet(self) #3
self.bullets.add(new_bullet) #4
def _update_screen(self):
"""更新屏幕上的图像,并切换到新屏幕。"""
self.screen.fill(self.settings.bg_color)
self.ship.blitme()
for bullet in self.bullets.sprites(): #5
bullet.draw_bullet()
pygame.display.flip()
--snip--
1.6.5删除消失的子弹
当前,子弹在抵达屏幕顶端后消失,但这仅仅是因为Pygame无法在屏幕外面绘制它 们。这些子弹实际上依然存在,其 坐标为负数且越来越小。这是个问题,因为它 们将继续消耗内存和处理能力。
需要将这些消失的子弹删除,否则游戏所做的无谓工作将越来越多,进而变得越来 越慢。为此,需要检测表示子弹的rect 的bottom 属性是否为零。如果是,则表 明子弹已飞过屏幕顶端:
def run_game(self):
"""开始游戏主循环。"""
while True:
self._check_events()
self.ship.update()
self.bullets.update()
#删除消失的子弹。
for bullet in self.bullets.copy(): #1
if bullet.rect.bottom <= 0: #2
self.bullets.remove(bullet) #3
print(len(self.bullets)) #4
self._update_screen()
使用for 循环遍历列表(或Pygame编组)时,Python要求该列表的长度在整个循环 中保持不变。因为不能从for 循环遍历的列表或编组中删除元素,所以必须遍历编 组的副本。我们使用方法copy() 来设置for 循环 (#1) ,从而能够在循环中修 改bullets 。我们检查每颗子弹,看看它是否从屏幕顶端消失 (#2) 。如果是, 就将其从bullets 中删除 (#3) 。在 (#4) 处,使用函数调用print() 显示当前还 有多少颗子弹,以核实确实删除了消失的子弹。
1.6.6限制子弹数量
settings.py
# 子弹设置
self.bullet_speed = 1.0
self.bullet_width = 3
self.bullet_height = 15
self.bullet_color = (252, 170, 60)
self.bullets_allowed = 3
这将未消失的子弹数限制为三颗。在AlienInvasion 的_fire_bullet() 中, 在创建新子弹前检查未消失的子弹数是否小于该设置:
alien_invasion.py
def _fire_bullet(self):
"""创建新子弹并将其加入编组bullets中。"""
if len(self.bullets) < self.settings.bullets_allowed:
new_bullet = Bullet(self)
self.bullets.add(new_bullet)
玩家按空格键时,我们检查bullets 的长度。如果len(bullets) 小于3,就创 建一颗新子弹;但如果有三颗未消失的子弹,则玩家按空格键时什么都不会发生。 如果现在运行这个游戏,屏幕上最多只能有三颗子弹。
1.6.7 创建方法 _update_bullets()
编写并检查子弹管理代码后,可将其移到一个独立的方法中。
alien_invasion.py
def _update_bullets(self):
"""更新子弹的位置并删除消失的子弹。"""
# 更新子弹的位置。
self.bullets.update()
# 删除消失的子弹。
for bullet in self.bullets.copy():
if bullet.rect.bottom <= 0:
self.bullets.remove(bullet)
alien_invasion.py
while True:
self._check_events()
self.ship.update()
self._update_bullets()
self._update_screen()