Python编写微信打飞机小游戏(十二)
最后更新于:2022-04-01 14:23:16
在这篇博客里,我们为游戏添加最后一个功能:GameOver画面的绘制。
在当我方飞机的三条命全部挂掉之后,程序会进入结束程序,我们的设计时当程序结束时显示用户的目前得分以及历史最高分,仅此而已。
1、加载图片资源
在这里我们特意重做了一个游戏结束的画面,其实就是在背景图片上加了一些文字而已,首先在main()函数中加载它:
~~~
gameover_image = pygame.image.load("image/game_over.png") # 游戏结束背景图片
gameover_rect = gameover_image.get_rect()
~~~
2、保存并显示历史最高分
程序要求能够保存用户所得到的历史最高分,这就要求程序在每一次结束前,都需要将用户的得分保存在磁盘上。首先我们在程序目录下新建一个文本文件“score_record.txt”来保存用户的得分,注意在新建完文本文件时我们需要在文件中输入一个数字“0”并保存,代表当前用户得分为零分,否则的话当程序第一次运行读取这个文件的话将会因为文件内容为空而读取失败,当然我们完全可以在程序中添加判断语句来避免这种情况,这里我们就采用一个投机取巧的方法,在创建文本文件时直接先给它一个零值。
接下来当我方小飞机生命用尽时,程序会进入到“elif life_num == 0:”的循环分支中,之前这里只是用一个“pass”语句一带而过,这里开始扩充这部分代码。首先,绘制结束画面并关闭所有的音效以及定时器:
~~~
screen.blit(gameover_image, gameover_rect)
pygame.mixer.music.stop() # 关闭背景音乐
pygame.mixer.stop() # 关闭所有音效
pygame.time.set_timer(supply_timer, 0) # 关闭补给机制
~~~
接下来的工作就是读入“score_record.txt”文件中保留的用户历史最高分数,并和当前的用户得分进行比较,如果当前用户得分高于历史最高分,则将文本文件中保留的历史最高分替换掉:
~~~
with open("score_record.txt", "r") as f:
record_score = int(f.read())
if score > record_score: # 如果玩家得分大于历史最高分,则将当前分数存档
with open("score_record.txt", "w") as f:
f.write(str(score))
~~~
这里涉及到最简单的Python读取文件的操作,不懂得地方大家可以从网上查阅相关资料,这里就不再赘述。替换完成后,将历史最高分和当前最高分显示在结束画面上,字体仍借用之前的分数字体“score_font”对象即可:
~~~
record_score_text = score_font.render("%d" % record_score, True, color_white)
screen.blit(record_score_text, (150, 25))
game_over_score_text = score_font.render("%d" % score, True, color_white)
screen.blit(game_over_score_text, (180, 370))
~~~
这里两个字体的摆放位置时根据背景图片上的文字而定的,运行程序,一切正常。
3、最后一个BUG
其实程序到这里已经完全能够运行了,但这里仍然有一个不太完美的地方:就是当GameOver后,程序会不停的循环执行life_num = 0部分的代码,这就导致程序会不停的打开和关闭txt文件,这完全是没有必要的,只需在程序结束时check一次txt文件中的内容就够了,为此我们在主函数中设置一个标志位用来表示当前文件的打开状态:
~~~
flag_recorded = False # 是否已经打开记录文件标志位
~~~
然后在打开文件后,将其置为true,并且在打开文件前对其进行判断:
~~~
elif life_num == 0: # 生命值为零,绘制游戏结束画面
# 关闭补给机制及音效
if not flag_recorded: # 读取历史最高分
flag_recorded = True
# 如果玩家得分大于历史最高分,则将当前分数存档
~~~
ok,运行程序,一切正常,我们的打飞机游戏至此已经全部编写完成,由于博客所讲的代码比较零散,稍后我会上传一份完整的、添加详细注释的代码,这是我的第一个Python程序,欢迎大家指正,谢谢。
[完整代码](http://download.csdn.net/detail/u013088062/9280087)
Python编写微信打飞机小游戏(十一)
最后更新于:2022-04-01 14:23:14
在这篇博文中,我们准备为打飞机小游戏添加一个暂停的功能,即用户在游戏过程中随时可以通过单击屏幕右上方的一个暂停按钮来暂停和恢复游戏。这个功能看似比较简单,但其中涉及了鼠标操作、图片切换、代码结构的重置等等,接下来我们一一进行介绍。
1、加载暂停按钮图标
在image文件夹下一共有四张暂停按钮的图片,分别为深色和浅色两组,首先在main()函数中加载相关图片资源并初始化暂停/开始标志位:
~~~
paused = False # 标志是否暂停游戏
pause_nor_image = pygame.image.load("image/game_pause_nor.png") # 加载暂停相关按钮
pause_pressed_image = pygame.image.load("image/game_pause_pressed.png")
resume_nor_image = pygame.image.load("image/game_resume_nor.png")
resume_pressed_image = pygame.image.load("image/game_resume_pressed.png")
paused_rect = pause_nor_image.get_rect()
paused_rect.left, paused_rect.top = width - paused_rect.width - 10, 10 # 设置暂停按钮位置
paused_image = pause_nor_image # 设置默认显示的暂停按钮
~~~
代码看起来有点繁琐,其实每句代码的功能都十分简单。先设置一个暂停/开始标志位“paused”,为true表示暂停状态,为false表示非暂停状态。然后加载四张图片,注意这里四张图片都是由特定含义的,“pause_nor_image”代表未选中状态下的暂停按钮, “pause_pressed_image”代表选中状态下的暂停按钮,“resume_nor_image”代表未选中状态下的开始按钮,“resume_pressed_image”代表选中状态下的开始按钮,鼠标移动到按钮区域按钮就变为选中状态。然后创建一个“paused_image”变量用以保存当前需要显示的按钮图片,游戏开始时默认为“pause_nor_image”。注意这里需要事先得到并设置好按钮图片的区域位置,在接下来的程序中会用到。
2、定义“选中/未选中”状态切换
之前已经说过,选中和未选中状态是由鼠标是否移动到按钮区域来决定的,因此需要定义鼠标移动的事件响应函数,先上代码:
~~~
# ====================检测用户的退出及暂停操作====================
for event in pygame.event.get(): # 响应用户的偶然操作
if event.type == QUIT:
# 如果用户按下屏幕上的关闭按钮,触发QUIT事件,程序退出elif event.type == MOUSEMOTION:
if paused_rect.collidepoint(event.pos): # 如果鼠标悬停在按钮区域
if paused: # r如果当前的状态是暂停
paused_image = resume_pressed_image
else:
paused_image = pause_pressed_image
else:
if paused:
paused_image = resume_nor_image
else:
paused_image = pause_nor_image
~~~
对于鼠标移动事件,每当鼠标指针的位置坐标发生变化时,都会触发一次“MOUSEMOTION”事件,在系统接收到鼠标移动事件后,首先要判断当前鼠标指针是否位于按钮的矩形区域,这个功能可以通过rect类的的collidepoint()成员函数完成,它能够实现判断一个坐标点(例如当前的event.pos)是否在调用者的(一个矩形对象)的区域范围之内。如果鼠标处于按钮区域,则将当前显示的图片指定为“选中状态下的开始/暂停”(resume_pressed_image和pause_pressed_image),否则设定为“非选中状态下的开始/暂停”(resume_nor_image和pause_nor_image),这些小逻辑还是挺磨人的,需要仔细的推敲一把才能理清。
3、定义“暂停/开始”状态切换
程序的暂停/开始时通过鼠标单击的动作来决定的,因此需要编写鼠标单击事件的响应函数,在函数中修改“paused”标志位的值来控制暂停/开始状态的切换:
~~~
for event in pygame.event.get(): # 响应用户的偶然操作
if event.type == QUIT:
# 如果用户按下屏幕上的关闭按钮,触发QUIT事件,程序退出elif event.type == MOUSEBUTTONDOWN:
button_down_sound.play()
if event.button == 1 and paused_rect.collidepoint(event.pos): # 如果检测到用户在指定按钮区域按下鼠标左键
paused = not paused
if paused: # r如果当前的状态是暂停
paused_image = resume_pressed_image
pygame.time.set_timer(supply_timer, 0) # 关闭补给机制以及所有音效
pygame.mixer.music.pause()
pygame.mixer.pause()
else:
paused_image = pause_pressed_image
pygame.time.set_timer(supply_timer, 30 * 1000) # 开启补给机制以及所有音效
pygame.mixer.music.unpause()
pygame.mixer.unpause()
~~~
简单分析一下这段代码:当鼠标按下时(无论是左键、右键还是中间滚轮)会触发“MOUSEBUTTONDOWN”事件。程序检测到鼠标按下的消息事件后,先播放一声鼠标单击的音效,然后检测按键顺序以及鼠标位置。如果检测到当前为左键单击(event.button == 1)并且鼠标指针位于按钮区域之内(paused_rect.collidepoint(event.pos)返回true),则认为用户发出了有效的暂停指令,此时需要将paused标志位取反。紧接着就需要判断当前状态是暂停还是非暂停,如果单击鼠标之后变为暂停状态(paused为真),则需要关闭补给机制、关闭背景音效、关闭混音器,将按钮图片设置为resume_pressed_image(待开始),让程序静静的等待即可;相反若鼠标单击之后为开始状态,则需要重新开启这些机制,同时调整按钮图片为pause_pressed_image(待暂停)。
4、将paused标志位加入到控制语句
在完成了准备阶段的工作之后,我们开始让paused这个控制标志位真正参与到程序的控制结构中。由于之前在添加“life_num”控制变量的过程中已经对程序结构进行了统一调整,这里的工作就变得相对简单,只需在“life_num”的判断语句之后加上对paused标志位的判断即可:
~~~
if life_num and (not paused): # 如果游戏未被暂停,正常运行
~~~
对了,差点忘了在while循环内部绘制暂停按钮图标,不过这里绘制操作应该放在“if life_num and (not paused):”判断语句的控制范围之外,以便使得无论程序是否暂停,用户都有图标可以点击:
~~~
screen.blit(paused_image, paused_rect)
~~~
OK,程序到这里应该能够顺利运行了,在下一篇博客中我们将为游戏添加最后一个功能:GameOver界面的设置。
Python编写微信打飞机小游戏(十)
最后更新于:2022-04-01 14:23:11
在这篇博文里,我们将为我方飞机添加三条生命。
1、加载相关资源
在目前的游戏中,我方飞机是拥有不死生命的,损毁了就在下方复活,这显然不合逻辑,因此需要为我放飞机增加生命数,并在屏幕右下方显示对应图标。首先在main()函数中设置初始化生命数并加载小飞机图片:
~~~
life_image = pygame.image.load("image/life.png").convert()
life_rect = life_image.get_rect()
life_num = 3 # 一共有三条命
~~~
2、将“life_num”加入程序循环判断语句中
既然我方飞机拥有了生命属性,那就意味着我们不能够再向之前那样随意的摆放while()循环中的代码了,很多代码在执行前都需要判断life_num是否为零,比如说发射子弹,我方飞机都已经挂掉了,再发射子弹就显得不尽合理了吧。
因此我们需要考虑哪些代码在执行前需要判断life_num是否为零,具体来说,只要涉及到和精灵有关的操作都应该在life_num > 0时进行,因此我们将下列操作放在life_num的判断条件之外:(1)打印背景图片和分数的操作。因为不能因为我方飞机挂掉就让屏幕一篇漆黑,背景还是应该有的;(2)事件响应机制。因为我方飞机挂掉之后同样需要响应用户事件,比如鼠标单击事件、系统退出事件。除了以上这两个操作之外,其余操作都应该放在“if life_num:”的条件循环之内,因此可能需要我们将之前的代码顺序做一些调整:
~~~
while running:
# 将背景图片打印到内存的屏幕上# 检测用户的退出及暂停操作
~~~
~~~
if life_num
# 绘制全屏炸弹数量和剩余生命数量# 检测用户的键盘操作# 绘制补给并检测玩家是否获得# 子弹与敌机的碰撞检测
# 我方飞机碰撞检测
# 绘制我方飞机# 绘制敌方飞机
# …………………………
~~~
~~~
elif life_num == 0: # 生命值为零,绘制游戏结束画面
pass
# 将内存中绘制好的屏幕刷新到设备屏幕上
# 设置帧数为60
~~~
3、life_num递减操作
重新安排完代码结构后,接下来只需在我方飞机损毁时,将life_num减一:
~~~
if me.active:# 绘制我方飞机的两种不同的形式else:
if not (delay % 3):
# 绘制我方飞机损毁画面
if me_destroy_index == 0:
life_num -= 1# 我方飞机重生
~~~
当然这里也用了“me_destroy_index == 0”的限定条件,以保证在损毁期间只减一次生命数(详见之前博文)。
4、绘制life_num图标
在游戏过程中,为了显示当前玩家还有多少生命数,需要通过在屏幕右下方显示小飞机图标来指示,玩家当前有几条命就显示几个图标。小图标的资源已将在开始时加载好了,我们先给出完整的显示代码,稍后解释:
~~~
for i in range(life_num):
screen.blit(life_image, (width - 10 - (i + 1) * life_rect.width, height - 10 - life_rect.height))
~~~
这里只是做了一些简单的数学运算,“(width - 10 - (i + 1) * life_rect.width, height - 10 - life_rect.height)”这段代码就能够实现根据当前的life_num的值画出对应数量的小飞机图标。
5、为我方飞机添加初始化无敌机制
程序编写到这里貌似已经实现了预期目标,不过其实这里面有一个影藏的BUG,需要多次实现才会出现。因为我方飞机在重生时都是在固定位置出现的,那么就有这样一种可能性:就是在我方飞机重生时,在屏幕下方正中间(我方飞机重生的位置)正好有一架敌机,这就导致我方飞机一出生就挂掉,根本没有躲避的事件,虽然这种BUG出现的几率不大,但我们还是在这里提供一个解决方案,就是让我方飞机在重生之后具有三秒的无敌时间。因此我方飞机又多了一个属性:无敌。
首先向我方飞机类的内部添加用来表示当前无敌状态的标志位:
~~~
self.invincible = False # 飞机初始化时有三秒的无敌时间
~~~
对应的在reset()成员函数中将其置为true:
~~~
self.invincible = True
~~~
接下来我们通过事件触发系统来设计一个定时器,用以记录无敌时间,首先在main函数中定义这个计时器:
~~~
invincible_time = USEREVENT + 2 # 接触我方飞机无敌时间定时器
~~~
由于之前已经定义了两个用户事件了(supply_timer,double_bullet_timer),因此这里指定的标号为USEREVENT + 2。
然后在我方飞机reset之后(invincible置为true,无敌状态开始),激活这个定时器,计时开始(3秒):
~~~
if me.active:# 绘制我方飞机的两种不同的形式else:
if not (delay % 3):
# 绘制我方飞机损毁画面
if me_destroy_index == 0:
me.reset() # 我方飞机重生并开始无敌时间计时
pygame.time.set_timer(invincible_time, 3 * 1000)
~~~
激活定时器之后,编写事件响应函数。若该事件被触发,说明三秒的无敌时间已过,需要将我方飞机的无敌标志位置为false,并关闭计时器:
~~~
for event in pygame.event.get(): # 响应用户的偶然操作
if event.type == QUIT:
# 如果用户按下屏幕上的关闭按钮,触发QUIT事件,程序退出elif event.type == invincible_time: # 如果无敌时间已过
me.invincible = False
pygame.time.set_timer(invincible_time, 0)
~~~
最后,我们修改一下我方飞机的碰撞检测函数,使得当我方飞机与敌机发生碰撞时,只有在我方飞机的invincible变量为false(非无敌状态)的情况下,才执行我方飞机和敌机的销毁操作:
~~~
# ====================我方飞机碰撞检测====================
enemies_down = pygame.sprite.spritecollide(me, enemies, False, pygame.sprite.collide_mask)
if enemies_down and not me.invincible: # 如果碰撞检测返回的列表非空,则说明已发生碰撞,若此时我方飞机处于无敌状态
me.active = False
for e in enemies_down:
e.active = False # 敌机损毁
~~~
这里就是在原有的if语句调价的基础上添加“not me.invincible”判断,且二者为“与”操作的关系。好的,程序到此就告一段落,顺利的实现了预期功能,在下一篇博文中我们在给游戏添加一个暂停的功能。
Python编写微信打飞机小游戏(九)
最后更新于:2022-04-01 14:23:09
在之前的博文中我们为游戏添加了随分数累加的难度递增机制,这就带来一个问题:当到达后面的难度等级后,敌方飞机铺天盖地而来,我方小飞机根本应付不过来,因此在这篇博文中我们为我方飞机赋予必杀技——随机补给全屏炸弹和超级子弹。
首先来简单描述这两个必杀技,全屏炸弹是指在游戏过程中,当用户按下空格键时,就触发一枚全屏炸弹(如果当前有的话),此时屏幕上的所有敌机立即销毁。超级子弹是指玩家在接收到指定补给包之后,我方飞机能够一次发射两发子弹,攻击力加倍。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)
~~~
注意这里将时间触发时间设置为零表示关闭了这个事件,等待下一次获得超级子弹之后再重新开启。今天的博文就到这里,内容有点多,大家慢慢看吧。
Python编写微信打飞机小游戏(八)
最后更新于:2022-04-01 14:23:07
现在飞机已经能够带着血条突突突飞下来让玩家虐,于是我们给用户一点打飞机的成就感——计分系统以及难度递增机制。
1、计分系统
首先,我们在main()函数中添加全局变量(score)并初始化为零用以统计当前用户的打飞机得分:
~~~
score = 0 # 统计用户得分
~~~
接下来只需要在敌机销毁时对score进行累加就可以了,我们在这里先给敌机标个价:小型敌机一架值500分,中型敌机一架值2000分,大型敌机一架值6000分,然后在敌机损毁时根据敌机类型选择加多少分,对于小型敌机:
~~~
for each in small_enemies: # 绘制小型敌机并自动移动
if each.active:
# 绘制小型敌机
else:
if e1_destroy_index == 0:
# 敌机损毁音效
if not (delay % 3):
# 播放损毁画面
if e1_destroy_index == 0:
score += 500
# reset()
~~~
中型敌机:
~~~
for each in mid_enemies: # 绘制中型敌机并自动移动
if each.active:
# 绘制中型敌机# 绘制血槽
else:
if e2_destroy_index == 0:
# 播放损毁音效
if not (delay % 3):
# 绘制损毁画面
if e2_destroy_index == 0:
score += 2000
# reset()
~~~
大型敌机:
~~~
for each in big_enemies: # 绘制大型敌机并自动移动
if each.active: # 如果飞机正常存在
# 绘制大型敌机# 绘制血槽
if each.rect.bottom == -50:
# 播放大型飞机的音效(循环播放)
else: # 如果飞机已撞毁
# 出场音效停止
if e3_destroy_index == 0:
# 播放飞机撞毁音效
if not (delay % 3): # 每三帧播放一张损毁图片# 大型敌机有六张损毁图片
if e3_destroy_index == 0: # 如果损毁图片播放完毕,则重置飞机属性
score += 6000
# reset()
~~~
需要指出一点,这里之所以添加“if e3_destroy_index == 0:”是因为整个飞机损毁的过程是由四帧(或六帧)图像的播放来完成的,如果不加这个限制,则在飞机损毁过程中每播放一帧就加一次分,这样小型机和中型机损毁一次就要加4次分,大型机损毁一次就要加6次分,因此需要规定每次飞机损毁时只在播放损毁最后一帧画面之后再进行加分、复位,播放声音等操作。
2、显示分数
在游戏过程中,需要将玩家得分实时显示在界面的左上角,这就涉及到Pygame的font模块的应用。首先在main函数初始化过程(while循环之前)调用font模块成员函数创造系统字体对象:
~~~
score_font = pygame.font.SysFont("arial", 48) # 定义分数字体
~~~
这里的score_font对象采用系统字体“arial”,字号为48。之后通过调用字体对象的render()成员函数将当前分数存于字体对象中,并返回字体的surface格式对象,用以接下来的屏幕打印工作:
~~~
score_text = score_font.render("Score : %s" % str(score), True, color_white)
screen.blit(score_text, (10, 5))
~~~
这里先将当前分数通过str()函数转换成字符串,然后格式化到“Score:”的字符串中,第二个参数表示当前字体在打印时自动开启抗锯齿功能,第三个参数为字体颜色。建议将这两句代码放在while循坏体内比较靠前的位置,以便使代码更为整洁。运行程序,分数能够正确的、实时的显示在屏幕上,接下来我们来给游戏增加难度——难度分级递增机制。
3、设计难度分级标准
我们的目的是随着用户得分的提高,游戏对应难度也逐渐加大,首先需要在main()函数添加一个全局标志位level用以指示当前游戏处于那种难度:
~~~
level = 1 # 游戏难度级别
~~~
接下来简单的制定以下难度门槛:分数处于(0,5000)为第一级难度,处于(5000,30000)为第二级难度,处于(30000,60000)为第三级难度:
~~~
# ====================定义难度递进操作====================
if level == 1 and score > 5000: # 如果达到第二难度等级,则增加3架小型敌机,2架中型敌机,1架大型敌机,并提升小型敌机速度
level = 2
pass
elif level == 2 and score > 30000: # 如果达到第三难度等级
level = 3
pass
elif level == 3 and score > 60000: # 如果达到第四难度等级
level = 4
pass
~~~
接下来开始根据对应难度等级来给游戏加难度,如何才算增加难度,无非是增加敌机数量,增加敌机速度,因此我们定义两个函数来完成这个功能,首先定义增加飞机数量的函数,刚好我们之前已经定义好了这个函数:
~~~
# ====================敌方飞机生成控制函数====================
def add_small_enemies(group1, group2, num):
def add_mid_enemies(group1, group2, num):
def add_big_enemies(group1, group2, num):
~~~
关于这些函数的具体作用参见之前博文,接下来定义速度增加函数,原理就是增加敌机对象内部speed成员变量的值即可:
~~~
# ====================提升敌机速度====================
def inc_speed(target, inc):
for each in target:
each.speed += inc
~~~
target为待加速的敌机精灵组,inc为加速的程度(大致可以成为加速度吧)。
4、完善难度分级递增机制
定义完“add_small_enemies”、“inc_speed”等操作函数后,我们给出完整的难度分级递增机制部分代码(建议放在while循环的开始部分):
~~~
# ====================定义难度递进操作====================
if level == 1 and score > 5000: # 如果达到第二难度等级,则增加3架小型敌机,2架中型敌机,1架大型敌机,并提升小型敌机速度
level = 2
level_up_sound.play()
add_small_enemies(small_enemies, enemies, 3)
add_mid_enemies(mid_enemies, enemies, 2)
add_big_enemies(big_enemies, enemies, 1)
inc_speed(small_enemies, 1)
elif level == 2 and score > 30000: # 如果达到第三难度等级
level = 3
level_up_sound.play()
add_small_enemies(small_enemies, enemies, 3)
add_mid_enemies(mid_enemies, enemies, 2)
add_big_enemies(big_enemies, enemies, 1)
inc_speed(small_enemies, 1)
inc_speed(mid_enemies, 1)
elif level == 3 and score > 60000: # 如果达到第四难度等级
level = 4
level_up_sound.play()
add_small_enemies(small_enemies, enemies, 3)
add_mid_enemies(mid_enemies, enemies, 2)
add_big_enemies(big_enemies, enemies, 1)
inc_speed(small_enemies, 1)
inc_speed(mid_enemies, 1)
inc_speed(big_enemies, 1)
~~~
这段代码的分级机制很简单,每增加一级难度,就在之前的基础上添加三架小型敌机,两架中型敌机和一架大型敌机,同时将各个机型的速度均加一,在难度提升的瞬间还有特殊音效播放。ok程序到这里应该可以正确运行,试玩一下发现,当分数超过30000分时敌机已经铺天盖地的飞了下来,为了玩家的生存,我们将在下一篇博文中为我方飞机添加超级武器:超级子弹和全屏炸弹。
~~~
~~~
~~~
~~~
Python编写微信打飞机小游戏(七)
最后更新于:2022-04-01 14:23:04
这边博客我们为大型敌机和中型敌机设置血量并以血槽的形式显示出来,同时解决之前遇到的声道阻塞的BUG(声道阻塞的问题详见之前的博文)。
1、为敌机添加表示血量的成员变量
所谓敌机血量,就是指敌机在挂掉之前能够挨几发子弹,这是敌机对象的一个成员属性,因此我们在中型敌机(MidEnemy)和大型敌机(BigEnemy)中添加energy成员变量:
~~~
class MidEnemy(pygame.sprite.Sprite):
energy = 5
def __init__(self, bg_size):
………………
self.energy = MidEnemy.energy
~~~
~~~
class BigEnemy(pygame.sprite.Sprite):
energy = 15
def __init__(self, bg_size):
………………
self.energy = BigEnemy.energy
~~~
注意这里之所以将energy初始化为类的全局变量以及类对象的成员变量两种形式,是因为在接下来绘制血槽的过程中,需要计算当前血量和总血量的比值,全局energy用以保存总血量(定值),类对象的成员变量energy(随着被击中的次数而递减)表示当前血量,这一点在后面绘制血槽的过程中将再次解释。从代码中可以看出,我们将中型敌机血量设计为承受5发子弹,大型敌机的血量设计为承受15发子弹的数量(皮糙肉厚)。
当然在对应的reset()函数中需要重置energy变量的值,将其重新设置为满血:
~~~
self.energy = MidEnemy.energy
~~~
以及
~~~
self.energy = BigEnemy.energy
~~~
2、为中型敌机和大型敌机添加中弹受损图片
既然中型敌机和大型敌机并不是一击毙命,因此有必要在其中弹时配以特效图片(虽然是5毛特效)以表征该敌机血量正在减少,相关图片资源已经存于image文件夹中,只需在中型敌机和大型敌机的类内部(__init__()函数中)进行加载:
~~~
self.image_hit = pygame.image.load("image/enemy2_hit.png") # 加载中型敌机中弹图片
~~~
~~~
self.image_hit = pygame.image.load("image/enemy3_hit.png") # 加载大型敌机中弹图片
~~~
与此同时,我们需要知道中型敌机和大型敌机在什么时候被击中,以便播放中弹画面,因此需要在中型敌机和大型敌机内部添加一个表示当前飞机是否被击中的标志位:
~~~
self.hit = False # 飞机是否被击中标志位
~~~
并且在对应reset()函数中重置其属性:
~~~
self.hit = False
~~~
加载完相关资源后,接下来开始转入主程序模块进行绘制血槽的工作。
3、定义绘制血槽所用背景颜色
为了反映当前血量情况,突出剩余血量的比重,我们采用如下的血槽绘制机制:血槽的底色为黑色,当前血量用绿色线条表示,并随energy变量的减少而缩短,当血量低于百分之二十时,血量的显示由绿色变为红色,为了方便对指定颜色的调用,我们在main函数开始部分(while循环之外)对各个颜色进行宏定义:
~~~
color_black = (0, 0, 0)
color_green = (0, 255, 0)
color_red = (255, 0, 0)
color_white = (255, 255, 255)
~~~
稍微注意这里Pygame的彩色分量顺序就是平时大家所熟知的R,G,B。(opencv中为BGR)
4、绘制血槽
绘制血槽的操作是在敌方飞机绘制过程中同步进行的,考虑到代码的多层嵌套关系,这里先将绘制血槽的完整代码给出,再作解释:
~~~
for each in big_enemies: # 绘制大型敌机并自动移动
if each.active: # 如果飞机正常存在
# 飞机移动move()
if not each.hit:
# 如果飞机未被击中# 绘制大型敌机的两种不同的形式
else:
screen.blit(each.image_hit, each.rect)
each.hit = False
# ====================绘制血槽====================
pygame.draw.line(screen, color_black,
(each.rect.left, each.rect.top - 5),
(each.rect.right, each.rect.top - 5),
2)
energy_remain = each.energy / enemy.BigEnemy.energy
if energy_remain > 0.2: # 如果血量大约百分之二十则为绿色,否则为红色
energy_color = color_green
else:
energy_color = color_red
pygame.draw.line(screen, energy_color,
(each.rect.left, each.rect.top - 5),
(each.rect.left + each.rect.width * energy_remain, each.rect.top - 5),
2)
if each.rect.bottom == 0:
# 播放大型飞机的音效(循环播放)
else: # 如果飞机已撞毁
pass
~~~
这里代码有点多,我们逐行解释。通过for语句轮询大型敌机精灵组中的每个敌机对象,如果该飞机对象为激活状态(if each.active:),则进行接下来的飞机移动、血槽绘制的工作,否则进入飞机损毁的代码程序,包括播放音效以及损毁图片等等(之前博文已经介绍过)。如果飞机状态为激活状态,则需要判断其内部的“hit”标志位来判断当前飞机是否中弹,如果hit = false,则说明飞机没有中弹,正常绘制飞机的状态即可(大型敌机有帧切换特效,详见之前博文),如果hit = true,则说明飞机当前中弹,需要绘制飞机的中弹图片,同时重置hit标志位。
接下来是血槽,只要飞机处于激活状态,就需要绘制血槽,而无需考虑飞机当前是否中弹。首先通过pygame.draw.line()函数绘制血槽背景,背景颜色为黑色,线的长度与精灵对象的宽度相等,位置处于精灵图片上方五个像素的位置。之后通过energy_remain = each.energy / enemy.BigEnemy.energy计算当前剩余血量百分比,这里即体现出了之前在定义energy时将其定义为类全局变量和对象局部变量两种形式的优势。
得到剩余血量比重后,则判断当前剩余血量是否大于百分之二十,如果大于0.2,则血槽颜色为绿色,否色为红色。指定好血槽颜色之后即可再次调用pygame.draw.line()来绘制血槽长度,位置和背景位置相同,但血槽长度需要通过“血槽背景长度(总长度)*剩余血量百分比”来获得,即代码中的“each.rect.width * energy_remain”,这样当前血槽长度就和当前血量(self.energy)成正比。
同理,中型敌机的血槽绘制方式和上面大型敌机的绘制方式基本一样的,只需将在计算剩余血量时将“enemy.BigEnemy.energy”改为“enemy.MidEnemy.energy”即可。程序编写到这里,运行将会看到中型敌机和大型敌机都将会顶着一个血槽光环出场了,但这里仍然是一击毙命,原因是我们只是绘制了血槽,还没有真正的对血量(energy)进行操作。
5、添加碰撞检测血量递减机制
血量的真正意义是通过中弹递减来体现的,即中弹一次(飞机和子弹发生一次碰撞),energy变量就减一,当energy = 0时,飞机损毁,因此我们需要修改一下之前写的碰撞检测处理函数:
~~~
# ====================子弹与敌机的碰撞检测====================
for b in bullets:
if b.active: # 只有激活的子弹才可能击中敌机
# 子弹移动,碰撞检测if enemies_hit: # 如果子弹击中飞机
# 子弹损毁
for e in enemies_hit:
if e in big_enemies or e in mid_enemies:
e.energy -= 1
e.hit = True # 表示飞机已经被击中
if e.energy == 0:
e.active = False # 大中型敌机损毁
else:
e.active = False # 小型敌机损毁
~~~
这段代码相对来说容易理解,之前在完成子弹和敌机的碰撞检测后,对于发生碰撞的敌机(enemies_hit),我们直接令e.active = false来销毁敌机。然而在这里我们需要判断,如果碰撞的是中型敌机和大型敌机,则将其energy变量减一,并将hit赋值为true表示中弹,在当energy = 0时才执行销毁操作;若发生碰撞的是小型敌机,那无话可说,一击毙命。
ok,程序运行到这里应该能够顺利执行,本来还想继续介绍一些关于声道阻塞的BUG解决方案,鉴于内容已经不少了,还是放到下次博文吧。
Python编写微信打飞机小游戏(六)
最后更新于:2022-04-01 14:23:02
接下来,我们为我方飞机添加武器——发射子弹。
考虑到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,这篇博文就到这里,大家工作顺利。
Python编写微信打飞机小游戏(五)
最后更新于:2022-04-01 14:23:00
在完成敌方敌机的初步设置后,运行程序我们发现在屏幕上我方飞机和敌方飞机能够友好共存,互不干涉,这显然不符合游戏的宗旨,在这篇文章中我们为游戏添加我方飞机和敌机之间的碰撞损毁机制。
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,这次博文就先介绍到这里吧。
Python编写微信打飞机小游戏(四)
最后更新于:2022-04-01 14:22:58
之前的工作已经基本上将我方飞机的图形显示工作做的差不多了,这篇博客中我们将开始添加敌方飞机——小型敌机、中型敌机(直升机)和大型敌机(坦克)。新建一个enemy.py文件,导入pygame和random模块,开始编写吧(还是要注意文件编码问题,以后就不再啰嗦了)。
敌方飞机类与我方飞机模块有一定的相似性,但不会左右移动,不会发射子弹等等。小型敌机是敌方飞机中最基本的类型,一击毙命,没有血量、没有出场音效。中型敌机有一定血量,损毁时附带特殊音效。大型敌机血量最多,出场和损毁时都有特殊音效,游戏中中型敌机和大型敌机都将显示血槽。
1、小型敌机类
和我方飞机一样,小型敌机类也是继承自Pygame的精灵类,在初始化过程中需要先调用基类的初始化函数,在加载飞机图像、获取飞机图像尺寸,设置小飞机的移动速度等等:
~~~
class SmallEnemy(pygame.sprite.Sprite):
def __init__(self, bg_size):
pygame.sprite.Sprite.__init__(self)
self.image = pygame.image.load("image/enemy1.png") # 加载敌方飞机图片
self.rect = self.image.get_rect() # 获得敌方飞机的位置
self.width, self.height = bg_size[0], bg_size[1] # 本地化背景图片位置
self.speed = 2 # 设置敌机的速度
~~~
在初始化小飞机的位置时,需要稍微做一点工作:
~~~
self.rect.left, self.rect.top = (randint(0, self.width - self.rect.width), # 定义敌机出现的位置
randint(-5 * self.rect.height, -5) # 保证敌机不会在程序已开始就立即出现
)
~~~
小飞机在初始化时需要随机指定其位置,其x左边(横向宽度)在(0,width - rect.width)之间,保证飞机能够在整个屏幕的X轴的任何位置出现并且不会越界。其top(纵向高度)在(-5 * rect.height, -5)之间,这里之所以最大值设置为负数(-5),是为了保证程序开始运行时敌机不会立即出现,给用户以反应时间。这里还有一个小细节需要注意,就是如果Python的代码太长,在换行时建议使用小括号来指定范围,不推荐只使用“\”来连接各个行。
2、move()和reset()函数
在定义完小型飞机的数据结构之后,需要定义其移动操作move()函数(成员函数)。敌机的移动函数功能相对单一,只是在屏幕上以固定速度向下移动:
~~~
def move(self): # 定义敌机的移动函数
if self.rect.top < self.height:
self.rect.top += self.speed
else:
self.reset()
~~~
这里即可体现出speed参数的作用。当然还需要编写一个reset()成员函数来将已经移动出屏幕的敌机进行重置:
~~~
def reset(self): # 当敌机向下移动出屏幕时
self.rect.left, self.rect.top = (randint(0, self.width - self.rect.width),
randint(-5 * self.rect.height, 0)
)
~~~
3、中型敌机
中型敌机结构与小型敌机类此,区别在于中型敌机的图标、血量以及损毁音效等方面,其中敌机血量以及损毁音效的添加我们在之后的博文中再加以介绍:
~~~
class MidEnemy(pygame.sprite.Sprite):
def __init__(self, bg_size):
pygame.sprite.Sprite.__init__(self)
self.image = pygame.image.load("image/enemy2.png") # 加载敌方飞机图片
self.rect = self.image.get_rect() # 获得敌方飞机的位置
self.width, self.height = bg_size[0], bg_size[1] # 本地化背景图片位置
self.speed = 1 # 设置敌机的速度,应该比小型敌机速度稍慢
self.rect.left, self.rect.top = (randint(0, self.width - self.rect.width), # 定义敌机出现的位置
randint(-10 * self.rect.height, -self.rect.height)
)
def move(self): # 定义敌机的移动函数
if self.rect.top < self.height:
self.rect.top += self.speed
else:
self.reset()
def reset(self): # 当敌机向下移动出屏幕时
self.rect.left, self.rect.top = (randint(0, self.width - self.rect.width), # 定义敌机出现的位置
randint(-10 * self.rect.height, -self.rect.height) # 保证一开始不会有中型敌机出现
)
~~~
4、大型敌机
大型敌机作为boss级别的存在,其不仅仅具有高血量、特殊出场音效以及损毁音效,其在移动过程中也存在图片切换的特效(与我方飞机尾部喷气特效类似),因此在加载图片时同样需要加载两张图片,以便在主程序显示的过程中进行切换:
~~~
class BigEnemy(pygame.sprite.Sprite):
def __init__(self, bg_size):
pygame.sprite.Sprite.__init__(self)
self.image1 = pygame.image.load("image/enemy3_n1.png") # 加载敌方飞机图片,其中大型飞机有帧切换的特效
self.image2 = pygame.image.load("image/enemy3_n2.png")
self.rect = self.image1.get_rect() # 获得敌方飞机的位置
self.width, self.height = bg_size[0], bg_size[1] # 本地化背景图片位置
self.speed = 2 # 设置敌机的速度
self.rect.left, self.rect.top = (randint(0, self.width - self.rect.width), # 定义敌机出现的位置
randint(-15 * self.rect.height, -5 * self.rect.height)
)
def move(self): # 定义敌机的移动函数
if self.rect.top < self.height:
self.rect.top += self.speed
else:
self.reset()
def reset(self): # 当敌机向下移动出屏幕时
self.rect.left, self.rect.top = (randint(0, self.width - self.rect.width), # 定义敌机出现的位置
randint(-15 * self.rect.height, -5 * self.rect.height)
)
~~~
5、敌方飞机实例化控制函数
在完成敌机模块enemy.py的构建之后,我们开始在主程序中将敌机实例化并显示在屏幕上,首先在程序开始时导入已经编写好的模块:
~~~
import enemy
~~~
注意我们的打飞机程序在设计时是设计成有难度分级的,玩家得分越多、需要挑战的难度等级就越高;难度等级越高,所出现的敌机数量就越多,可见敌机的实例数量是随时可能发生变化的,直接实例化具体数量的敌机显然不满足要求,在此我们通过编写一个实例化控制函数来实现敌机对象的动态添加,以小型敌机添加函数为例:
~~~
# ====================敌方飞机生成控制函数====================
def add_small_enemies(group1, group2, num):
for i in range(num):
e1 = enemy.SmallEnemy(bg_size)
group1.add(e1)
group2.add(e1)
~~~
这个函数结构简单,通俗易懂,即将指定个敌机对象添加到精灵组(sprite.group)中。参数group1、group2是两个精灵组类型的形参,用以存储多个精灵对象(敌机)。需要注意的一点是group既然是特定的精灵组结构体,在向其内部添加精灵对象时需要调用其对应的成员函数add(),不能使用列表添加函数append()。至于for循环这些python的基础知识这里就不再赘述
以此类推,编写中型敌机和大型敌机的实例化控制函数:
~~~
def add_mid_enemies(group1, group2, num):
for i in range(num):
e2 = enemy.MidEnemy(bg_size)
group1.add(e2)
group2.add(e2)
def add_big_enemies(group1, group2, num):
for i in range(num):
e3 = enemy.BigEnemy(bg_size)
group1.add(e3)
group2.add(e3)
~~~
友情提示,所有函数都应该在第一次使用之前进行定义,也就是说以上三个函数都应该定义在main()函数之前,以保证在main()函数中顺利调用。
6、敌机实例化
第一步,在main函数中,在进入while循环之前,调用enemy.py模块,向精灵组中添加敌机精灵对象:
~~~
# ====================实例化敌方飞机====================
enemies = pygame.sprite.Group() # 生成敌方飞机组
small_enemies = pygame.sprite.Group() # 敌方小型飞机组
add_small_enemies(small_enemies, enemies, 1) # 生成若干敌方小型飞机
mid_enemies = pygame.sprite.Group() # 敌方小型飞机组
add_mid_enemies(mid_enemies, enemies, 1) # 生成若干敌方中型飞机
big_enemies = pygame.sprite.Group() # 敌方小型飞机组
add_big_enemies(big_enemies, enemies, 1) # 生成若干敌方大型飞机
~~~
先调用pygame.sprite.Group()生成精灵组,这里需要生成两种精灵组,一种精灵组用以存储所有敌机精灵(不区分小型中型大型),另一种则是针对不同型号敌机创建不同的精灵组来存储。之所以这样做,是因为不同类型的敌机之前既有共同属性,又有各自的特殊属性,在处理中更为方便。精灵组生成之后调用对应的生成控制函数来向其中添加敌机精灵对象即可(这里先将添加数量均设置为1)。
第二步,显示敌机。绘制小型敌机和中型敌机的方法类似,即将当前精灵组中的所有对象通过for()循环进行索引,并逐个blit()到屏幕对象中,并激活其内部的移动函数使其移动,注意这部分代码应该位于while循环之内:
~~~
for each in small_enemies: # 绘制小型敌机并自动移动
each.move()
screen.blit(each.image, each.rect)
~~~
~~~
for each in mid_enemies:
if each.active:
each.move()
screen.blit(each.image, each.rect)
~~~
大型敌机在绘制时需要添加图片切换的特效,参照之前我方飞机的尾部喷气特效的实现方法:
~~~
for each in big_enemies: # 绘制大型敌机并自动移动
each.move()
if switch_image:
screen.blit(each.image1, each.rect) # 绘制大型敌机的两种不同的形式
else:
screen.blit(each.image2, each.rect)
~~~
~~~
if each.rect.bottom == -50:
big_enemy_flying_sound.play(-1)
~~~
~~~
~~~
这里当大型敌机即将出场时rect.bottom == -50,需要实现播放出场音效,并且是循环播放(参数设置为-1),如上所示。OK,运行代码,就会看到敌方飞机陆续从屏幕上飞过,不过这里应该有一个bug,就是大型敌机出场时不能顺利播放出场音效,这涉及Pygame的一个声道阻塞的问题,我们将在后续的博文中进行总结,今天就先进行到这里吧。
Python编写微信打飞机小游戏(三)
最后更新于:2022-04-01 14:22:55
通过之前的任务,我们已经能够实现小飞机在屏幕上自由移动了,这次博客的任务是是的我们的小飞机看起来更有灵性——尾部喷气特效的绘制。
1、帧率
说道帧率,玩游戏的人可能并不陌生。如果你是一个lol的玩家,那你肯定最关心两件事,一是帧率(FPS),二是延迟。延迟取决于网络性能,而帧率则取决于电脑的硬件配置,自然是帧率越高游戏画面就越流畅。帧率,简单理解就是游戏在一秒钟进行多少次画面刷新。Pygame支持的帧率大约是从30到120,为了避免游戏程序因为电脑性能的差异出现太大差别,我们在此为程序指定一个时钟的帧率(比如说三十)。
Pygame通过pygame.time.Clock()函数来设置帧率,具体来说由下面两条语句完成:
~~~
clock = pygame.time.Clock() # 设置帧率
clock.tick(60) # 设置帧数为60
~~~
意义很明确,设置游戏的帧率为60,需要强调的一点是第二句话一定要放在while循环内部,否则将不起作用。为了体验帧率的实际意义,大家可以尝试把帧率设置为一个低值(比如设为1),来体验一下帧率的实际作用。
2、图像加载
对飞机添加尾部“突突突”喷气特效的手段其实很简单,就是在两张飞机图片之间来回切换显示。在下载得到的图片音效资源中,留心观察会发现其中有两张我方飞机的图片:“hero1”和“hero1”,这两张图片的不同之处就在为飞机尾部的喷气形状方面,试想如果在程序运行期间将这两张图片不停的切换显示,应该就能得到想要的那种尾部动态效果了。
首先,在我方飞机模块中加载这两张图片(之前已经加载过第一张了):
~~~
self.image1 = pygame.image.load("image/hero1.png") # 加载飞机图片1
self.image2 = pygame.image.load("image/hero2.png") # 加载飞机图片2
~~~
注意这里由于两张图片实际上是代表一个对象的,因此两张图片将来在显示时也共享同一位置,因此下面的self.rect(我方飞机位置)等属性不用更改。
3、图片切换
图片加载完成之后开始在主程序主进行切换绘制,试想如何让程序在前后两帧选择绘制不同的图像呢?通用的解决方案就是设置一个控制图片切换的布尔型标志位(switch_image),在每次程序运行中都将标志位进行取反,从而产生程序前后运行的差异。我们在main()函数中声明这个标志位并赋值:
~~~
switch_image = False # 控制飞机图片切换的标志位(用以模拟发动机喷火效果)
~~~
接下来在程序循环过程中,每次循环都将该标志位进行取反操作:
~~~
switch_image = not switch_image
~~~
接下来需要修改我方飞机的绘制操作,之前我们直接在每次循环中都使用blit()函数来绘制相同的飞机图片,在此我们将其修改为由标志位来控制的绘图操作:
~~~
if switch_image:
screen.blit(me.image1, me.rect) # 绘制我方飞机的两种不同的形式
else:
screen.blit(me.image2, me.rect)
~~~
OK,此时再运行程序应该就能看到我方飞机的喷气特效了。不过细心的朋友应该会觉得这个喷气效果抖动得有点太快了,每一帧都要切换一次,有点过于夸张,为了解决这个问题,我们设置一个延时计数器调节图片切换的速率(不在完全依赖帧数)
4、延时
延时是程序运行中一个很重要的属性,不仅仅在此处我们要通过延时来控制图片切换速度,到后面还要通过延时来控制子弹发射的速度、控制飞机损毁图片的打印速度等等。延时的实现可以说是一个最简单的计时器(与事件触发系统中的定时器time.set_timer()不同)。
首先我们在main函数中定义延时计数器delay并初始化,一般将其初始化为帧数,从而能够方便的与当前帧率联系在一起,此处初始化为60:
~~~
delay = 60 # 延时参数
~~~
然后在while()循环中,每循环一次将延时参数减一,减到零时再将其置为60:
~~~
if delay == 0:
delay = 60
delay -= 1
~~~
注意此时的delay和图片帧数已经相互关联了,即每播放一帧,delay就减一。
5、加延时的图片切换
设置完延时参数后,比如说我们希望每3帧切换一次图片,这时就需要修改switch_image的变换频率,使其在取反之前先咨询一下delay参数,决定本次循环需不需要进行图片切换:
~~~
if not delay % 3:
switch_image = not switch_image
~~~
运行程序,我们会发现图片尾气的切换变慢了,喷气效果比之前要好了一些,当然我们设置这个delay参数可不是仅仅为了让飞机的喷气好看点,后面还有很多重要的用途,ok这次博文就说到这里吧。
Python编写微信打飞机小游戏(二)
最后更新于:2022-04-01 14:22:53
通过之前的准备,已经成功加载了程序资源并使得程序能够顺利运行,接下来开始编写我方小飞机的模块。在工程目录下创建一个名为myplane.py的文件,导入pygame模块(注意文件编码问题)
1、精灵
Pygame中所有移动对象都可看做是一个精灵(sprite),精灵之间能够进行相互的交互通信,例如碰撞检测等等,对于pygame模块中精灵的介绍网上有很多资源,例如[《Pygame之精灵》](http://www.cnblogs.com/xiaowuyi/archive/2012/06/26/2563990.html "Pygame之精灵")在这里我方飞机就是一个精灵对象,因此其类定义应继承于pygame的精灵类:
~~~
class MyPlane(pygame.sprite.Sprite):
~~~
2、myplane的初始化
游戏运行的第一步就是生成我方飞机。我方飞机在生成时需要给定一些具体参数,如飞机的形状、飞机的生成位置,这些都属于myplane类的成员变量(Python是一门面向对象的变成语言)。Python对成员变量的初始化都是在__init__(self)函数中完成的,类似于C++语言中的构造函数,其中self则形如一个显式的this指针,代表当前类的一个抽象对象。init()函数的具体形式如下:
~~~
def __init__(self, bg_size):
pygame.sprite.Sprite.__init__(self)
self.image1 = pygame.image.load("image/hero1.png") # 加载飞机图片1
self.rect = self.image1.get_rect() # 得到当前我方飞机的位置
self.width, self.height = bg_size[0], bg_size[1] # 本地化背景图片的尺寸
self.rect.left, self.rect.top = (self.width - self.rect.width) // 2, (self.height - self.rect.height - 60) # 定义飞机初始化位置,底部预留60像素
self.speed = 10 # 设置飞机移动速度
~~~
首先在初始化时需要先调用基类(pygame.sprite.Sprite)的init()函数。注意由于在确定飞机的初始化位置时需要知道当前背景的尺寸,因此通过形参bg_size将当前背景尺寸传进来(为了方便,转换成width和height两个变量),然后是加载我方飞机的图片,并通过image的成员函数get_rect()得到我方飞机的图片尺寸。在指定飞机位置时,只需要对飞机这个精灵对象的rect属性进行设置,rect有许多有趣的属性(center、topmid等),在此只需指定其left(左边框坐标)和top(顶部边框坐标)来实现图片位置的定位,注意需要在底部预留一段空白区域(此处为60个像素)。最后,指定我方飞机的移动速度,这里暂时设置为10,稍后用到。
3、定义我方飞机移动函数
在对我方飞机完成初始化之后,需要定义飞机的移动操作,通过WASD或者上下左右键实现对飞机的控制。以向上移动(move_up()函数)为例,代码如下:
~~~
def move_up(self): # 飞机向上移动的操作函数,其余移动函数方法类似
if self.rect.top > 0: # 如果飞机尚未移动出背景区域
self.rect.top -= self.speed
else: # 若即将移动出背景区域,则及时纠正为背景边缘位置
self.rect.top = 0
~~~
对飞机的移动是通过将对应边框坐标加减speed来完成的,例如向上移动的话就是self.rect.top - self.speed,注意此处需要对飞机当前的位置进行越界检测,如果此时self.rect.to<0了,即说明飞机已经超出图像的上界,即将top值置为零,即如论在如何向上移动,飞机都只停留在北京的上边缘。其余三个方向的移动函数原理类似,这里不再赘述:
~~~
def move_down(self):
if self.rect.bottom < self.height - 60:
self.rect.top += self.speed
else:
self.rect.bottom = self.height - 60
def move_left(self):
if self.rect.left > 0:
self.rect.left -= self.speed
else:
self.rect.left = 0
def move_right(self):
if self.rect.right < self.width:
self.rect.right += self.speed
else:
self.rect.right = self.width
~~~
4、在主函数mian中实例化我方飞机
在初步编写好我方飞机模块myplane.py之后,需要在主程序中进行实例化,以显示我方灵活的小飞机。首先在主程序中加载我方飞机模块:
~~~
import myplane
~~~
然后在mian()函数中完成飞机的实例化操作:
~~~
me = myplane.MyPlane(bg_size) # 生成我方飞机
~~~
强调由于整个程序运行只需要进行一次实例化,因此应将此操作放在main()函数中的whlie循环之外。在实例化完成后,需要将我方飞机绘制到屏幕上进行显示:
~~~
screen.blit(me.image1, me.rect)
~~~
注意由于飞机是实时移动的,需要在每帧图像中都对飞机进行重新绘制,因此绘制飞机的blit()操作应该写在main()函数的while循环之内
5、检测用户按键消息并控制飞机移动
在控制飞机移动时需要响应用户的鼠标键盘事件。这里介绍Pygame中两种事件响应机制,一是形如“for event in pygame.event.get():”的形式,只需要验证“event.type”的键值即可得到当前用户发出的事件类型,这种消息处理机制适合处理偶然发生的事件,如用户的暂停操作、关闭操作等。另外一种则是下面将要用到的“pygame.key.get_pressed()”,这种形式下会将指定类型的用户操作保存为一个列表,只需判断列表中对应时间消息的标志位的真假即可判断某类时间是否发生,适合处理频繁出现的事件操作(如键盘的方向控制事件),详情如下:
~~~
key_pressed = pygame.key.get_pressed() # 获得用户所有的键盘输入序列
if key_pressed[K_w] or key_pressed[K_UP]: # 如果用户通过键盘发出“向上”的指令,其他类似
me.move_up()
if key_pressed[K_s] or key_pressed[K_DOWN]:
me.move_down()
if key_pressed[K_a] or key_pressed[K_LEFT]:
me.move_left()
if key_pressed[K_d] or key_pressed[K_RIGHT]:
me.move_right()
~~~
当然,这些操作应该放置在while循环内部,因为这是一个实时检测实时刷新的过程。到此我们的程序的基本框架就完成了,循环播放背景音乐,小飞机能够灵活控制移动(不会越界),但我方小飞机还不会喷气、不会发生子弹,这些功能我们在下一篇博文中再介绍吧。
Python编写微信打飞机小游戏(一)
最后更新于:2022-04-01 14:22:51
最近开始学习Python语言,发现Python有一个神奇的Pygame模块,在编写小游戏时显得非常方便,于是参照教学视频编写了一个微信打飞机的小游戏,网上有很多相关的博客,但都不是很详细,大都是直接贴代码,于是决定沉下心来把编写程序的过程记录下来,与大家分享。
首先声明一点,这篇博客完全参照了小甲鱼的《零基础入门学习Python》教学视频,为了尊重原创,特将原作者的视频地址公布如下:[《零基础入门学习Python》](http://blog.fishc.com/category/python "零基础入门学习Python")
Python编译器和Pygame模块的安装网上的教程很多,这里不再赘述,需要强调的一点是,两者之前不仅要求版本一致,所对应操作系统位数也应该一致,例如32位的Pygame模块必须对应32位的Python编译器,我采用的是Python3.4(32位),所用Python的IDE为Pycharm。
在编写游戏的过程中需要用到大量图片和声音资源,如背景图片、背景音乐等,这里将相关资源统一打包,供大家下载:[《微信打飞机图片音效资源》](http://download.csdn.net/detail/u013088062/9250515 "《微信打飞机图片音效资源》")
[Python编写微信打飞机小游戏(一)](http://blog.csdn.net/u013088062/article/details/49705439)
[Python编写微信打飞机小游戏(二)](http://blog.csdn.net/u013088062/article/details/49722387)
[Python编写微信打飞机小游戏(三)](http://blog.csdn.net/u013088062/article/details/49744985)
[Python编写微信打飞机小游戏(四)](http://blog.csdn.net/u013088062/article/details/49766129)
[Python编写微信打飞机小游戏(五)](http://blog.csdn.net/u013088062/article/details/49786277)
[Python编写微信打飞机小游戏(六)](http://blog.csdn.net/u013088062/article/details/49823693)
[Python编写微信打飞机小游戏(七)](http://blog.csdn.net/u013088062/article/details/49834123)
[Python编写微信打飞机小游戏(八)](http://blog.csdn.net/u013088062/article/details/49872089)
[Python编写微信打飞机小游戏(九)](http://blog.csdn.net/u013088062/article/details/49890837)
[Python编写微信打飞机小游戏(十)](http://blog.csdn.net/u013088062/article/details/49893717)
[Python编写微信打飞机小游戏(十一)](http://blog.csdn.net/u013088062/article/details/49904363)
[Python编写微信打飞机小游戏(十二)](http://blog.csdn.net/u013088062/article/details/49907607)
1、导入相关模块。
创建主函数文件,如“mian.py”,在程序开始前将用到的Python模块导入:
~~~
# 导入相关模块
# coding: utf-8
import pygame
import sys
import tracebackfrom random import *
from pygame.locals import *
~~~
这里有几个问题需要解释一下:(1)如果Python不能正常打印中文字符,需要在文件开头加上“# coding: utf-8”来指定对应的编码类型;(2)“sys”为系统模块,“traceback”为Python用来捕获异常的模块,“random”为随机数生成模块,“pygame.locals”则包含了Pygame中的一些固定的标志常量,在下面的程序中将会用到。(3)这里涉及了Python两种导入模块的方式,一种是形如“import pygame”,这种方法表示声明一个模块,如果需要使用其中的元素,应采用“模块名.变量名”的格式;另一种形如“from random import *”,表示将模块中的所有元素全部导入当前工程,在使用时直接使用元素名称即可。
2、加载相关资源
将下载得到的资源(一个image文件夹,一个sound文件夹)放入工程目录下(与mian.py相同的文件夹中),之后在主程序中加载这些资源。在加载之前,需要对pygame的相关功能进行初始化:
~~~
# ==================初始化==================
pygame.init()
pygame.mixer.init() # 混音器初始化
bg_size = width, height = 480, 852 # 设计背景尺寸
screen = pygame.display.set_mode(bg_size) # 设置背景对话框
pygame.display.set_caption("飞机大战……FishC Demo")
background = pygame.image.load("image/background.png") # 加载背景图片,并设置为不透明
~~~
注意:(1)mixer为pygame模块的声音控制对象(俗称混音器),在涉及到与声音有关的操作时会用到。(2)bg_size取决于背景图片的尺寸
在完成相关初始化后,可以正式的加载背景图片和相关音效了:
~~~
# ==========载入游戏音乐====================
pygame.mixer.music.load("sound/game_music.wav")
pygame.mixer.music.set_volume(0.2)
bullet_sound = pygame.mixer.Sound("sound/bullet.wav")
bullet_sound.set_volume(0.2)
big_enemy_flying_sound = pygame.mixer.Sound("sound/big_spaceship_flying.wav")
big_enemy_flying_sound.set_volume(0.2)
enemy1_down_sound = pygame.mixer.Sound("sound/enemy1_down.wav")
enemy1_down_sound.set_volume(0.2)
enemy2_down_sound = pygame.mixer.Sound("sound/enemy2_down.wav")
enemy2_down_sound.set_volume(0.2)
enemy3_down_sound = pygame.mixer.Sound("sound/enemy3_down.wav")
enemy3_down_sound.set_volume(0.2)
me_down_sound = pygame.mixer.Sound("sound/game_over.wav")
me_down_sound.set_volume(0.2)
button_down_sound = pygame.mixer.Sound("sound/button.wav")
button_down_sound.set_volume(0.2)
level_up_sound = pygame.mixer.Sound("sound/achievement.wav")
level_up_sound.set_volume(0.2)
bomb_sound = pygame.mixer.Sound("sound/use_bomb.wav")
bomb_sound.set_volume(0.2)
get_bomb_sound = pygame.mixer.Sound("sound/get_bomb.wav")
get_bomb_sound.set_volume(0.2)
get_bullet_sound = pygame.mixer.Sound("sound/get_double_laser.wav")
get_bullet_sound.set_volume(0.2)
~~~
注意,由于pygame音频处理能力有限,在读取音频信息时,需要将音频文件转换成wav格式,普通的MP3格式一般情况下是无法正常读取的,更多pygame有关音频文件的操作可以参考下面的博客:[《Pygame音频操作》](http://www.cnblogs.com/xiaowuyi/archive/2012/06/15/2550338.html "Pygame音频操作")[
](http://www.cnblogs.com/xiaowuyi/archive/2012/06/15/2550338.html "pygame音频操作")
3、游戏主进程,循环播放背景音乐
在加载完相关资源之后,开始编写游戏的主函数。由于还没有定义相关的模块,这里显示编写一个最简单的框架,让程序能够循环运行起来。
~~~
if __name__ == '__main__':
try:
main()
except SystemExit:
pass
except:
traceback.print_exc()
pygame.quit()
input()
~~~
这是Python的主程序入口,如果我们运行main.py文件,程序则自动开始运行。如果只是将main.py作为一个模块import到其他工程中,则不会触发这个函数的运行。注意在这里我们使用了try语句来捕获程序运行时出现的异常,如果main()函数在运行过程中抛出任何异常,除了系统正常退出(SystemExit)的异常外,其他异常都通过“traceback.print_exc()”来打印异常信息,同时调用“pygame.quit()”退出程序。
由于还没有编写其他代码,在此处我们只编写一个最简单的main()函数来保证程序的正常运行,即简单的显示背景图片并循环播放背景音乐:
~~~
def main():
pygame.mixer.music.play(-1)
running = True
while running:
screen.blit(background, (0, 0))
for event in pygame.event.get(): # 响应用户的偶然操作
if event.type == QUIT: # 如果用户按下屏幕上的关闭按钮,触发QUIT事件,程序退出
pygame.quit()
sys.exit()
pygame.display.flip()
~~~
这里对混音器mixer的成员函数play()赋值为-1代表循环播放该音乐,如果形参为空,则只播放一次该音乐。这里在主程序循环中需要设置用户退出操作,当检测到用户按下界面的关闭按钮时(×),程序将调用pygame.quit()和sys.exit()完成退出。注意如果不设置这个操作,将会导致程序无法正常关闭(一般表现为程序无法响应)。在屏幕绘制方面,Python采用了一种双缓冲的屏幕刷新机制,即先通过blit()函数将一个图片(统称为surface对象)绘制在内存中,最后统一用pygame.display.flip()函数将绘制好的surface对象一次全部刷新到屏幕上。至于什么是surface对象,可以暂时理解为Python世界中的所有的图形图像吧。ok,第一部分就先介绍这些。
[《Python编写微信打飞机小游戏(一)》](http://blog.csdn.net/u013088062/article/details/49705439)
前言
最后更新于:2022-04-01 14:22:49
> 原文出处:[Python编写微信打飞机小游戏](http://blog.csdn.net/column/details/pythonplane.html)
作者:[u013088062](http://blog.csdn.net/u013088062)
**本系列文章经作者授权在看云整理发布,未经作者允许,请勿转载!**
# Python编写微信打飞机小游戏
> Python编写微信打飞机全程教程,解释详细,通俗易懂