在完成敌方敌机的初步设置后,运行程序我们发现在屏幕上我方飞机和敌方飞机能够友好共存,互不干涉,这显然不符合游戏的宗旨,在这篇文章中我们为游戏添加我方飞机和敌机之间的碰撞损毁机制。
1、碰撞检测
碰撞检测是游戏设计中的最基本的部分,几乎任何游戏中的主角都具有发射一些飞行道具的能力,如何准备判断主角射出的子弹、飞镖、能量球、龟派气功是否准确命中目标,就是碰撞检测所要实现的功能。对于一些规则的图形(例如说圆形),我们可以计算两个圆形圆心之间的距离与其半径的关系来判断其是否已经相撞,但对于一些不规则图形(如我们这里的小飞机),这种简单的方法是行不通的。为此,Pygame模块已经在精灵(Sprite)类中添加了碰撞检测函数:spritecollide()。
Pygame碰撞检测有两种机制,一种是直接调用spritecollide()函数,函数需要传入一个精灵对象和一个精灵组对象,用以检测一个精灵与另一个精灵组中的所有精灵是否发生碰撞。这种基本的碰撞检测机制实际上是存在一定缺陷的。由于它是通过检测两个精灵所拥有的对象图片(精灵的image)是否发生重叠来检测碰撞的发生,当精灵图片除了精灵本身若还存在较大空白区域的话(例如我们的飞机和敌机图片),可能程序在检测到碰撞时只是两个精灵图片的空白部分发生了重叠,而两个精灵对象还并没有重叠到一起,这就给游戏带来了不好的体验,因此在本程序中我们使用另外一种更为精确的碰撞检测方法:基于精灵图像掩膜的碰撞检测方法。
这种方法同样是调用spritecollide()函数,只是在调用时指定函数的调用方式为掩膜检测类型,这样在碰撞检测时程序检测的就是精灵的非透明部分(掩膜)是否发生碰撞,而非整个图像是否发生重叠。至于如何将精灵图片中的背景区域变得透明,大家可以从网上查阅相关方法,不过这里我们给出的图片资源都已经经过了透明化处理,可以拿来直接使用的。
2、为我方飞机和敌方飞机指定掩膜属性以及生存状态标志位
由于需要基于掩膜进行碰撞检测,因此需要在飞机类(包括我方飞机和敌机)中添加一个掩膜(mask)属性:
~~~
self.mask = pygame.mask.from_surface(self.image1) # 获取飞机图像的掩膜用以更加精确的碰撞检测
~~~
需要在MyPlane、SmallEnemy、MidEnemy、BigEnemy这些飞机模块中都添加这句代码,以将其对应图片中不透明部分(精灵的实际面积)转换成掩膜以便碰撞检测时调用。同时,既然我方飞机和敌机随时都有可能损毁(无论是被子弹射中还是被全屏炸弹消灭还是通过撞击来玉石俱焚),因此有必要在类内部添加一个标志位来记录当前对象时正常存活还是已经损毁(我方飞机例外):
~~~
self.active = True # 设置飞机当前的存在属性,True表示飞机正常飞行,False表示飞机已损毁
~~~
同样,这句代码也应该共存于以上三个类中。同时需要在各个类的reset()成员函数中将active标志位置为真,以保证各个类型的飞机在重置之后是激活的状态:
~~~
self.active = True
~~~
3、主程序中进行碰撞检测
接下来在主程序循环中,我们需要实时检测我方飞机是否和敌方飞机的任何一个对象发生了碰撞:
~~~
enemies_down = pygame.sprite.spritecollide(me, enemies, False, pygame.sprite.collide_mask)
if enemies_down: # 如果碰撞检测返回的列表非空,则说明已发生碰撞,若此时我方飞机处于无敌状态
me.active = False
for e in enemies_down:
e.active = False # 敌机损毁
~~~
注意在这里体会基于掩膜碰撞检测的调用方式,只要将spritecollide()的第四个参数指定为pygame.sprite.collide_mask,程序就能自动根据精灵的mask成员变量进行精准的碰撞检测。在这里me表示我方敌机实例,enemies代表敌方飞机的所有实例(精灵组对象),这也是为什么之前我们在调用诸如add_small_enemies等敌机添加控制函数时,每次都把生成的敌机一方面添加到对应类型的精灵组中,另一方面也将其添加到总体敌机的精灵组中的原因:add_small_enemies(small_enemies, enemies, 1)。
spritecollide()这个函数将返回一个列表,如果我方飞机(me)与敌机组(enemies)中的任何一个精灵对象检测到了碰撞,就会将enemies中检测到碰撞的对象添加到结果列表中(enemies_down),在接下来的程序中只需判断enemies_down是否为空,如果非空则说明已发生碰撞,一方面将我方飞机的active标志位置为false,表示我方飞机以挂,同时将检测到发生碰撞的敌机(enemies_down列表中所包含的精灵对象)的标志位也设置为false,达到玉石俱焚的目的。
4、加载损毁图片资源
飞机撞毁之后会有爆炸的特效,这是通过将多张渐进演变的爆炸特效图片一次播放后取得的效果,说白了就是当检测到飞机损毁之后,就一次播放爆炸的图片。爆炸特效的图片资源已经存放在image文件夹下来,其中我方飞机的爆炸特效有四张、小型敌机有四张、中型敌机有四张,大型敌机有六张。
在对应类中加载爆炸图像以便使用,我方飞机(MyPlane类中):
~~~
self.destroy_images = [] # 加载飞机损毁图片
self.destroy_images.extend([pygame.image.load("image/hero_blowup_n1.png"),
pygame.image.load("image/hero_blowup_n2.png"),
pygame.image.load("image/hero_blowup_n3.png"),
pygame.image.load("image/hero_blowup_n4.png")])
~~~
由于飞机损毁图片都是多张,为了方便索引,我们先创建一个名为“destroy_image”的类成员列表变量,然后通过列表的extend()方法将各个图片添加到列表中。一次类推,小型敌机类SmallEnemy:
~~~
self.destroy_images = [] # 加载飞机损毁图片
self.destroy_images.extend([pygame.image.load("image/enemy1_down1.png"),
pygame.image.load("image/enemy1_down2.png"),
pygame.image.load("image/enemy1_down3.png"),
pygame.image.load("image/enemy1_down4.png")])
~~~
中型敌机类MidEnemy:
~~~
self.destroy_images = [] # 加载飞机损毁图片
self.destroy_images.extend([pygame.image.load("image/enemy2_down1.png"),
pygame.image.load("image/enemy2_down2.png"),
pygame.image.load("image/enemy2_down3.png"),
pygame.image.load("image/enemy2_down4.png")])
~~~
大型敌机类BigEnemy:
~~~
self.destroy_images = [] # 加载飞机损毁图片
self.destroy_images.extend([pygame.image.load("image/enemy3_down1.png"),
pygame.image.load("image/enemy3_down2.png"),
pygame.image.load("image/enemy3_down3.png"),
pygame.image.load("image/enemy3_down4.png"),
pygame.image.load("image/enemy3_down5.png"),
pygame.image.load("image/enemy3_down6.png")])
~~~
5、播放爆炸损毁特效
接下来需要在主程序中加入损毁的爆炸效果。基本思路是当程序检测到当前飞机对象(无论是我方飞机还是敌机)因碰撞而挂掉(成员变脸active=false)后,则依次打印其若干张损毁图像。在因此打印的过程中,我们采用索引值的方式来判别接下来应该打印第几张损毁特效图片,因此需要在main函数的开始部分(while之前)先声明各个索引值:
~~~
# ====================飞机损毁图像索引====================
e1_destroy_index = 0
e2_destroy_index = 0
e3_destroy_index = 0
me_destroy_index = 0
~~~
以我方飞机损毁为例,当检测到active变量为true时,正常绘制我方飞机模型,当检测到active为false时,开始绘制损毁特效:
~~~
if me.active:# 绘制我方飞机的两种不同的形式else:
if not (delay % 3):
screen.blit(me.destroy_images[me_destroy_index], me.rect)
me_destroy_index = (me_destroy_index + 1) % 4
if me_destroy_index == 0:
me_down_sound.play()
me.reset()
~~~
与之前飞机尾气喷气特效的原理类似,这里在播放损毁特效时同样需要控制图片的播放速度,因此需要借用延时参数delay(之前的博文中有介绍),这里涉及为每三帧切换播放一张损毁图片,当损毁图片播放完毕后(me_destroy_index 再次等于零),播放飞机损毁音效(me_down_sound.play())。
与此类似,小型敌机的损毁特效代码:
~~~
for each in small_enemies:
if each.active:
# 绘制小型敌机
else:
if e1_destroy_index == 0:
enemy1_down_sound.play()
if not (delay % 3):
screen.blit(each.destroy_images[e1_destroy_index], each.rect)
e1_destroy_index = (e1_destroy_index + 1) % 4
if e1_destroy_index == 0:
each.reset()
~~~
在所有损毁图片绘制完成后(e1_destroy_index再次等于零),调用reset()成员函数来重置敌机位置。
同理,中型敌机损毁特效代码:
~~~
for each in mid_enemies: # 绘制中型敌机并自动移动
if each.active:
# 绘制中型敌机else:
if e2_destroy_index == 0:
enemy2_down_sound.play()
if not (delay % 3):
screen.blit(each.destroy_images[e2_destroy_index], each.rect)
e2_destroy_index = (e2_destroy_index + 1) % 4
if e2_destroy_index == 0:
each.reset()
~~~
大型敌机损毁特效:
~~~
for each in big_enemies: # 绘制大型敌机并自动移动
if each.active: # 如果飞机正常存在
# 绘制大型敌机
else:
big_enemy_flying_sound.stop()
if e3_destroy_index == 0:
enemy3_down_sound.play() # 播放飞机撞毁音效
if not (delay % 3): # 每三帧播放一张损毁图片
screen.blit(each.destroy_images[e3_destroy_index], each.rect)
e3_destroy_index = (e3_destroy_index + 1) % 6 # 大型敌机有六张损毁图片
if e3_destroy_index == 0: # 如果损毁图片播放完毕,则重置飞机属性
each.reset()
~~~
需要补充一点,在之前MyPlane()类中貌似没有写reset()函数,在这里补上吧:
~~~
def reset(self):
self.rect.left, self.rect.top = (self.width - self.rect.width) // 2, (self.height - self.rect.height - 60)
self.active = True
~~~
ok,运行程序,屏幕上出现敌机,控制我方飞机移动并与敌机相撞后,顺利播放损毁特效以及音效,ok,这次博文就先介绍到这里吧。