接下来,我们为我方飞机添加武器——发射子弹。
考虑到Python语言的模块化,我们同样将子弹封装为一个模块,bullet.py。新建py文件,导入Pygame,编程开始。
1、定义子弹类——Bullet1
强调这里之所谓命名为Bullet1,是因为游戏中我方飞机射出的子弹是有两种形式,一种是普通子弹,另外一种是超级子弹。其中超级子弹(Bullet2)将在之后的补给发放机制中进行讲解,这里先给出Bullet1类的代码:
~~~
# ====================定义普通子弹====================
class Bullet1(pygame.sprite.Sprite):
def __init__(self, position):
pygame.sprite.Sprite.__init__(self)
self.image = pygame.image.load("image/bullet1.png")
self.rect = self.image.get_rect()
self.rect.left, self.rect.top = position
self.speed = 12
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
~~~
Bullet1与之前定义的飞机类在结构上很相似,同样都继承至Pygame的精灵类,同样具有active标志位、mask掩膜成员、具有移动函数move()和复位函数reset(),需要注意的有一下几点:1、子弹的速度属性speed要稍微大一点。2、子弹的初始化位置是一个随我方飞机位置变化而变化的量,因此需要在初始化子弹对象时由外部传入(代码中的position,是一个rect类型变量)。3、子弹在屏幕中是自下而上移动的,因此是“-= self.speed”。
2、在主程序中实例化子弹
玩过小蜜蜂游戏的同学都知道,这种打飞机类的游戏子弹的发射速度是要比90坦克的炮弹速度快的,严格的说不是速度快,而是频率高。90坦克中我方坦克一次只发射一枚炮弹,在炮弹达到最大射程或者击中敌方坦克之后才能打出下一发炮弹。打飞机则不然,一次只发射一发子弹显然不能应付众多敌机,需要源源不断的发射子弹,落实到程序中也就是需要实例化多个子弹对象并且循环打印,这与之前敌方飞机的初始化方式很像,都是添加多个精灵对象并循环显示,因此我们采用和之前敌机实例化时相类似的机制,首先在main函数的while循环之前创建子弹精灵的索引,然后向其中添加指定数目的子弹:
~~~
# ====================生成普通子弹====================
bullet1 = []
bullet1_index = 0
bullet1_num = 6 # 定义子弹实例化个数
for i in range(bullet1_num):
bullet1.append(bullet.Bullet1(me.rect.midtop))
~~~
这里通过for循环语句来产生指定数目的子弹对象,并存储于列表结构体中(bullet1),值得注意的一点是,在前面已经提到,在实例化子弹对象时,需要外部传入子弹的初始位置,这里的me.rect.midtop代表的是我方飞机的上方正中间的位置,其实rect结构体还有很多有趣的成员变量来表征其某部分属性,比如说以后我们会用到的rect.center,代表矩形的中心位置,这些知识遇到了再积累吧。
3、显示子弹及音效
子弹初始化完成后需要将子弹显示在屏幕上:
~~~
if not (delay % 10): # 每十帧发射一颗移动的子弹
bullet_sound.play()
bullets = bullet1
bullets[bullet1_index].reset(me.rect.midtop)
bullet1_index = (bullet1_index + 1) % bullet1_num
~~~
这部分代码应该放在while循环之内,这里if not (delay % 10)是设置子弹打印的速度,即每十帧绘制一发子弹,delay参数在之前已经详细介绍过,这里不再赘述。在调用子弹对象的reset()成员函数是,即将该对象的active成员变量设置为true,说明该子弹对象已经处于激活状态了。程序编写到这里,如果此时运行程序的话,只能听到发射子弹的声音,并不能看到实际发射的子弹,原因是没有对子弹进行绘制(blit函数)。但这里并不能简单的把子弹绘制到屏幕上,因为我们还要为子弹赋予它的本质功能,击毁敌机,也就是和敌机的碰撞检测。
4、子弹与敌机的碰撞检测
在实例化完子弹之后,我们要做的第一件事并不是将子弹显示出来,二是要先检测该发子弹是否击中了敌机,如果击中,就不再显示这发子弹了。因此这里正常的程序设计思路应该是:每一发子弹 -> 该发子弹是否已经激活 -> 如果激活,是否击中敌机 -> 如果没击中,正常绘制子弹图像 -> 如果击中,则子弹损毁,同时敌机损毁,代码如下:
~~~
for b in bullets:
if b.active: # 只有激活的子弹才可能击中敌机
b.move()
screen.blit(b.image, b.rect)
enemies_hit = pygame.sprite.spritecollide(b, enemies, False, pygame.sprite.collide_mask)
if enemies_hit: # 如果子弹击中飞机
b.active = False # 子弹损毁
for e in enemies_hit:
e.active = False # 小型敌机损毁
~~~
这里在碰撞检测是,考虑到子弹对象和敌机对象都已经在内部定义了mask(掩膜)成员变量,因此直接调用基于掩膜的的碰撞检测函数即可(详见上篇博文),再次说明碰撞检测函数的返回值(enemise_hit)是一个存放精灵的精灵组结构,通过for()循环遍历其中的元素及确定那个精灵检测到了碰撞。
ok,程序编写到这里,按道理来说应该能够正常运行,但在这里很可能会出现一个类似于“local variable 'bullets' referenced before assignment”的错误,在这里简单分析一下。错误的意思是bullets变量没有定义,出现这个错误的原因在于我们过早的将delay延时参数进行了减一操作。假如我们将“delay -= 1”这句话放在了while循环的开始位置,delay的初始值为60,进入while循环的一瞬间它就会减为59,这样“if not (delay % 10)”这个条件就不会成立,也就不会执行“bullets = bullet1”,当然也不会播放子弹音效,因此在程序执行到“for b in bullets:”时,由于之前的“bullets = bullet1”操作没有顺利执行,自然bullets参数属于未定义的状态。解决方案也很简单,就是把delay参数的控制代码:
~~~
if delay == 0:
delay = 60
delay -= 1
~~~
移至while循环的末尾,这样程序在进入循环是delay=60,所有代码顺利执行。
截止到这里,程序顺利运行,我方飞机射出的子弹将敌机一击毙命。当然这里也有不合理的地方,小型敌机可以一击毙命,但中型敌机和大型敌机皮糙肉厚,应该能多挨几发子弹才对,这在之后的博文中会给中型敌机和大型敌机赋予一定血量,并以血槽的形式显示出来。OK,这篇博文就到这里,大家工作顺利。