当前,如果玩家按住箭头键的时间足够长,飞船将移到屏幕外面,消失得无影无踪。下面来修复这种问题,让飞船到达屏幕边缘后停止移动。为此,我们将修改 ship类的方法 update():
def update(self):
#根据移动标志调整飞船的位置
#更新飞船的centerx值,而不是rect
if self.moving_right and self.rect.right < self.screen_rect.right:
self.center += self.ai_settings.ship_speed_factor
if self.moving_left and self.rect.left > 0:
self.center -= self.ai_settings.ship_speed_factor
#根据self.center更新rect值
self.rect.centerx = self.center
上述代码在修改 self.center 的值之前检查飞船的位置。self.rect.right 返回飞船外接矩形的有边缘的x坐标,如果这个值小于self.screen_rect.right 的值,就说明飞船未触及屏幕右边缘。左边缘的情况与此类似:如果rect的左边缘的x坐标大于零,就说明飞船未触及屏幕左边缘。这确保仅当飞船在屏幕内时,才调整 self.center 的值。
如果此时 alien_invasion.py,飞船将在触及屏幕左边缘或右边缘后停止移动。
随着游戏开发的进行,函数 check_events() 将越来越长,我们将其部分代码放在两个函数中:
一个负责处理KEYDOWN事件,另一个处理 KEYUP事件:
import sys
import pygame
def check_keydown_events(event,ship):
if event.key == pygame.K_RIGHT:
ship.moving_right = True
elif event.key == pygame.K_LEFT:
ship.moving_left = True
def check_keyup_events(event,ship):
if event.key == pygame.K_RIGHT:
ship.moving_right = False
elif event.key == pygame.K_LEFT:
ship.moving_left = False
def check_events(ship):
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
elif event.type == pygame.KEYDOWN:
check_keydown_events(event,ship)
elif event.type == pygame.KEYUP:
check_keyup_events(event,ship)
def update_screen(ai_settings,screen,ship):
#每次循环时都重绘屏幕
screen.fill(ai_settings.bg_color)
ship.blitme()
#让最近绘制的屏幕可见
pygame.display.flip()
项目到此,我们完成了窗口绘制,飞船绘制和位置设置。还设置了飞船的移动速度,让其进行到一定程度时加快游戏节奏,当然,还有飞船的左右移动。我们一共创建了四个模块。下面我们来总结一下四个模块的分别作用:
主文件创建一系列整个游戏都要使用的对象:存储在 ai_settings中的设置、存储在 screen中的
surface以及一个飞船实例。文件 alien_invasion还包含游戏的主循环,这是一个调用
check_events()、ship.update()和update_screen()的while循环。
要启动《外星人入侵》,只需运行文件alien_invasion.py。其他文件(settings.py、game_functions.py、ship.py)包含的代码被直接或间接地导入到这个文件中。
文件 settings.py包含Settings类,这个类只包含 __ init __(),它负责初始化控制游戏外观和飞船速度的属性。
该文件包含一系列函数,游戏的大部分工作都是由它们完成的。函数check_events()检测相关的事件,如松键或按键,并使用辅助函数
check_keydown_events()和check_keyup_events()来处理事件。就目前而言,这些函数管理飞船的移动。模块game_functions还包含
update_screen(),它用于在每次执行主循环时都重绘屏幕。
该文件包含 Ship类,这个类包含方法 __ init __(),管理飞船位置的方法 update()以及在屏幕上绘制飞船的方法
blitme()。表示飞船的图像存储在文件夹 images下的ship.bmp中。
下面我们来给飞船增加射击功能,我们将编写玩家按空格键时发射子弹(小矩形)的代码。子弹将在屏幕中向上穿行,抵达屏幕边缘后消失。
首先,更新 settings.py,在其方法__ init __() 末尾存储新类 Bullet所需的值:
self.bullet_speed_factor = 1
self.bullet_width = 3
self.bullet_height = 15
self.bullet_color = 60,60,60
这里我们创建存储 Bullet类的文件 bullet.py,其前半部分如下:
import pygame
from pygame.sprite import Sprite
class Bullet(Sprite):
#一个对飞船发射子弹管理的类
def __init__(self,ai_settings,screen,ship):
#在飞船所处位置创建一个子弹对象
super(Bullet,self).__init__()
self.screen = screen
#在(0,0)处创建一个表示子弹的矩形,再设置正确的位置
self.rect = pygame.Rect(0,0,ai_settings.bullet_width,
ai_settings.bullet_height)
self.rect.centerx = ship.rect.centerx
self.rect.top = ship.rect.top
#存储用小数表示的子弹位置
self.y = float(self.rect.y)
self.color = ai_settings.bullet_color
self.speed_factor = ai_settings.bullet_speed_factor
Bullet类继承了我们从模块pygame.sprite中导入的Sprite类。通过使用精灵,可将游戏中相关的元素编组,进而同时操作编组中的所有元素。为创建子弹实例,需要向__ init __ ()传递ai_settings、screen和ship实例,还调用了super()来继承Sprite
code 说明
self.rect = pygame.Rect(0,0,ai_settings.bullet_width,ai_settings.bullet_height)
创建子弹的属性rect。子弹并非基于图像的,因此我们必须使用pygame.Rect()类从空白开始创建一个矩形。创建这个类的实例时,必须提供矩形左上角的x坐标和y坐标,还有矩形的宽度和高度。我们在(0,0)处创建这个矩形,但接下来的两行代码将其移到了正确的位置,因为子弹的初始位置取决于飞船当前的位置。子弹的官渡和高度是从ai_settings中获取的。
self.rect.centerx = ship.rect.centerx
self.rect.top = ship.rect.top
我们将子弹的centerx设置为飞船的rect.centerx。子弹应从飞船顶不射出,因此我们将表示子弹的rect的top属性设置为飞船的rect的top属性,让子弹看起来像是从飞船中射出的。
#存储用小数表示的子弹位置
self.y = float(self.rect.y)
self.color = ai_settings.bullet_color
self.speed_factor = at_settings.bullet_speed_factor
我们将子弹的y坐标存储为小数值,以便能够微调子弹的速度。然后将子弹的颜色和速度分别设置存储到self.color和self.speed_factor中。
下面是bullet.py的第二部分----方法update()和draw_bullet():
def update(self):
#向上移动子弹
#更新表示子弹位置的小数值
self.y -= self.speed_factor
self.rect.y = self.y
def draw_bullet(self):
#在屏幕上绘制子弹
pygame.draw.rect(self.screen,self.color,self.rect)
方法update()管理子弹的位置。发射出去后,子弹在屏幕中向上移动,这意味着y坐标将不断减小,因此为更新子弹的位置,我们从self.y中减去self.speed_factor的值。接下来,我们将self.rect.y设置为 self.y的值。属性speed_factor让我们能够随着游戏的进行或根据需要提高子弹的速度,以调整游戏的行为。子弹发射后,其x坐标始终不变,因此子弹将沿直线垂直地往上穿行。
在需要绘制子弹时,我们调用draw_bullet()。函数draw.rect()使用存储在self.color中的颜色填充表示子弹的rect占据的屏幕部分。
定义Bullet类和必要的设置后,我们就可以编写代码了。
在玩家每次按空格键时都射出一发子弹。
首先,我们将在alien_invasion.py中创建一个编组(group),用于存储所有有效的子弹,以便能够管理发射出去的所有子弹。这个编组将是pygame.sprite.Group类的一个实例;pygame.sprite.Group类类似于列表,但提供了有助于开发游戏的额外功能。在主循环中,我们将使用这个编组在屏幕上绘制子弹,以及更新每颗子弹的位置:
from pygame.sprite import Group
#创建一个用于存储子弹的编组
bullets = Group()
# while 循环来控制游戏
while True:
gf.check_events(ai_settings,screen,ship,bullets)
ship.update()
bullets.update()
gf.update_screen(ai_settings,screen,ship,bullets)
在这里我们导入pygame.sprite中的Group类。
这个编组是在while循环外面创建的,这样就无需每次运行该循环时都创建一个新的子弹编组。
将bullets传递给了check_events()和update_screen(),在check_events()中,需要在玩家按空格键时处理bullets;而在update_screen()中,需要更新要绘制到屏幕上的bullets。
当你对编组调用update()时,编组将自动对其中的每个精灵调用update(),因此代码行bullest.update()将为编组bullest中的每颗子弹调用bullet.update()
在game_funcations.py中:
我们需要修改check_keydown_events(),以便在玩家按空格键时发射一颗子弹。我们无需修改check_keyup_events(),因为玩家松开空格键时什么都不会发生。我们还需要修改update_screen(),确保在调用flip()前在屏幕上重绘每颗子弹。
from bullet import Bullet
def check_keydown_events(event,ai_settings,screen,ship,bullets):
if event.key == pygame.K_RIGHT:
ship.moving_right = True
elif event.key == pygame.K_LEFT:
ship.moving_left = True
elif event.key == pygame.K_SPACE:
#创建一颗子弹,并将其加入到编组bullets中
new_bullet = Bullet(ai_settings,screen,ship)
bullets.add(new_bullet)
def check_events(ai_settings,screen,ship,bullets):
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
elif event.type == pygame.KEYDOWN:
check_keydown_events(event,ai_settings,screen,ship,bullets)
elif event.type == pygame.KEYUP:
check_keyup_events(event,ship)
def update_screen(ai_settings,screen,ship,bullets):
#每次循环时都重绘屏幕
screen.fill(ai_settings.bg_color)
#在飞船和外星人后面重绘所有子弹
for bullet in bullets.sprites():
bullet.draw_bullet()
ship.blitme()
#让最近绘制的屏幕可见
pygame.display.flip()
编组bullets传递给了check_keydown_events():
玩家按空格键时,创建一颗新子弹:new_bullet的Bullet实例,并使用方法add()将其加入到编组bulllets中;代码bullet.add(new_bullet)将新子弹存储到编组bullets中。
在check_events()的定义中,我们需要添加形参bullets;调用check_keydown_events()时,我们也需要将bullets作为实参传递给它。
def update_screen(ai_settings,screen,ship,bullets):
我们给在屏幕上绘制子弹的update_screen()添加了形参bullets。方法bullets.sprites()返回一个列表,其中包含编组bullets中的所有精灵。为在屏幕上绘制发射的所有子弹,我们遍历编组bullets中的所有精灵,并对每个精灵都调用draw_bullet()
如果此时运行alien_invasion.py,将能够左右移动飞船,并发射任意数量的子弹。子弹会在屏幕中向上穿行,抵达屏幕顶步后消失
我们可以在settings.py中修改子弹的尺寸、颜色和速度。
当前,子弹抵达屏幕顶端后消失,这仅仅是因为pygame无法在屏幕外面绘制它们。这些子弹实际上依然存在,它们的y坐标为负数,且越来越小。这是一个问题,因为它们将继续消耗内存和处理能力。
所以我们需要将这些已消失的子弹删除,否则游戏所做的无谓工作将越来越多,进而变得越来越慢。
检测这样的条件,即表示子弹的rect的bottom属性为零,它表名子弹已穿过屏幕顶端:
alien_invasion.py
#在while循环中,删除已消失的子弹
for bullet in bullets.copy():
if bullet.rect.bottom <= 0:
bullets.remove(bullet)
print(len(bullets))
在for循环中,不应从列表或编组中删除条目,因此必须遍历编组的副本。
我们使用方法copy()设置for循环,这让我们能够在for循环中修改bullets。我们检查每颗子弹,看看它是否已从屏幕顶部消失。如果是这样,就将其从bullets中删除。
print()语句用来显示当前子弹还有多少颗,从而核实已消失的子弹确实删除了。
.
如果代码最后没有问题,确认子弹可以正常删除,我们将print语句删除,如果留下这条语句,游戏的速度将大大降低,因为将输出写入到终端而花费的时间比将图形绘制到游戏窗口花费的时间还多。
很多射击游戏都对可同时出现在屏幕上的子弹数量进行限制,以鼓励玩家有目标地射击。下面我们也加入同样的限制。
首先,在 settings.py中存储允许的最大子弹数:
self.bullets_allowed = 3
将未消失的子弹限制为3颗。
在game_functions.py的check_keydown_events()中,我们在创建新子弹前检查未消失的子弹数是否小于该设置:
def check_keydown_events(event,ai_settings,screen,ship,bullets):
if event.key == pygame.K_RIGHT:
ship.moving_right = True
elif event.key == pygame.K_LEFT:
ship.moving_left = True
elif event.key == pygame.K_SPACE:
#创建一颗子弹,并将其加入到编组bullets中
if len(bullets) <ai_settings.bullets_allowed:
new_bullet = Bullet(ai_settings,screen,ship)
bullets.add(new_bullet)
玩家按空格时,我们检查bullets的长度,如果len(bullets)小于3,我们就创建一个新子弹;但如果已有3颗未消失的子弹,则玩家按空格键时什么都不发生。如果你现在运行这个游戏,屏幕上最多只能有3颗子弹。
编写并检查子弹管理代码后,可将其移动模块
game_functions中,以让主程序文件alien_invasion.py尽可能简单,我们创建一个名为
update_bullets()的新函数,并将其添加到game_functions.py的末尾。
def update_bullets(bullets):
#更新子弹的位置,并删除已消失的子弹
#更新子弹的位置
bullets.update()
#删除已消失的子弹
for bullet in bullets.copy():
if bullet.rect.bottom <= 0:
bullets.remove(bullet)
这个函数中的代码是从 alien_invasion.py 剪切粘贴过来的,它只需要一个参数,即编组bullets。
alien_invasion.py中的while循环又变得很简单了:
# while 循环来控制游戏
while True:
gf.check_events(ai_settings,screen,ship,bullets)
ship.update()
bullets.update()
gf.update_bullets(bullets)
gf.update_screen(ai_settings,screen,ship,bullets)
我们让主循环包含尽可能少的代码,这样只要看函数名就能迅速知道游戏中发生的情况。主循环检查玩家的输入,然后更新飞船的位置和所有未消失的子弹的位置。接下来,我们使用更新后的位置来绘制新屏幕。
下面将发射子弹的代码移到一个独立的函数中,这样,
在check_keydown_events()中只需使用一行代码来发射子弹,让elif代码变得飞船简单。
def check_keydown_events(event,ai_settings,screen,ship,bullets):
if event.key == pygame.K_RIGHT:
ship.moving_right = True
elif event.key == pygame.K_LEFT:
ship.moving_left = True
elif event.key == pygame.K_SPACE:
#创建一颗子弹,并将其加入到编组bullets中
fire_bullet(ai_settings,screen,ship,bullets)
def fire_bullet(ai_settings,screen,ship,bullets):
#如果还没有达到限制,就发射一颗子弹
#创建新子弹,并将其加入到编组 bullets中
if len(bullets) <ai_settings.bullets_allowed:
new_bullet = Bullet(ai_settings,screen,ship)
bullets.add(new_bullet)
函数 fire_bullet() 只包含玩家按空格键时用于发射子弹的代码;
在check_keydown_events()中,我们在玩家按空格键时调用 fire_bullet()。