在之前的博文中我们为游戏添加了随分数累加的难度递增机制,这就带来一个问题:当到达后面的难度等级后,敌方飞机铺天盖地而来,我方小飞机根本应付不过来,因此在这篇博文中我们为我方飞机赋予必杀技——随机补给全屏炸弹和超级子弹。
首先来简单描述这两个必杀技,全屏炸弹是指在游戏过程中,当用户按下空格键时,就触发一枚全屏炸弹(如果当前有的话),此时屏幕上的所有敌机立即销毁。超级子弹是指玩家在接收到指定补给包之后,我方飞机能够一次发射两发子弹,攻击力加倍。ok,开始工作。
1、定义supply模块
定义一个名为supply.py的模块用以保存超级武器的相关属性,包含全屏炸弹补给类和超级子弹补给类。对于全屏炸弹补给类,这里先给出完整代码:
~~~
# ====================定义超级炸弹补给包====================
class BombSupply(pygame.sprite.Sprite):
def __init__(self, bg_size):
pygame.sprite.Sprite.__init__(self)
self.image = pygame.image.load("image/ufo2.png")
self.rect = self.image.get_rect()
self.width, self.height = bg_size[0], bg_size[1]
self.rect.left, self.rect.bottom = randint(0, self.width - self.rect.width), -100
self.speed = 5
self.active = False
self.mask = pygame.mask.from_surface(self.image)
def move(self):
if self.rect.top < self.height:
self.rect.top += self.speed
else:
self.active = False
def reset(self):
self.active = True
self.rect.left, self.rect.bottom = randint(0, self.width - self.rect.width), -100
~~~
可见BombSupply类结构与之前敌方飞机的类结构非常相似,其实补给包本身就和敌机具有相同的属性:随机位置初始化,以一定速度向下移动,需要进行掩膜碰撞检测(只不过碰撞之后不会造成我方敌机损毁)等等。类似的,超级子弹补给类的代码如下,两者在结构上完全相同,不再赘述。
~~~
# ====================定义超级子弹补给包====================
class BulletSupply(pygame.sprite.Sprite):
def __init__(self, bg_size):
pygame.sprite.Sprite.__init__(self)
self.image = pygame.image.load("image/ufo1.png")
self.rect = self.image.get_rect()
self.width, self.height = bg_size[0], bg_size[1]
self.rect.left, self.rect.bottom = randint(0, self.width - self.rect.width), -100
self.speed = 5
self.active = False
self.mask = pygame.mask.from_surface(self.image)
def move(self):
if self.rect.top < self.height:
self.rect.top += self.speed
else:
self.active = False
def reset(self):
self.active = True
self.rect.left, self.rect.bottom = randint(0, self.width - self.rect.width), -100
~~~
2、实例化超级炸弹
值得注意的一点是,在游戏初始是自带3个全屏炸弹的,因此我们先不启动补给发放机制,先把这三发超级炸弹使用好。
首先在游戏运行时需要将全屏炸弹的图标和剩余数量显示在画面左下角的位置,由于炸弹数量是可变的(我方飞机可能吃到补给包),因此在main函数中初始化一个全局变量用来保存当前全屏炸弹的数量:
~~~
bomb_num = 3 # 初始为三个炸弹
~~~
然后加载全屏炸弹的小图标并获得其位置属性,注意之前supply.py模块中加载的图片是补给包的图标(带小降落伞的),并非全屏炸弹的图标:
~~~
bomb_image = pygame.image.load("image/bomb.png") # 加载全屏炸弹图标
bomb_rect = bomb_image.get_rect()
bomb_front = score_font
~~~
注意既然接下来既然需要显示字符(全屏炸弹数量),我们就直接应用之前创建好的用来打印分数的字体对象即可(详见上篇博文)。接下来的工作就是在main()函数的while循环中将图片以及炸弹数量实时的绘制到屏幕上:
~~~
# ====================绘制全屏炸弹数量和剩余生命数量====================
bomb_text = bomb_front.render("× %d" % bomb_num, True, color_black)
bomb_text_rect = bomb_text.get_rect()
screen.blit(bomb_image, (10, height - 10 - bomb_rect.height))
screen.blit(bomb_text, (20 + bomb_rect.width, height - 10 - bomb_text_rect.height))
~~~
注意这里由于要求炸弹图标的乘号以及炸弹数量并排显示,因此需要获取图片与字体的尺寸,至于如何摆放,相信大家稍加推敲就能理解的。此时运行程序,屏幕左下角正常显示图标。
3、触发全屏炸弹
既然已经把全屏炸弹显示出来了,仅仅放在那震慑是不够的,每当用户按下一次空格键,就触发一枚全屏炸弹,屏幕上所有敌机立即损毁。当然,我们首先需要知道用户什么时候按下了空格键:
~~~
# ====================检测用户的退出及暂停操作====================
for event in pygame.event.get(): # 响应用户的偶然操作
if event.type == QUIT:
# 如果用户按下屏幕上的关闭按钮,触发QUIT事件,程序退出elif event.type == KEYDOWN:
if event.key == K_SPACE: # 如果检测到用户按下空格键
if bomb_num: # 如果炸弹数量大于零,则引爆一颗超级炸弹
bomb_num -= 1
bomb_sound.play()
for each in enemies:
if each.rect.bottom > 0: # 屏幕上的所有敌机均销毁
each.active = False
~~~
由于空格键的按下属于偶然操作,因此采用“pygame.event.get()”的事件响应机制,代码结构简单,即当检测到用户键盘按下并且对应键值为“K_SPACE”时,炸弹数量减一(如果炸弹数量大于零),播放全屏炸弹音效、屏幕上所有敌机损毁,在这里也能体现出当初我们在实例化敌机对象时将敌机统一放在了“enemies”这个精灵组结构中的优势了,操作异常便捷。
4、开启补给机制
全屏炸弹顺利施放,接下来开启补给机制。设定为每10秒发放一次补给,我们通过触发时间机制来完成这个功能,即将补给发放定义为一个用户时间,每隔30秒触发一次,在响应时间的过程中初始化补给包,首先在main()函数中定义事件:
~~~
supply_timer = USEREVENT # 补给包发放定时器
pygame.time.set_timer(supply_timer, 10 * 1000) # 定义每10秒发放一次补给包
~~~
注意Pygame模块中为每个事件通过宏定义的方式定义了一个标号,用以区分各个时间。我们在定义自己的用户事件的时候也要人为的为其赋值一个事件标号,为了保证用户定义的事件标号与系统事件标号不冲突,Pygame特意提供了“USEREVENT”这个宏,标号在(0,USEREVENT-1)的事件为系统时间,因此此处我们将定义的用户事件指定为“USEREVENT”是不会和系统事件发生冲突的。事件定义完之后调用time类中的set_timer()函数设定事件触发的事件间隔,注意这里的时间是以毫秒为单位的,因此需要乘以1000转换成秒。
当然要想顺利的打印补给包,首先需要对其进行实例化(和敌方飞机的实例话类似):
~~~
# ====================实例化补给包====================
bullet_supply = supply.BulletSupply(bg_size)
bomb_supply = supply.BombSupply(bg_size)
~~~
接下来在whlie()循环内部编写补给定时器“supply_timer”的事件响应函数:
~~~
# ====================检测用户的退出及暂停操作====================
for event in pygame.event.get(): # 响应用户的偶然操作
if event.type == QUIT:
# 如果用户按下屏幕上的关闭按钮,触发QUIT事件,程序退出elif event.type == supply_timer: # 响应补给发放的事件消息
if choice([True, False]):
bomb_supply.reset()
else:
bullet_supply.reset()
~~~
事件响应函数的主要任务是确定当前应该实例化何种补给包,是全屏炸弹补给包还是超级子弹补给包,这种选择应该是随机的,因此可以用“choice()”实现随机选择机制,然后再根据随机结果将对应的补给包进行复位操作(reset会将对应的补给对象的active变量激活),继而引导接下来的补给发放机制。
5、检测用户是否获得补给包
在通过事件响应函数确定当前发放的补给类型之后,接下里就需要检测我方飞机是否顺利拾获了补给包,核心是通过碰撞检测机制来完成,首先判断是否获得了全屏炸弹补给包:
~~~
# ====================绘制补给并检测玩家是否获得====================
if bomb_supply.active: # 如果是超级炸弹补给包
bomb_supply.move()
screen.blit(bomb_supply.image, bomb_supply.rect)
if pygame.sprite.collide_mask(bomb_supply, me): # 如果玩家获得超级炸弹补给包
get_bomb_sound.play()
if bomb_num < 3:
bomb_num += 1
bomb_supply.active = False
~~~
程序思路很清晰,如果当前全屏炸弹补给包被激活,则打印其图标并开始自动移动,在移动过程中若与我方飞机发生了碰撞(碰撞检测返回true),则认为玩家顺利获取了全屏炸弹补给包,此时再判断玩家剩余全屏炸弹的数量,如果小于三个,则加一,之后再将补给包对象的active变量置为false,关闭补给对象。
接下来判断用户是否获得超级子弹补给包,这里有一点需要注意,就是在获得全屏炸弹之后,我方飞机将以此发射两发子弹,因此需要设置一个标志位“is_double_bullet”来表示当前的子弹状态,“is_double_bullet = false”表示当前发射的是普通子弹,“is_double_bullet = true”表示当前发射的是超级子弹,首先在main函数中声明变量并初始化:
~~~
is_double_bullet = False # 是否使用超级子弹标志位
~~~
然后开始进行超级子弹补给包的碰撞检测:
~~~
if bullet_supply.active: # 如果是超级子弹补给包
bullet_supply.move()
screen.blit(bullet_supply.image, bullet_supply.rect)
if pygame.sprite.collide_mask(bullet_supply, me):
get_bullet_sound.play()
is_double_bullet = True
bullet_supply.active = False
~~~
既然添加了两种子弹属性,因此有必要在绘制子弹之前进行一把判断了,不过到现在为止我们貌似还没有定义超级子弹类,之前在定义bullet.py模块的时候曾经提到过,Bullet1代表普通子弹,Bullet2代表超级子弹,在这里我们将bullet模块中的Bullet2的类定义代码补上,和Bullet1的结构完全相同:
~~~
# ====================定义超级子弹====================
class Bullet2(pygame.sprite.Sprite):
def __init__(self, position):
pygame.sprite.Sprite.__init__(self)
self.image = pygame.image.load("image/bullet2.png")
self.rect = self.image.get_rect()
self.rect.left, self.rect.top = position
self.speed = 14
self.active = True
self.mask = pygame.mask.from_surface(self.image)
def move(self):
if self.rect.top < 0:
self.active = False
else:
self.rect.top -= self.speed
def reset(self, position):
self.rect.left, self.rect.top = position
self.active = True
~~~
在定义好Bullet2之后,需要在主程序中实例化超级子弹。和之前普通子弹的实例化方式类似(参见之前博文),只是这里在子弹实例话的顺序位置安排上有一点小技巧,先上代码:
~~~
# ====================生成超级子弹====================
bullet2 = []
bullet2_index = 0
bullet2_num = 10 # 定义子弹实例化个数
for i in range(bullet2_num//2):
bullet2.append(bullet.Bullet2((me.rect.centerx - 33, me.rect.centery)))
bullet2.append(bullet.Bullet2((me.rect.centerx + 30, me.rect.centery)))
~~~
可见,这里的超级子弹是成对实例化的(10发子弹,分为5次循环),即敌机左边一发右边一发,这样只要在将来打印的过程中一次打印相邻的两个子弹对象,就能产生“一次射两发”的效果了,类似的,如何实现一次射三发、四发,大家应该也都能掌握了。
接下来需要做的就是对原有的子弹显示程序进行修改,增加一项判断分支,代码如下:
~~~
if not (delay % 10):
# 播放子弹音效if not is_double_bullet:
# 发射普通子弹else: # 如果当前是超级子弹状态
bullets = bullet2
bullets[bullet2_index].reset((me.rect.centerx - 33, me.rect.centery))
bullets[bullet2_index + 1].reset((me.rect.centerx + 30, me.rect.centery))
bullet2_index = (bullet2_index + 2) % bullet2_num
~~~
果不其然,这里在打印超级子弹的时候一次激活子弹组中的相邻两发子弹,即实现左右两边各发射一发子弹的效果。这里之所有采用bullets这个中间变量,是为了简化接下来的子弹与敌机的碰撞检测(不必再区分普通子弹和超级子弹)。至于(me.rect.centerx - 33, me.rect.centery)、(me.rect.centerx - 33, me.rect.centery)这两个坐标位置也是经验值,正好代表我方飞机的两个发动机的位置。
6、设置超级子弹时间定时器
运行程序,上面的所有预期功能都正常事项,不过存在一个问题,就是一旦我们获取了超级子弹补给包,则我方飞机就会无休止的发射超级子弹,这显然是不合理的,毕竟超级武器还是超级武器,一直都能用就不能体现其珍贵了,因此我们添加一个机制,即获得超级子弹补给包之后,超级子弹转态只能持续一定时间,规定时间过后自动变为普通子弹。
同样我们通过事件触发机制完成这个功能,首先在main()函数中定义这个事件:
~~~
double_bullet_timer = USEREVENT + 1 # 超级子弹持续时间定时器
~~~
在成功获取超级子弹补给包之后,开启这个定时器:
~~~
if bullet_supply.active: # 如果是超级子弹补给包
pass
if pygame.sprite.collide_mask(bullet_supply, me):
pass
pygame.time.set_timer(double_bullet_timer, 18 * 1000)
~~~
可见这里我们将超级子弹的持续时间设置为18秒。然后编写对应的事件响应函数,在响应过程中关闭超级子弹状态即可:
~~~
# ====================检测用户的退出及暂停操作====================
for event in pygame.event.get(): # 响应用户的偶然操作
if event.type == QUIT:
# 如果用户按下屏幕上的关闭按钮,触发QUIT事件,程序退出elif event.type == double_bullet_timer:
is_double_bullet = False
pygame.time.set_timer(double_bullet_timer, 0)
~~~
注意这里将时间触发时间设置为零表示关闭了这个事件,等待下一次获得超级子弹之后再重新开启。今天的博文就到这里,内容有点多,大家慢慢看吧。