外星人入侵(四)
- 1.前文总结回顾
- 1.1 alien_invasion.py
- 1.2 settings.py
- 1.3 ship.py
- 2.射击
- 2.1 添加子弹设置
- 2.2 创建Bullet类
- 2.3 将子弹存储到编组中
- 2.4 开火
- 2.5 删除消失的子弹
- 2.6 限制子弹数量
- 2.7 创建方法_update_bullets()
1.前文总结回顾
1.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.2 settings.py
文件settings.py包含Settings类,这个类只包含方法__init__(),用于初始化控制游戏外观和飞船速度的属性。
1.3 ship.py
文件ship.py包含Ship类,这个类包含方法__init__()、管理飞船位置的方法update()和在屏幕上绘制飞船的方法blitme()。表示飞船的图像存储在文件夹images下的文件ship.bmp中。
2.射击
下面来添加射击功能。我们将编写在玩家按空格键时发射子弹(用小矩形表示)的代码。子弹将在屏幕中向上飞行,抵达屏幕上边缘后消失。
2.1 添加子弹设置
首先,更新settings.py,在方法__init__()末尾存储新类Bullet所需的值:
def __init__(self):
--snip--
# 子弹设置
self.bullet_speed = 1.0
self.bullet_width = 3
self.bullet_height = 15
self.bullet_color = (60, 60, 60)
这些设置创建宽3像素、高15像素的深灰色子弹。子弹的速度比飞船稍低。
2.2 创建Bullet类
下面来创建存储Bullet类的文件bullet.py,其前半部分如下:
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,
self.settings.bullet_height)
❷ self.rect.midtop = ai_game.ship.rect.midtop
# 存储用小数表示的子弹位置。
❸ self.y = float(self.rect.y)
Bullet类继承了从模块pygame.sprite导入的Sprite类。通过使用精灵(sprite),可将游戏中相关的元素编组,进而同时操作编组中的所有元素。为创建子弹实例,init()需要当前的AlienInvasion实例,我们还调用了super()来继承Sprite。另外,我们还定义了用于存储屏幕以及设置对象和子弹颜色的属性。
在❶处,创建子弹的属性rect。子弹并非基于图像,因此必须使用pygame.Rect()类从头开始创建一个矩形。创建这个类的实例时,必须提供矩形左上角的[插图]坐标和[插图]坐标,以及矩形的宽度和高度。我们在(0, 0)处创建这个矩形,但下一行代码将其移到了正确的位置,因为子弹的初始位置取决于飞船当前的位置。子弹的宽度和高度是从self.settings中获取的。
在❷处,将子弹的rect.midtop设置为飞船的rect.midtop。这样子弹将从飞船顶部出发,看起来像是从飞船中射出的。我们将子弹的[插图]坐标存储为小数值,以便能够微调子弹的速度(见❸)。
下面是bullet.py的第二部分,包括方法update()和draw_bullet():
def update(self):
"""向上移动子弹。"""
#更新表示子弹位置的小数值。
❶ self.y -= self.settings.bullet_speed
# 更新表示子弹的rect的位置。
❷ self.rect.y = self.y
def draw_bullet(self):
"""在屏幕上绘制子弹。"""
❸ pygame.draw.rect(self.screen, self.color, self.rect)
方法update()管理子弹的位置。发射出去后,子弹向上移动,意味着其[插图]坐标将不断减小。为更新子弹的位置,从self.y中减去settings .bullet_speed的值(见❶)。接下来,将self.rect.y设置为self.y的值(见❷)。
属性bullet_speed让我们能够随着游戏的进行或根据需要提高子弹的速度,以调整游戏的行为。子弹发射后,其[插图]坐标始终不变,因此子弹将沿直线垂直向上飞行。
需要绘制子弹时,我们调用draw_bullet()。draw.rect()函数使用存储在self.color中的颜色填充表示子弹的rect占据的屏幕部分(见❸)。
2.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循环中更新子弹的位置:
def run_game(self):
"""开始游戏主循环。"""
while True:
self._check_events()
self.ship.update()
❶ self.bullets.update()
self._update_screen()
对编组调用update()时(见❶),编组自动对其中的每个精灵调用update()。因此代码行bullets.update()将为编组bullets中的每颗子弹调用bullet.update()。
2.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
class AlienInvasion:
--snip--
def _check_keydown_events(self, event):
--snip--
elif event.key == pygame.K_q:
sys.exit()
❷ elif event.key == pygame.K_SPACE:
self._fire_bullet()
def _check_keyup_events(self, event):
--snip--
def _fire_bullet(self):
"""创建一颗子弹,并将其加入编组bullets中。"""
❸ new_bullet = Bullet(self)
❹ self.bullets.add(new_bullet)
def _update_screen(self):
"""更新屏幕上的图像,并切换到新屏幕。"""
self.screen.fill(self.settings.bg_color)
self.ship.blitme()
❺ for bullet in self.bullets.sprites():
bullet.draw_bullet()
pygame.display.flip()
--snip--
首先导入Bullet类(见❶),再在玩家按空格键时调用_fire_bullet()(见❷)。在_fire_bullet()中,创建一个Bullet实例并将其赋给new_bullet(见❸),再使用方法add()将其加入编组bullets中(见❹)。方法add()类似于append(),不过是专门为Pygame编组编写的。
方法bullets.sprites()返回一个列表,其中包含编组bullets中的所有精灵。为在屏幕上绘制发射的所有子弹,遍历编组bullets中的精灵,并对每个精灵调用draw_bullet()(见❺)。
如果此时运行alien_invasion.py,将能够左右移动飞船,并发射任意数量的子弹。子弹在屏幕上向上飞行,抵达屏幕顶部后消失得无影无踪,如图12-3所示。你可在settings.py中修改子弹的尺寸、颜色和速度。
2.5 删除消失的子弹
当前,子弹在抵达屏幕顶端后消失,但这仅仅是因为Pygame无法在屏幕外面绘制它们。这些子弹实际上依然存在,其[插图]坐标为负数且越来越小。这是个问题,因为它们将继续消耗内存和处理能力。
需要将这些消失的子弹删除,否则游戏所做的无谓工作将越来越多,进而变得越来越慢。为此,需要检测表示子弹的rect的bottom属性是否为零。如果是,则表明子弹已飞过屏幕顶端:
alien_invasion.py
def run_game(self):
"""开始游戏主循环。"""
while True:
self._check_events()
self.ship.update()
self.bullets.update()
# 删除消失的子弹。
❶ for bullet in self.bullets.copy():
❷ if bullet.rect.bottom <= 0:
❸ self.bullets.remove(bullet)
❹ print(len(self.bullets))
self._update_screen()
使用for循环遍历列表(或Pygame编组)时,Python要求该列表的长度在整个循环中保持不变。因为不能从for循环遍历的列表或编组中删除元素,所以必须遍历编组的副本。我们使用方法copy()来设置for循环(见❶),从而能够在循环中修改bullets。我们检查每颗子弹,看看它是否从屏幕顶端消失(见❷)。如果是,就将其从bullets中删除(见❸)。在❹处,使用函数调用print()显示当前还有多少颗子弹,以核实确实删除了消失的子弹。
如果这些代码没有问题,我们发射子弹后查看终端窗口时,将发现随着子弹一颗颗地在屏幕顶端消失,子弹数将逐渐降为零。运行该游戏并确认子弹被正确删除后,请将这个函数调用print()删除。如果不删除,游戏的速度将大大降低,因为将输出写入终端花费的时间比将图形绘制到游戏窗口花费的时间还要多。
2.6 限制子弹数量
很多射击游戏对可同时出现在屏幕上的子弹数量进行了限制,以鼓励玩家有目标地射击。下面在游戏《外星人入侵》中做这样的限制。
首先,在settings.py中存储最大子弹数:
# 子弹设置
--snip--
self.bullet_color = (60, 60, 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,就创建一颗新子弹;但如果有三颗未消失的子弹,则玩家按空格键时什么都不会发生。如果现在运行这个游戏,屏幕上最多只能有三颗子弹。
2.7 创建方法_update_bullets()
编写并检查子弹管理代码后,可将其移到一个独立的方法中,确保AlienInvasion类组织有序。为此,创建一个名为_update_bullets()的新方法,并将其放在_update_screen()前面:
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)
_update_bullets()的代码是从run_game()剪切并粘贴而来的,这里只是让注释更清晰了。
run_game()中的while循环又变得简单了:
alien_invasion.py
while True:
self._check_events()
self.ship.update()
self._update_bullets()
self._update_screen()
我们让主循环包含尽可能少的代码,这样只要看方法名就能迅速知道游戏中发生的情况。主循环检查玩家的输入,并更新飞船的位置和所有未消失子弹的位置。然后,使用更新后的位置来绘制新屏幕。
请再次运行alien_invasion.py,确认发射子弹时没有错误。