Pygame 外星人入侵(5)发射子弹
目录
引言
在之前的博文中,我们实现了游戏屏幕的绘制、飞船的绘制以及玩家通过按键来操控飞船移动的功能。
在这篇博文中,将会完成让玩家飞船发射子弹的功能。
一、定义子弹类
我们既然要 “发射”“子弹”,那么首先就必须要有可发射的“子弹”。
所以我们需要自己定义一个子弹类,用于生成子弹对象,以及对子弹动作的维护。
大概需要以下几个步骤:
1、在设置中新增子弹相关的设置参数
2、新增 bullet 模块,维护子弹类
3、编写子弹类
1、增加子弹相关的设置参数
由于我们计划通过 Pygame 绘制的矩形来代表一颗子弹,因此需要在设置类中增加子弹矩形的长、宽、颜色参数,以及子弹飞行的速度参数。
settings.py
class Settings():
...
# 5、和子弹有关的设置
# 5.1 子弹的宽度
self.bullet_width = 3
# 5.2 子弹的长度
self.bullet_height = 15
# 5.3 子弹的速度 略微慢于飞机移动的速度
self.bullet_speed = 1
# 5.4 子弹的颜色
self.bullet_color = (30, 30, 30)
至此,我们计划飞船发射的子弹是一个宽3像素、长15像素的黑色矩形,且它在被发射后会以 每次 1 像素的速度在屏幕上飞行。
2、新增子弹模块
由于子弹本身需要有很多方法,因此单独开设一个模块来保存子弹类,所有的子弹代码都被保存在这个模块中。
同时,因为我们打算使用 Pygame 中 sprite 模块中的 Sprite 类 来管理子弹,所有必须提前做相应的导入。
bullet.py
from pygame.sprite import Sprite
# 子弹类继承自精灵类
class Bullet(Sprite):
...
3、定义子弹类
我们的子弹类计划继承自精灵类,原本我不太能理解为什么要用精灵类,甚至不知道精灵类是做什么的。
我们在之前制作的元素有:
1、游戏屏幕——Surafece
2、背景——Color
3、飞船——Surface——image
但是现在又多出一个精灵类,何必多此一举?为什么不找一张子弹的图片,然后像处理飞船一样来处理子弹呢?
其实是这样的,因为我们管理的飞船和子弹其实是有区别的,那就是飞船只有一架,而子弹会同时在屏幕上出现好多颗。
当我们把“子弹”用 Surface 来实现时,在批量处理子弹时,会遇到困难。
而精灵类就是为了方便我们批量管理游戏元素而存在的。除了精灵类本身外,Pygame 中的 sprite 模块中还有一个 Group 类,是一个精灵的容器,我们可以通过 Group 对象来批量管理一堆精灵(一堆子弹)。
现在来定义子弹类,子弹类包括如下几个方法:
1、初始化子弹,初始化的位置在飞船的顶部(用以模拟飞船发射的效果)
2、子弹运动的方法
3、绘制子弹到屏幕上的方法
# 初始化子弹,即生成子弹,需要在飞船的顶部中间位置初始化出一枚子弹
def __init__(self, screen, ship, settings):
super().__init__()
self.screen = screen
# 先在(0,0)处初始化一个子弹对象,之后再将它移到正确的位置上
self.rect = pygame.Rect(0, 0, settings.bullet_width, settings.bullet_height)
# 然后将初始化的子弹位置移动到飞船的中间顶部
self.rect.centerx = ship.rect.centerx
self.rect.top = ship.rect.top
# 需要将子弹的纵坐标保存为小数类型,这样可以接受小数速度的移动
self.y = float(self.rect.centery)
# 设置子弹的速度和颜色
self.speed = settings.bullet_speed
self.color = settings.bullet_color
# 更新子弹的位置,延垂直方向递减
def update(self, settings):
# 保证可以接受小数值的速度
self.y -= self.speed
self.rect.centery = self.y
这里子弹的坐标同样通过一个浮点型临时变量来保存,方便接收浮点数的速度值。
# 在屏幕上绘制子弹
def draw_bullet(self):
pygame.draw.rect(self.screen, self.color, self.rect)
至此,就有了可以发射的“子弹”了。
二、发射子弹
有了“子弹”,现在要实现如何“发射”。
1、实现发射逻辑
在玩家按下空格键时,会从飞船顶部中间位置发射出一枚子弹。因此,需要做这样几个步骤:
1、响应玩家按下空格键的事件
2、响应后生成子弹的业务
3、通过 Group 对象来保存生成的全部子弹
def check_keydown_events(event, ship, screen, settings, bullets):
...
# 响应玩家按下空格键的事件:
elif event.key == K_SPACE:
# 生成一颗新子弹
new_bullet = Bullet(screen, ship, bullets)
# 将这颗新子弹加入子弹队列中
bullets.add(new_bullet)
为了管理全部子弹对象,我们需要提前在主模块中创建一个子弹队列,通过 Group 类。
同时,要修改主游戏循环中的逻辑,在更新飞船后,更新子弹队列中所有子弹的位置,然后再绘制子弹。
from pygame.sprite import Group
...
# 创建一个空的子弹队列
bulltes = Group()
...
while True:
...
# 更新所有子弹的位置
bullets.update()
# 最后绘制所有元素到屏幕上
update_screen(my_screen, my_settings, my_ship, bullets)
这样处理,我们就还需要修改 update_screen 方法的代码:
def update_screen(screen, setting, ship, bullets):
# 为纯黑的游戏屏幕填充上不一样的颜色
screen.fill(setting.background_color)
# 在背景之上绘制我们的飞船,注意这里的逻辑,必须是飞船在背景之后绘制,确保飞船在背景的上层
ship.blitme()
# 在屏幕和飞船之上,绘制子弹
for bullet in bullets.sprites():
bullet.draw_bullet()
# 刷新屏幕,使得元素能够不断刷新位置
pygame.display.flip()
2、优化1:删除出界的子弹
现在运行游戏已经可以自由地发射子弹了,但是随着我们发射越来越多的子弹,游戏会变得越来越卡顿,这是为什么呢?
因为子弹一直向上飞行,直到飞出屏幕外后,它仍然在飞行,只是我们看不到罢了,子弹队列中的子弹数量也是一直在增加。
这样显然不利于游戏的运行,那么我们就要考虑将出界的子弹都删除。
我们可以在主游戏循环中更新子弹后,判断子弹的位置是不是超出游戏屏幕了,如果超出了游戏屏幕,则将这颗子弹从队列中删除。
# 更新子弹队列中的所有子弹位置
bullets.update(settings)
# 如果子弹超出了屏幕范围,那么就删除这枚子弹
for bullet in bullets.copy(): # 注意这里我们判断的是副本中的子弹,但是删除的是本体的子弹
if bullet.rect.bottom <= 0:
bullets.remove(bullet)
这里注意的是,我们执行判断的是队列的副本,而执行删除操作的是队列的本体。
现在当我们生成再多子弹,也不会觉得卡顿了,因为出界的子弹都会被删除。
3、优化2:限制子弹数量
很多游戏都会有限制玩家可以发射的子弹数量,旨在让玩家有目的的射击,而不是无脑发射子弹。现在我们尝试做一下这个功能。
1、在设置类中新增最大子弹数参数
2、修改响应玩家按下空格键后的业务逻辑
3、生成新子弹前,先判断当前子弹队列中的子弹数是否超过限制
设置类
# 5.5 最大子弹数
self.max_bullets = 3
check_keydown_events
if len(bullets) < settings.max_bullets:
# 只有当前子弹数小于限制数时,才会发射新子弹出来
new_bullet = Bullet(screen, ship, settings)
bullets.add(new_bullet)
这样就可以实现,当我们屏幕上已有 3 颗子弹时,按空格键也不会发射新子弹的效果。
三、代码封装
在优化1 和 优化2 两个部分,我们对更新子弹和发射子弹两部分的代码进行了扩充,这样显得有些冗长,本着这本书一贯的作风,我们需要对这两部分的代码进行封装
1、封装 更新子弹、删除出界子弹 的代码
2、封装 玩家按下空格键后的业务代码
def update_screen(screen, setting, ship, bullets):
# 为纯黑的游戏屏幕填充上不一样的颜色
screen.fill(setting.background_color)
# 在背景之上绘制我们的飞船,注意这里的逻辑,必须是飞船在背景之后绘制,确保飞船在背景的上层
ship.blitme()
# 在屏幕和飞船之上,绘制子弹
for bullet in bullets.sprites():
bullet.draw_bullet()
# 刷新屏幕,使得元素能够不断刷新位置
pygame.display.flip()
# 发射子弹的方法
def fire_bullet(bullets, settings, screen, ship):
# 发射前检查当前子弹数是否超过最大限制的数量
if len(bullets) < settings.max_bullets:
# 只有当前子弹数小于限制数时,才会发射新子弹出来
new_bullet = Bullet(screen, ship, settings)
bullets.add(new_bullet)
如此一来,我们原本相应位置的代码就可以得到有效减少。
四、小结
在这篇博文的小结中,先说一下 Bullet 类 update 方法的理解。
我们在子弹类中编写了一个修改子弹位置的方法 update,之所以要用这个函数名,是因为 Pygame 的精灵类本身就有一个方法叫做 update ,只是这个方法是一个空函数,里面什么功能都没有,那么这个函数到底有什么意义呢?
其实是因为,当我们把一系列精灵对象放入一个 Group 对象的队列中后,可以直接对这个队列对象调用 update 方法,这样一来,会自动执行队列中所有精灵对象的 update 方法。实现了方便精灵类批量管理的功能。而方便批量管理,也是我们在开头就叙述过的,这里只是阐述一下具体是如何实现的。
所以我们在更新子弹位置时,写的是这样的代码:
bullets.update()
这样,队列中的每一个子弹,都会执行 update 方法。
精灵类中原生的 update 虽然没有任何功能,但是却可以实现个体和群体之间的联系,因此我们改写 update 函数体,以实现批量移动子弹。
这种做法有点类似于我们继承某个类时,改写构造函数的做法。
至此,我们实现了飞船发射子弹的功能,接下来,就要尝试生成入侵的外星人,以及如何通过子弹来击杀外星人了。