Bootstrap

基于pygame实现的飞机大战游戏

目录

 

**摘要**


  为了深入学习python程序设计语言面向对象设计的特点,以此开发一款桌面版的飞机大战游戏,学习python程序设计语言中数据与功能的组合。此项目是基本pygame开发的一款桌面版的飞机大战游戏。通过pygame中的精灵与精灵组的特性实现游戏面板上数据内容和图像内容的显示,实现飞机、子弹、道具与敌机的生成。帧数的设置实现游戏页面的显示和飞机的动态效果,监听游戏事件以实现游戏事件的执行,pygame中的collide模块实现飞机、子弹、道具和敌机之间的碰撞接触,mixer模块实现背景音乐和游戏音效。游戏有良好的界面显示,简单的游戏操作,给玩家良好的游戏体验。  

1.引言

1.1 背景

 此项目是基于pygame开发的一款飞机大战游戏,良好的游戏界面,玩家通过简单的键盘操作即可进行游戏,体验游戏乐趣。

1.2 意义

 以pygame开发飞机大战游戏,体会和理解面向对象开发的同时,了解并学习pygame的高级使用

1.3 功能

 (1)显示游戏背景图片
 (2)显示游戏状态、游戏分数、炸弹提示、生命次数和文本提示等与游戏数据或状态相关的内容
 (3)显示玩家飞机与敌机的逐帧动画,根据游戏关卡随机生成相应数量的敌机
 (4)玩家飞机每0.2秒自动连续发射3颗子弹,子弹沿屏幕上方飞行,若与敌机接触则碰撞击毁敌机
 (5)玩家飞机通过方向键进行移动,还可以通过按键“B”释放炸弹,每次释放炸弹后,数量减一,当数量为0时,则不可使用
 (6)每30秒随机从屏幕顶部掉落道具,玩家飞机拾取道具可增加炸弹或增强子弹
 (7)玩家飞机若与敌机碰撞,则减少生命次数,当生命次数达到0则游戏结束
 (8)播放音乐与音效

2.系统结构

2.1 整体框架

在这里插入图片描述

图1 整体框架

 

2.2 精灵与精灵组

为了实现高仿真的飞机大战游戏,使用了两个由pygam提供的非常重要的类:精灵(Sprite)和精灵组(Group)
(1)精灵:
 A.在游戏开发中,通常把显示图像的对象叫做精灵Sprite
 B.精灵需要两个重要的属性:
  a. image 要显示的图像
  b. rect 图像要显示在屏幕的位置
 C.子类可以重写update()方法,在每次刷新屏幕时,更新精灵位置,需 在子类的初始化方法中,设置image和rect属性
(2)精灵组:
 A.一个精灵组可以包含多个精灵对象
 B. 调用精灵组对象的update()方法,可以自动调用组内每一个精灵的 update()方法
 C. 调用精灵组对象的draw()方法,可以将组内每一个精灵的image绘制在rect位置

2.3 功能介绍

2.3.1 玩家飞机

(1)在飞机大战游戏中,玩家可以操作玩家飞机(又叫英雄飞机)执行移动位置、引爆炸弹以及拾取道具等动作,英雄飞机还能够自动向上连续发射子弹,其中:
  A. 英雄出场后,会有3秒钟的无敌时间,也就是在这3秒钟内,任何敌机都无法摧毁英雄飞机
  B. 使用键盘的方向键可以在游戏窗口内移动英雄飞机(但不允许将英雄飞机移动到游戏窗口之外)
  C. 英雄出场后,每隔0.2秒,会自动连续发射3颗子弹,其中
   a. 子弹从英雄飞机头部的正上方发射,沿垂直方向向游戏窗口的上方飞行
   b. 如果子弹飞行途中,击中了敌机,会对敌机造成伤害
   c. 如果子弹飞出了游戏窗口,中途没有击中任何一架敌机,子弹会被销毁
  D. 英雄出场后,会默认携带3颗炸弹
   a. 玩家按下键盘上的字母B会引爆炸弹,一旦引爆,游戏窗口中所有敌机都会被炸毁
   b. 引爆炸弹后,游戏窗口左下角的炸弹数量会相应减少,如果炸弹数量不足,则显示数字0,同时不再允许玩家继续引爆炸弹

(2)英雄飞机包含的属性如表1所示:

表1 英雄飞机属性表

名称 速度 飞行动画 被击中图片 被摧毁动画 摧毁音效 升级音效
英雄 5 有 无 有 有 有

(3)子弹包含的属性如表2所示:

表2 子弹属性表

名称 速度 伤害力 音效
子弹 1 2 1 有

2.3.2 敌机类型和关卡设定

(1)敌机类型
飞机大战游戏中一共有三种类型的敌机,各自对应的属性如表3所示:

表3 敌机属性表


序号 名称 生命值 速度 分值 飞行动画 被击中图片 被摧毁动画 摧毁音效
01 小敌机 1 1~7 1000 图片 无 有 有
02 中敌机 6 1~3 6000 图片 有 有 有
03 大敌机 15 1 15000 有 有 有 有

(2)关卡设定
飞机大战游戏根据玩家的得分逐步提高难道,共设立3个关卡,具体的设定如表4所示:

表4 关卡级别表


序号 名称 分值范围 小敌机数量(速度) 中敌机数量(速度) 大敌机数量(速度)
01 关卡1 <10000 16(1~3) 0(1) 0(1)
02 关卡2 <50000 24(1~5) 2(1) 0(1)
03 关卡3 >=50000 32(1~7) 4(1~3) 2(1)

2.3.3 敌机登场

游戏开始后,根据不同的关卡,准备不同数量的敌机,敌机按照以下规则登场:
 (1)敌机的初始位置在游戏窗口上方的随机位置
 (2)敌机按照各自不同的速度,沿垂直方向向游戏窗口的下方飞行
 (3)如果飞行途中,与英雄飞机相撞,那么:
   A. 英雄飞机被摧毁,同时播放被摧毁动画及被摧毁音效
    a. 动画播放过程中,不允许操作英雄飞机
    b. 动画播放完成后,游戏窗口右下角英雄的命数会相应减少。如果还有剩余 命数,那么在英雄牺牲的位置出现新的英雄继续战斗(新出场的英雄有3秒的无敌时间,在无敌时间内,英雄飞机不会被摧毁,同时也不会摧毁其他敌机),反之没有剩余命数,则游戏结束
  B. 摧毁英雄飞机的敌机,同时要播放被摧毁动画及被摧毁音效
    a. 动画播放过程中,敌机在屏幕上位置不会移动
    b. 动画播放完成后,敌机会被设置回初始状态,跳转到第(1)步继续执行
 (4)如果飞行途中,被子弹击中,则敌机的生命值减去子弹的伤害力:
  A. 如果敌机的生命值>0,显示被击中图片(如果有),敌机继续向屏幕下方飞行
  B. 如果敌机的生命值<=0,播放被撞毁动画和音效
   a. 动画播放过程中,敌机在屏幕上位置不会移动
   b. 动画播放完成后,敌机会被设置回初始转态,跳转到(1)步继续执行
 (5)如果敌机飞出了游戏窗口,中途没有被摧毁,会被设置回初始状态,跳转到第(1)步继续执行

2.3.4 游戏道具和奖励

英雄飞机每得到100000分会被奖励1次生命。此外,游戏开始每隔30秒会从游戏窗口上方随机位置向下飞出游戏道具,具体道具作用如表5所示:

表5 道具作用表


序号 名称 功能描述 速度 播放音效
01 炸弹补给 英雄飞机拾取后,炸弹数量加1 5 有
02 子弹增强 英雄飞机拾取后,发射的子弹改为双排,持续时长20秒 5 有

2.3.5 游戏结束

当英雄飞机没有剩余命数,无法再继续进行战斗则游戏结束。在英雄飞机被撞毁的动画播放完成之后,会完成以下动作:
(1)整个游戏画面静止
(2)游戏窗口中央靠上位置显示“Game Over!”文字
(3)”Game Over!”下方显示最好得分,玩家可以对比左上角显示的当前得分更新最好得分,并在下次游戏时显示最新的最好得分
(4)在最好得分下面会有“Press spaceBar to continue”的提示语

2.4 游戏模块

飞机大战游戏项目中设计4个模块,分别是:
(1)game.py主游戏模块.封装Game类并负责启动游戏
(2)game_items.py游戏元素模块,封装英雄飞机、子弹、敌机、道具等游戏元素类,并定义全局变量
(3)game_hud.py游戏面板模块,封装指示器面板类,统一管理游戏状态、游戏分数、炸弹提示、生命次数以及文本提示等与游戏数据或状态相关的内容
(4)game_music.py游戏音乐模块,封装音乐播放器类,专门负责音乐和音效的播放

3.实现过程及代码

3.1 游戏框架搭建

3.1.1 游戏类的设计

1.根据功能和整体框架,游戏Game类的设计如图2所示:


在这里插入图片描述

 

图2 Game类图

2. 游戏的属性
根据属性的作用,又可以将游戏的属性分为游戏属性和精灵组属性
(1)游戏属性 在游戏对象中,除了常规的主窗口属性和游戏状态属性之外,还定义了2个精灵相关的属性以及1个音乐播放器属性。在游戏开发中,通知把显示图像的对象叫做精灵Sprite。在pygame中精灵有两个重要的属性。
 A. image要显示的图像(surface对象)
 B. rect图像要显示在游戏窗口的矩形区域 在游戏窗口中看到的每一个独立的图像或者一行文本,都可以看做一个精灵Sprite对象,例如:英雄飞机、一颗子弹、一架敌机等

游戏对象中定义的游戏属性列表如表6所示:

表6 游戏属性表


序号 名称 说明
01 main_window 游戏主窗口,初始大小为(480,700)
02 is_game_over 游戏结束标记,初始为False
03 is_pause 游戏暂停标记,初始为False
04 hero 英雄精灵,初始显示在游戏窗口中间靠下位置
05 hud_panel 指示器面板,负责显示和游戏状态以及数据相关的内容,包括:状态图像、游戏得分、炸弹数量、英雄命数,以及在游戏暂停或结束时,显示在游戏窗口中央位置的提示信息等
06 player 音乐播放器,负责播放背景音乐和游戏音效


(2)精灵组属性
精灵组是保存了多个精灵对象的组。精灵组有两个重要的应用场景:
 A. 一次性绘制或更新多个精灵
 B. 碰撞检测。碰撞检测就是检测多个精灵之间是否发生碰撞,例如:子弹是否集中敌机、敌机是否撞到英雄飞机

游戏对象中定义的精灵组属性列表如表7所示:

表7 精灵组属性表


序号 名称 说明
01 all_group 所有精灵组,存放所有要显示的精灵,用于屏幕绘制和更新位置
02 enemies_group 敌机精灵组,存放所有敌机精灵对象,用于检测子弹击中敌机以及敌机撞击英雄飞机
03 supplies_group 道具精灵组,存放所有道具机灵对象,用于检测英雄飞机拾取道具


3. 游戏对象的方法
游戏类中为游戏对象封装了如表8如示:

表8 游戏对象的方法表


序号 名称 说明
01 reset_game 重置游戏,开启新一轮游戏之前,将游戏属性恢复到初始值
02 create_enemies 创建敌机,在新游戏开始或者关卡升级后,根据当前游戏级别创建敌机精灵
03 create_supplies 创建道具,游戏开始后每隔30秒随机投放炸弹补给或子弹增强道具
04 start 开始游戏,创建时钟对象并且开启游戏循环,在游戏循环中监听事件、更新精灵位置、绘制精灵、更新显示、设置刷新帧率
05 event_handler 事件监听,监听并处理每一次游戏循环执行时发生的事件,避免游戏循环中的代码过长
06 check_collide 碰撞检测,监听并处理每一个游戏循环执行时是否发生精灵与精灵之间的碰撞,其中包括:子弹击中敌机、英雄拾取道具、敌机撞击英雄等

3.1.2 搭建游戏框架

  1. 在game_items.py模块定义游戏窗口矩形区域的全局变量,代码如下:
import pygame
 定义全局变量
SCREEN_RECT = pygame.Rect(0, 0, 480, 700)  # 游戏窗口矩形
  • 1
  • 2
  • 3
  1. 在game.py定义Game类,并实现初始化方法和重设游戏方法,
    (1). 在初始化方法中:
      A. 创建游戏窗口
      B. 设置窗口标题
      C. 设置游戏状态属性
    (2)定义reset_game 方法,恢复游戏状态的初始值
    (3)定义event_handler方法监听退出事件和监听空格键按键并切换状态
    (4)在start方法中:
      A. 定义时钟对象
      B. 完成游戏循环的基础代码
    代码如下:
```python
import pygame
from game_hud import *
from game_items import *
from game_music import *

class Game(object):
    """游戏核心类"""
    def __init__(self):
        # 游戏窗口
        self.main_window = pygame.display.set_mode(SCREEN_RECT.size)
        pygame.display.set_caption('飞机大战')

#游戏状态
		self.is_game_over = False  # 结束标记
		self.is_game_pause = False  # 暂停标记
	
def reset_game(self):
    	"""重置游戏数据"""
    	self.is_game_over = False
    	self.is_game_pause = False

def start(self):
      """开启游戏主逻辑"""
    	# 创建时钟
    	clock = pygame.time.Clock()
		while True:
			# 处理事件监听
            if self.event_handler():
                # event.handler 返回True则说明发生了退出事件
				return
			# 根据游戏转态切换界面显示的内容
            if self.is_game_over:
                print('游戏已经结束,按空格键重新开始游戏...')
            elif self.is_game_pause:
                print('游戏已经暂停,按空格键继续...')
            else:
                print('游戏进行中...')  
      #刷新页面
            pygame.display.update()
	# 设置刷新率
clock.tick(60)

	def event_handler(self):
    """获取并处理事件"""
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
            # 退出按钮被点击
        		return True
			elif event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE:
                # 用户按下了键盘上的 ESC 键
                return True
			elif event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE:
                # 用户按下了键盘上的 空格 键
                if self.is_game_over:
                    # 游戏已经结束,重置游戏
                    self.reset_game()
                else:
                    # 游戏还没结束,切换暂停状态
                    self.is_game_pause = not self.is_game_pause
		return False
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  1. 在主程序中创建游戏对象并且启动游戏
if __name__ == '__main__':
    #初始化数据
    pygame.init()

    #开始游戏
    Game().start()

    #释放游戏资源
    pygame.quit()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

3.2 精灵与精灵组

3.2.1 图像的加载和绘制

  1. 在game_items中创建派生游戏精灵子类
     #定义精灵类
class GameSprite(pygame.sprite.Sprite):
    res_path = './res/images/'

    def __init__(self, image_name, speed, *group):
        """初始化精灵对象"""
        # 调用父类方法,把当前精灵对象放到精灵组里
        super(GameSprite, self).__init__(*group)

        # 创建图片
        self.image = pygame.image.load(self.res_path + image_name)

        # 获取矩形
        self.rect = self.image.get_rect()

        # 设置移动速度
        self.speed = speed

    def update(self, *args):
        """更新元素数据"""
        self.rect.y += self.speed
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  1. 在game.py中使用精灵组绘制精灵内容
class Game(object):
    """游戏核心类"""

    def __init__(self):
    	···
		# 游戏精灵组
        self.all_group = pygame.sprite.Group()  # 存放界面上的所有精灵
        self.enemies_group = pygame.sprite.Group()  # 敌机精灵组
        self.supplies_group = pygame.sprite.Group()  # 道具精灵组
		
		# 游戏精灵
		GameSprite('background.png', 1, self.all_group)  # 创建背景精灵
		hero_sprite = GameSprite('me1.png', 0, self)
		hero_sprite.rect.center = SCREEN_RECT.center

	def start(self):
		….
		# 绘制内容
    	self.all_group.draw(self.main_window)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

在start方法内更新精灵组

def start(self):
		…
		while True:
			…
			else:
            	print('游戏进行中...')
                self.all_group.update()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

3.2.2 游戏背景连续滚动

(1)基本构思
游戏在运行的时候,整个游戏窗口显示一张以星空为背景的图像,同时背景图像会缓缓地向下方移动,给玩家产生一种英雄飞机向上方飞行的错觉
背景图像连续、不间断地向下运动的实现,如示意图3:

在这里插入图片描述

图3 背景示意图



具体实现:设置2张与屏幕大小相同的图片,背景图像1置于屏幕正中间,背景图像2置于背景图像1正上方,与背景图像1相贴合,2张背景图像沿屏幕向下移动,当背景图像1移动出屏幕之外时,判断图像的y值是否大于或等于屏幕的高度h,如果是则背景图像1移动到背景图像2,此时背景图像2置于正中间,如此循环。
(2)代码实现
 A. 初始化方法的is_alt参数:
  a. False表示第1个精灵,初始矩形区域应该和游戏窗口重叠
  b. True表示另一个精灵,初始矩形区域应该和游戏窗口的正上方
 B. 重写update()
  a. 先调用父类方法,完成向下移动矩形区域的动作
  b. 然后判断是否移出游戏窗口,如果是,将精灵设置到游戏窗口的正上方
 在game_items.py模块中定义Background类继承自GameSprite,代码如下:

class Background(GameSprite):

	def __init__(self, is_alt, *group):
    """如果 is_alt 为True则在初始化时这个精灵显示在窗口上方,False则显示在窗口内部"""
    	super(Background, self).__init__('background.png', 1, *group)

    	if is_alt:
        	self.rect.y = -self.rect.h

	def update(self, *args):
    	super(Background, self).update(*args)

    	# 如果图片已经滚动到底部,则立即回到窗口的最上面,供重新显示
    	if self.rect.y > self.rect.h:
        	self.rect.y = -self.rect.y
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

在game.py中的Game类中调用,代码如下

class Game(object):
    """游戏核心类"""

	def __init__(self):
		…
		# 创建背景精灵
    	self.all_group.add(Background(False), Background(True))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

3.3 指示器面板

在飞机大战游戏中设计一个指示器面板,负责显示和游戏状态以及数据相关的内容,包括:状态图像、游戏得分、炸弹图像、炸弹数量、英雄命数指示,以及在游戏暂停或结束时,显示在游戏窗口中央位置的提示信息等。

3.3.1 指示器面板类的设计

根据游戏功能的需要,得指示器面板的类图如图4所示:

在这里插入图片描述

图4 指示器面板类图


指示器的示意图如图5所示:

在这里插入图片描述

图5 指示器示意图

3.3.2 指示器面板类的准备

  1. 指示器面板类的创建
    在game_hud.py模块中定义 HUDPanel类,并实现初始化方法,代码如下:
"""游戏控制面板/提示信息模块"""
import pygame
class HUDPanel(object):
    """所有面板精灵的控制类"""
        def __init__(self, display_group):
        # 基本数据
        self.score = 0  # 游戏得分
        self.lives_count = 3  # 生命计数
        self.level = 1  # 游戏级别
        self.best_score = 0  # 最好成绩
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  1. 设计状态按钮类
    按照游戏规则描述,玩家按下空格键暂停或继续游戏,左上角状态精灵的显示图像会发生相应的变化。在game_items.py中,从GameSprite中派生一个StatusButton类,专门处理状态图像的切换,代码如下:
class StatusButton(GameSprite):
    """状态按钮精灵类"""

    def __init__(self, image_names, *groups):
        """image_names 接受一个元组,元组的0下标必须是暂停的图片,1下标必须是运行的图片"""
        super(StatusButton, self).__init__(image_names[0], 0, *groups)

        # 准备用于切换显示的两张图片
        self.images = [pygame.image.load(self.res_path + name) for name in image_names]

    def switch_status(self, is_pause):
        """根据是否暂停,切换要使用的图片对象"""
        self.image = self.images[1 if is_pause else 0]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  1. 创建图像精灵
    (1)在game_hud.py中的初始化方法上方,定义几个属性,用于设置精灵的矩形区域和标签精灵的字体颜色,代码如下:
class HUDPanel(object):
    """所有面板精灵的控制类"""

    margin = 10  # 精灵之间的间距
    white = (255, 255, 255)  # 白色
    gray = (64, 64, 64)  # 灰色
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

(2)在初始化方法的末尾增加以下代码,创建精灵并设置显示位置:

def __init__(self, display_group):
	# 图像精灵
        # 状态精灵
        self.status_sprite = StatusButton(('pause_nor.png', 'resume_nor.png'), display_group)
        self.status_sprite.rect.top = self.margin
        self.status_sprite.rect.left = self.margin

        # 炸弹精灵
        self.bomb_sprite = GameSprite('bomb.png', 0, display_group)
        self.bomb_sprite.rect.x = self.margin
        self.bomb_sprite.rect.bottom = SCREEN_RECT.bottom - self.margin
        
        # 生命精灵
        self.lives_sprite = GameSprite('life.png', 0, display_group)
        self.lives_sprite.rect.right = self.lives_label.rect.left - self.margin
        self.lives_sprite.rect.bottom = SCREEN_RECT.bottom - self.margin
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

(3)在game.py的初始化方法中添加创建游戏控制面板的代码:

class Game(object):
    """游戏核心类"""

    def __init__(self):
    	···
		# 创建游戏控制面板
        self.hud_panel = HUDPanel(self.all_group)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

3.3.3 使用精灵实现文本标签

 文本标签的显示内容并不需要在每一次游戏循环执行时都发生变化,因此不需要重写update()方法,同时,在游戏执行过程中,如果希望更改文本标签的显示内容,可以通过set_text方法重写渲染文本图像image并且更新矩形区域rect
(1)使用自定义字体定义文本标签精灵
 在game_items.py模块中定义Label类继承自pygame.sprite.Sprite,代码如下:

class Label(pygame.sprite.Sprite):
    """标签精灵类"""
    font_path = './res/font/MarkerFelt.ttc'

    def __init__(self, text, size, color, *groups):
        """初始化标签精灵的数据"""
        super(Label, self).__init__(*groups)

        # 创建字体对象
        self.font = pygame.font.Font(self.font_path, size)

        # 字体颜色
        self.color = color

        # 精灵属性
        self.image = self.font.render(text, True, self.color)
        self.rect = self.image.get_rect()

    def set_text(self, text):
        """更新显示的文本内容"""
        self.image = self.font.render(text, True, self.color)
        self.rect = self.image.get_rect()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

(2)创建指示器面板中的文本标签精灵
在game_hud.py中的HUDPanel类初始化方法的末尾增加代码,创建指示器面板中的文本标签精灵,代码如下:

#得分标签
    self.score_label = Label('%d' % self.score, 32, self.gray, display_group)
    self.score_label.rect.midleft = (self.status_sprite.rect.right + self.margin,
                                     self.status_sprite.rect.centery)
	# 炸弹计数标签
    self.bomb_label = Label('X 3', 32, self.gray, display_group)
    self.bomb_label.rect.midleft = (self.bomb_sprite.rect.right + self.margin,
                                    self.bomb_sprite.rect.centery)

    # 生命计数标签
    self.lives_label = Label('X %d' % self.lives_count, 32, self.gray, display_group)
    self.lives_label.rect.midright = (SCREEN_RECT.right - self.margin,
                                      self.bomb_sprite.rect.centery)
    # 最好成绩标签
    self.best_label = Label('Best:%d' % self.best_score, 36, self.white)
    self.best_label.rect.center = SCREEN_RECT.center

    # 状态标签
    self.status_label = Label('Game Paused!', 48, self.white)
    self.status_label.rect.midbottom = (self.best_label.rect.centerx,
                                        self.best_label.rect.y - 2 * self.margin)

    # 提示标签
    self.tip_label = Label('Press spaceBar to continue', 22, self.white)
    self.tip_label.rect.midtop = (self.best_label.rect.centerx,
                                  self.best_label.rect.bottom + 8 * self.margin)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

3.3.4 游戏数据修改

1.显示炸弹数量
(1)英雄出场后,会默认携带3颗炸弹
(2)玩家按下按键字母B会引爆炸弹,引爆炸弹后,游戏窗口左下角的炸弹数量应该相应减少
在game_hud.py中的HUDPanel类中定义show_bomb方法,使用传入的炸弹数量参数更新炸弹计数标签的显示,代码如下:

def show_bomb(self, count):
        """修改炸弹数量 X count """
        self.bomb_label.set_text('X %d' % count)
        self.bomb_label.rect.midleft = (self.bomb_sprite.rect.right + self.margin,
                                        self.bomb_sprite.rect.centery)
  • 1
  • 2
  • 3
  • 4
  • 5

在game.py中event_handler方法中添加使用炸弹事件

def event_handler(self):
        """获取并处理事件"""
        	for event in pygame.event.get():
        		···
				# 必须在游戏结束也没暂停才能执行的操作
            	if not self.is_game_over and not self.is_game_pause:
                	if event.type == pygame.KEYDOWN and event.key == pygame.K_b: 
                		self.hud_panel.show_bomb(self.hero_sprite.bomb_count)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  1. 显示生命计数
    (1)英雄飞机被敌机撞毁后,游戏窗口右下角的命数计数会相应减少
    (2)英雄飞机每得到100000分会被奖励1条命,游戏窗口右下角的命数计数也会相应增加
    在game_hud.py中的HUDPanel类中定义show_lives方法,使用生命计数属性值更新生命计数标签的显示,代码如下:
def show_lives(self):
        """显示最新的生命计数为 X lives_count"""
        self.lives_label.set_text('X %d' % self.lives_count)

        # 修改生命计数精灵的位置
        self.lives_label.rect.midright = (SCREEN_RECT.right - self.margin,
                                          self.bomb_sprite.rect.centery)

        # 修改生命精灵的位置
        self.lives_sprite.rect.right = self.lives_label.rect.x - self.margin
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  1. 增加游戏得分
    每当英雄飞机摧毁一架敌机之后,游戏得分就应该相应增加,并且摧毁不同类型的敌机增加的分值不同
    (1)生命计数 – 英雄飞机每得到100000分会被奖励1条命
    (2)最好成绩 – 如果当前游戏得分超过了历史最好成绩,应该以当前的游戏得分作为最好成绩
    (3)游戏级别 – 不同的游戏级别,出场敌机的类型、数量和速度都不尽相同,而游戏得分和游戏级别的关系如下:
      A. 得分 < 10000(级别1)
      B. 得分 < 50000(级别2)
      C. 得分 >= 50000(级别3)
    在HUDPanel类的初始化方法上方,定义奖励生命和游戏级别的类属性
	reward_score = 100000  # 奖励分值
    level2_score = 10000  # 级别2分值
    level3_score = 50000  # 级别3分值
  • 1
  • 2
  • 3

在HUDPanel类中定义并实现increase_score方法,代码如下:

 def increase_score(self, enemy_score):
        """增加得分,注意同时要处理增加生命、关卡升级、更新最好成绩"""

        # 计算最新得分
        score = self.score + enemy_score

        # 判断是否增加生命
        if score // self.reward_score != self.score // self.reward_score:
            self.lives_count += 1
            self.show_lives()

        self.score = score

        # 更新最好成绩
        self.best_score = score if score > self.best_score else self.best_score

        # 计算最新关卡等级
        if score < self.level2_score:
            level = 1
        elif score < self.level3_score:
            level = 2
        else:
            level = 3

        is_upgrade = level != self.level
        self.level = level

        # 更新得分的精灵显示内容
        self.score_label.set_text('%d' % score)
        self.score_label.rect.midleft = (self.status_sprite.rect.right + self.margin,
                                         self.status_sprite.rect.centery)

        # 返回是否升级给游戏主逻辑
        return is_upgrade
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

3.3.5 最好成绩的文件读写

将历史最好历史成绩保存到record.txt文件中
(1)在游戏退出前,调用指示器面板的save_best_score方法,将最好成绩写入到record.txt文件中
(2)在指示器面板的初始化方法中调用load_best_score方法,从record.txt文件中读取最好成绩

  1. 保存最好成绩
    (1)在HUDPanel类的初始化方法上方,定义类属性指定保存最好成绩的文件名,代码如下:
	record_filename = "record.txt"  # 最好成绩文件名
  • 1

(2)在HUDPanel类中定义save_best_score方法:代码如下:

 def save_best_score(self):
        """保存当前最好得分到文件里"""
        file = open(self.record_filename, 'w')
        file.write('%d' % self.best_score)
        file.close()
  • 1
  • 2
  • 3
  • 4
  • 5

在game.py中的start方法中添加退出游戏之前保存最好成绩事件

while True:
        # 判断英雄是否已经死亡
        self.is_game_over = self.hud_panel.lives_count == 0

        # 处理事件监听
        if self.event_handler():
            # event.handler 返回True则说明发生了退出事件

            # 退出游戏之前要保存最好的成绩
            self.hud_panel.save_best_score()

            return
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

(3)在HUDPanel类中定义load_best_score方法:代码如下

 def load_best_score(self):
        """从文件里重新加载最好得分"""
        try:
            # 读取文件内容
            file = open(self.record_filename, 'r')
            content = file.read()
            file.close()

            # 转换内容为数字
            self.best_score = int(content)
        except (FileNotFoundError, ValueError):
            print('读取最高得分文件,发生异常')
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

在HUDPanel类中的初始化方法末尾添加加载最好成绩:代码如下

# 从文件里加载最好成绩
        self.load_best_score()
        print('初始化控制面板,当前最好得分是:', self.best_score)
  • 1
  • 2
  • 3

3.3.6 游戏状态变化

(1)面板暂停:在游戏暂停或结束时,在游戏窗口中央位置显示提示信息,而面板中的其他数据不会再发生变化
(2)面板恢复:在游戏执行期间,不显示中央位置的提示信息,面板中其他提示标签的数据,会随者游戏的推进而变化

  1. 精灵组的绘制顺序,先创建指示器面板,再创建英雄精灵,代码如下:
	# 游戏精灵
    # 创建背景精灵
    self.all_group.add(Background(False), Background(True))

    # 创建游戏控制面板
    self.hud_panel = HUDPanel(self.all_group)

    # 创建英雄飞机精灵
   	hero = GameSprite('me1.png', 0, self.all_group)
	hero.rect.center = SCREEN_RECT.center      # 显示在屏幕中央
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  1. 暂停和恢复面板
    (1)在game_hud.py中的HUDPanel类中定义panel_pause方法,代码如下:
def panel_paused(self, is_game_over, display_group):
    """游戏停止,显示提示信息。is_game_over为 True 则说明游戏结束,为 False 则说明游戏暂停"""
    # 判断是否已经显示了提示信息
    if display_group.has(self.best_label, self.status_label, self.tip_label):
        return

    # 根据游戏状态生成提示文本
    status = 'Game Over!' if is_game_over else 'Game Paused!'
    tip = 'Press spaceBar to '
    tip += 'play again.' if is_game_over else 'continue.'

    # 修改标签精灵的文本内容
    self.best_label.set_text('Best:%d' % self.best_score)
    self.status_label.set_text(status)
    self.tip_label.set_text(tip)

    # 修正标签精灵的位置
    self.best_label.rect.center = SCREEN_RECT.center
    self.status_label.rect.midbottom = (self.best_label.rect.centerx,
                                        self.best_label.rect.y - 2 * self.margin)
    self.tip_label.rect.midtop = (self.best_label.rect.centerx,
                                  self.best_label.rect.bottom + 8 * self.margin)

    # 把标签精灵添加到精灵组
    display_group.add(self.best_label, self.status_label, self.tip_label)

    # 修改状态按钮
    self.status_sprite.switch_status(True)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

(2)在game_hud.py中的HUDPanel类中定义panel_resume方法,代码如下:

def panel_resume(self, display_group):
    """取消停止状态,隐藏提示信息"""
    display_group.remove(self.best_label, self.status_label, self.tip_label)

    # 修改状态按钮
    self.status_sprite.switch_status(False)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

(3)在game.py中的start方法中调用,代码如下;

def start(self):
	···
	while True:
		···
		# 根据游戏转态切换界面显示的内容
        if self.is_game_over:
            # print('游戏已经结束,按空格键重新开始游戏...')
            self.hud_panel.panel_paused(True, self.all_group)
        elif self.is_game_pause:
            # print('游戏已经暂停,按空格键继续...')
            self.hud_panel.panel_paused(False, self.all_group)
        else:
            # print('游戏进行中...')
            # self.all_group.update()
            self.hud_panel.panel_resume(self.all_group)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

3.3.7 游戏结束后的重置面板

(1)在game.py中game循环中添加判断游戏是否结束的代码,如下:

		while True:
            # 判断英雄是否已经死亡
            self.is_game_over = self.hud_panel.lives_count == 0

            # 处理事件监听
            if self.event_handler():
                # event.handler 返回True则说明发生了退出事件

                # 退出游戏之前要保存最好的成绩
                self.hud_panel.save_best_score()

                return
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

(2)在game_hud.py中的HUDPanel类中定义reset_panel方法,代码如下:

def reset_panel(self):
        """重置面板数据"""
        # 重置数据
        self.score = 0
        self.lives_count = 3

        # 重置精灵数据
        self.increase_score(0)
        self.show_bomb(3)
        self.show_lives()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

(3)在game.py的reset_game重新游戏方法中调用,代码如下:

def reset_game(self):
    """重置游戏数据"""
    self.is_game_over = False
    self.is_game_pause = False

    # 重置面板
    self.hud_panel.reset_panel()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

3.4 逐帧动画的基本实现

3.4.1 派生飞机类

在game_items.py模块中从GameSprite类中派生一个Plane类,代码如下:

class Plane(GameSprite):
    """飞机精灵类"""

    def __init__(self, normal_names, speed, hp, value, wav_name, hurt_name, destroy_names, *group):
        """飞机类的初始化"""
        super(Plane, self).__init__(normal_names[0], speed, *group)
        # 飞机基本属性
        self.hp = hp  # 当前生命值
        self.max_hp = hp  # 初始生命值
        self.value = value  # 分值
        self.wav_name = wav_name  # 音效名

        # 正常状态图像列表
        self.normal_images = [pygame.image.load(self.res_path + name) for name in normal_names]
        self.normal_index = 0  # 正常状态图像索引
        self.hurt_image = pygame.image.load(self.res_path + hurt_name)  # 受伤图像
        # 摧毁状态图像列表
        self.destroy_images = [pygame.image.load(self.res_path + name) for name in destroy_names]
        self.destroy_index = 0  # 摧毁状态索引

    def reset_plane(self):
        """重置飞机"""
        self.hp = self.max_hp

        self.normal_index = 0
        self.destroy_index = 0

        self.image = self.normal_images[0]

    def update(self, *args):
        """更新状态,准备下一次要显示的内容"""
        # 判断是否要更新
        if not args[0]:
            return

        if self.hp == self.max_hp:
            # 切换要显示的图片
            self.image = self.normal_images[self.normal_index]

            # 计算下一次显示的索引
            count = len(self.normal_images)
            self.normal_index = (self.normal_index + 1) % count

        elif self.hp > 0:
            # 受伤
            self.image = self.hurt_image

        else:
            # 死亡
            if self.destroy_index < len(self.destroy_images):
                self.image = self.destroy_images[self.destroy_index]

                self.destroy_index += 1
            else:
                self.reset_plane()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55

3.4.2 设置逐帧动画频率

由于游戏循环的刷新帧率设置成了60帧/秒,导致玩家飞机喷射的动画效果太快,设置计数器监听,运行循环每执行10次才更换一次图像,每秒更换6次图像,从而减低逐帧动画的频率
(1)在game_items.py模块中的顶部定义一个全局变量,记录逐帧动画更新的间隔帧数,代码如下:

FRAME_INTERVAL = 10  # 逐帧动画间隔帧数
  • 1

(2)在game.py模块的start方法中添加以下代码:

 def start(self):
        """开启游戏主逻辑"""
        # 创建时钟
        clock = pygame.time.Clock()

        # 动画帧数计数器
        frame_count = 0
		while True:
			···
	        else:
              # print('游戏进行中...')
               	self.hud_panel.panel_resume(self.all_group) 
                frame_count = (frame_count + 1) % FRAME_INTERVAL
                self.all_group.update(frame_count == 0)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

##3.5 飞机类的设计与实现

3.5.1 派生敌机类

  1. 定义敌机类
    在game_items.py从Plane类中派生Enemy类,代码如下:
class Enemy(Plane):
    """敌人飞机"""

    def __init__(self, kind, max_speed, *groups):
        """初始化敌人飞机"""
        self.kind = kind
        self.max_speed = max_speed

        if kind == 0:
            # 小敌机
            super(Enemy, self).__init__(['enemy1.png'], 1, 1, 1000,
                                        'enemy1_down.wav', 'enemy1.png',
                                        ['enemy1_down%d.png' % i for i in range(1, 5)],
                                        *groups)
        elif kind == 1:
            # 中敌机
            super(Enemy, self).__init__(['enemy2.png'], 1, 6, 6000,
                                        'enemy2_down.wav', 'enemy2_hit.png',
                                        ['enemy2_down%d.png' % i for i in range(1, 5)],
                                        *groups)

        elif kind == 2:
            # 大敌机
            super(Enemy, self).__init__(['enemy3_n1.png', 'enemy3_n2.png'], 1, 15, 15000,
                                        'enemy3_down.wav', 'enemy3_hit.png',
                                        ['enemy3_down%d.png' % i for i in range(1, 7)],
                                        *groups)

        # 初始化飞机时,要让飞机随机的选择一个位置显示
        self.reset_plane()

    def reset_plane(self):
        """重置敌人飞机"""
        super(Enemy, self).reset_plane()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  1. 创建敌机精灵
    在game.py的Game类中定义创建敌机精灵的方法,代码如下:
    def create_enemies(self):
        """创建敌机"""
        count = len(self.enemies_group.sprites())
        groups = (self.all_group, self.enemies_group)

        # 根据不同的关卡创建不同数量的敌机
        if self.hud_panel.level == 1 and count == 0:
            # 关卡1
            for i in range(16):
                Enemy(0, 3, *groups)

        elif self.hud_panel.level == 2 and count == 16:
            # 关卡2
            for enemy in self.enemies_group.sprites():
                enemy.max_speed = 5

            for i in range(8):
                Enemy(0, 5, *groups)
            for i in range(2):
                Enemy(1, 1, *groups)

        elif self.hud_panel.level == 3 and count == 26:
            # 关卡3
            for enemy in self.enemies_group.sprites():
                enemy.max_speed = 7 if enemy.kind == 0 else 3

            for i in range(8):
                Enemy(0, 7, *groups)
            for i in range(2):
                Enemy(1, 3, *groups)
            for i in range(2):
                Enemy(2, 1, *groups)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

在game.py模块的初始化方法初始化敌机,代码如下:

  # 初始化敌机
    self.create_enemies()
  • 1
  • 2
  1. 设置敌机精灵随机位置和敌机飞行
    在屏幕上方再取一个屏幕的高度,敌机随机位置的y值为 屏幕高度 – 敌机高度的范围内,
    x值为 屏幕宽度 – 敌机宽度的范围,这个范围内随机位置向屏幕下方移动
    即:
    水平方向取一个0~(SCREEN_RECT.w – self.rect.w)之间的随机数,设置给敌机矩形区域的x
    垂直方向取一个0~(SCREEN_RECT.h – self.rect.h)之间的随机数,再减去游戏窗口的高度,然后设置给敌机矩形区域的y
    (1)对Enemy类的reset_plane方法做调整,代码如下:
 def reset_plane(self):
        """重置敌人飞机"""
        super(Enemy, self).reset_plane()

        # 敌人飞机的数据重置
        x = random.randint(0, SCREEN_RECT.w - self.rect.w)
        y = random.randint(0, SCREEN_RECT.h - self.rect.h) - SCREEN_RECT.h

        self.rect.topleft = (x, y)

        # 重置飞机的飞行速度
        self.speed = random.randint(1, self.max_speed)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

(2)修改Enemy类的update方法,代码如下:

def update(self, *args):
    """更新飞机的位置信息"""
    super(Enemy, self).update(*args)

    # 根据血量判断是否还要移动
    if self.hp > 0:
        self.rect.y += self.speed

    # 如果移动后已经到达屏幕外,则需要重置飞机
    if self.rect.y >= SCREEN_RECT.h:
        self.reset_plane()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

3.5.2 派生英雄飞机类

  1. 创建英雄飞机类
    (1)在game_items.py模块的顶部定义全局常量记录英雄默认炸弹数量和英雄飞机默认位置
HERO_BOMB_COUNT = 3  # 英雄默认炸弹数量
HERO_DEFAULT_MID_BOTTOM = (SCREEN_RECT.centerx,
                           SCREEN_RECT.bottom - 90)  # 英雄默认初始位置
  • 1
  • 2
  • 3

(2)在game_items.py模块中从Plane类派生一个Hero类,代码如下:

class Hero(Plane):
    """英雄飞机"""

    def __init__(self, *groups):
        """初始化英雄飞机"""
        self.is_power = False  # 是否无敌
        self.bomb_count = HERO_BOMB_COUNT  # 炸弹数量
        self.bullets_kind = 0  # 子弹类型
        self.bullets_group = pygame.sprite.Group()  # 子弹精灵组

        super(Hero, self).__init__(('me1.png', 'me2.png'),
                                   5, 1, 0, 'me_down.wav', 'me1.png',
                                   ['me_destroy_%d.png' % x for x in range(1, 5)],
                                   *groups)

        self.rect.midbottom = HERO_DEFAULT_MID_BOTTOM  # 创建好飞机之后要设置飞机位置为屏幕底部中间
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

(3)在game.py模块中的初始化方法修改创建英雄飞机精灵和修改显示炸弹数量

  # 创建英雄飞机精灵
    self.hero_sprite = Hero(self.all_group)
    self.hud_panel.show_bomb(self.hero_sprite.bomb_count)
  • 1
  • 2
  • 3
  1. 移动英雄飞机
    按照pygame坐标系的设定 ---- x轴沿水平方向向右,逐渐增加。右键 – 左键的值作为英雄水平移动的计数,再用基数乘以英雄飞机的速度,得到飞机在水平方向移动的距离,如表9所示:

表9 水平移动基数表

按下右键 按下左键 右键-左键 移动方向
0 0 0 不移动
1 0 1 向右
0 -1 -1 向左
1 1 0 不移动

同理,得垂直移动基数表如表10所示

表10 垂直移动基数表

按下上键 按下下键 上键-下键 移动方向
0 0 0 不移动
1 0 1 向上
0 -1 -1 向下
1 1 0 不移动

(1)在game.py模块中的start方法中修改代码:

···
	while True:
		···
		 else:
                # print('游戏进行中...')
                self.hud_panel.panel_resume(self.all_group)

                # 处理长按事件
                keys = pygame.key.get_pressed()  # get_pressed()得到一个元组,每一个按键在元组里都有一个下标,对应下标值为1则说明按键被按下
                move_hor = keys[pygame.K_RIGHT] - keys[pygame.K_LEFT]  # 水平的移动基数
                move_ver = keys[pygame.K_DOWN] - keys[pygame.K_UP]  # 垂直的移动基数

                # 检测碰撞
                self.check_collide()

                frame_count = (frame_count + 1) % FRAME_INTERVAL
                self.all_group.update(frame_count == 0, move_hor, move_ver)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

(2)在game_items.py模块中英雄飞机类增加update方法,代码如下:

def update(self, *args):
    """
    *args的0号下标说明是否要更新下一帧动画,1号说明玩家飞机水平方向移动基数,2号说明玩家飞机垂直方向移动基数
    """
    super(Hero, self).update(*args)

    if len(args) != 3 or self.hp <= 0:
        return

    # 屏幕边缘的位置修正
    self.rect.x += args[1] * self.speed
    self.rect.x = 0 if self.rect.x < 0 else self.rect.x
    if self.rect.right > SCREEN_RECT.right:
        self.rect.right = SCREEN_RECT.right

    self.rect.y += args[2] * self.speed
    self.rect.y = 0 if self.rect.y < 0 else self.rect.y
    if self.rect.bottom > SCREEN_RECT.bottom:
        self.rect.bottom = SCREEN_RECT.bottom
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  1. 炸毁游戏窗口内部的敌机
    (1)在game_items.py模块的Hero类中定义blowup方法,将出现在游戏窗口内的敌机全部引爆,同时计算并返回得分
def blowup(self, enemies_group):
    """炸毁所有敌机,并返回得到的总分值"""
    # 判断是否能够发起引爆
    if self.bomb_count <= 0 or self.hp <= 0:
        return 0

    # 引爆所有敌机,累计得分
    self.bomb_count -= 1
    score = 0
    count = 0

    for enemy in enemies_group.sprites():
        if enemy.rect.bottom > 0:
            score += enemy.value
            enemy.hp = 0
            count += 1

    print('炸毁了%d架敌机,获取得分%d' % (count, score))
    return score
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

(2)在event_handler方法中,监听到玩家按下字母B时,让英雄飞机调用blowup方法,并使用返回的分数
  A. 更新游戏得分
  B. 更新炸弹数量显示
  C. 判断是否升级,如果是,调用create_enemies方法创建更多敌机

def event_handler(self):
        """获取并处理事件"""
        for event in pygame.event.get():
        	···
			# 必须在游戏结束也没暂停才能执行的操作
            if not self.is_game_over and not self.is_game_pause:
                if event.type == pygame.KEYDOWN and event.key == pygame.K_b:
                    # 释放一颗炸弹,并修改炸弹数量
					 score = self.hero_sprite.blowup(self.enemies_group)
                    self.hud_panel.show_bomb(self.hero_sprite.bomb_count)
                    if self.hud_panel.increase_score(score):
                        self.create_enemies()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

3.6 碰撞检测

3.6.1 创建碰撞检测

(1)在game.py中创建check_collide方法,代码如下:

def check_collide(self):
        """检查是否有碰撞"""

        if not self.hero_sprite.is_power:
            collide_enemies = pygame.sprite.spritecollide(self.hero_sprite, self.enemies_group,
                                                          False, pygame.sprite.collide_mask)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

(2)在game_items.py模块中的GameSprite类的初始化方法中生成描边,代码如下:

# 生成遮罩属性,提高碰撞检测的执行效率
        self.mask = pygame.mask.from_surface(self.image)		
  • 1
  • 2

(3)在game.py模块中的循环调用碰撞方法,代码如下:

def start(self):
		···
		while True:
			···
			else:
                # print('游戏进行中...')
                self.hud_panel.panel_resume(self.all_group) 
                # 检测碰撞
                self.check_collide()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

3.6.2 敌机撞毁英雄飞机

英雄飞机被撞毁,播放被撞毁动画,动画播放完成后,游戏窗口右下角的英雄命数相应减少
(1)如果还有剩余命数,那么英雄飞机会在牺牲的位置出现新的英雄飞机进行战斗,期间会有3秒的无敌时间
(2)如果没有剩余命数,则游戏结束
撞毁英雄飞机的敌机,同样要播放被撞毁动画
(1)动画播放过程中,敌机在屏幕上位置不会移动
(2)动画播放完成后,敌机会被设置回初始状态,再次从游戏窗口上方飞出加入战斗

  1. 英雄飞机被撞毁
    (1)修改check_collide方法中的代码:
def check_collide(self):
    """检查是否有碰撞"""

    if not self.hero_sprite.is_power:
        collide_enemies = pygame.sprite.spritecollide(self.hero_sprite, self.enemies_group,
                                                      False, pygame.sprite.collide_mask)

		# 过滤掉已经被摧毁的敌机
        collide_enemies = list(filter(lambda x: x.hp > 0, collide_enemies))

        # 撞毁玩家飞机
        if collide_enemies:
            self.hero_sprite.hp = 0

        # 撞毁敌人飞机
        for enemy in collide_enemies:
            enemy.hp = 0
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  1. 发布自定义事件重置英雄飞机
    (1)在game_items.py模块的顶部定义全局常量记录英雄牺牲事件代号,代码如下:
HERO_DEAD_EVENT = pygame.USEREVENT              # 英雄牺牲事件
HERO_POWER_OFF_EVENT = pygame.USEREVENT + 1     # 取消英雄无敌事件
  • 1
  • 2

(2)重新Hero类中的reset_plane方法,代码如下:

def reset_plane(self):
    """重置玩家飞机数据"""
    super(Hero, self).reset_plane()

    self.is_power = True
    self.bomb_count = HERO_BOMB_COUNT
    self.bullets_kind = 0

    # 发布事件,让游戏主逻辑更新界面
    pygame.event.post(pygame.event.Event(HERO_DEAD_EVENT))

    # 发布定时事件
    pygame.time.set_timer(HERO_POWER_OFF_EVENT, 3000)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

(3)在game.py模块的event_handler方法中,当游戏未结束也没暂停时判断事件,代码如下:

for event in pygame.event.get():
	···
	# 必须在游戏未结束也没暂停才能执行的操作
    if not self.is_game_over and not self.is_game_pause: 
    	··· 
    	elif event.type == HERO_DEAD_EVENT:
            # 玩家飞机死亡
            self.hud_panel.lives_count -= 1
            self.hud_panel.show_lives()

            self.hud_panel.show_bomb(self.hero_sprite.bomb_count)

        elif event.type == HERO_POWER_OFF_EVENT:
            self.hero_sprite.is_power = False
            pygame.time.set_timer(HERO_POWER_OFF_EVENT, 0)  # 设置定时器延时为0,可以取消定时器
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

玩家飞机被摧毁,重置英雄飞机位置,在game.py模块的reset_game方法中,代码如下:

def reset_game(self):
	# 重置英雄飞机位置
    self.hero_sprite.rect.midbottom = HERO_DEFAULT_MID_BOTTOM
  • 1
  • 2
  • 3

3.6.3 英雄飞机发射子弹

(1)英雄出场后,每隔0.2秒,会自动连续发射3颗子弹,其中:
  A. 子弹会从英雄飞机头部的正上方发射,沿垂直方向向游戏窗口的上方飞行
  B. 如果子弹飞出了游戏窗口,中途没有击中任何一架敌机,子弹会被销毁
  C. 如果子弹飞行途中,击中了敌机,会对敌机造成伤害,敌机的生命值减去子弹的伤害度
(2)如果英雄飞机拾取到子弹增强道具,发射的子弹会改为双排,并且持续的时长为20秒

  1. 设计子弹类
    在game_items.py模块从GameSprite类派生一个Bulle类,代码如下:
class Bullet(GameSprite):
    """子弹类"""
   def __init__(self, kind, *group):
        """初始化子弹数据"""
        image_name = 'bullet1.png' if kind == 0 else 'bullet2.png'
        super(Bullet, self).__init__(image_name, -12, *group)

        self.damage = 1  # 杀伤力

    def update(self, *args):
        """更新子弹的数据"""
        super(Bullet, self).update(*args)

        # 飞出屏幕之外则需要销毁子弹
        if self.rect.bottom < 0:
            self.kill()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  1. 英雄飞机发射子弹
    在Hero类中定义fire方法,并利用定时器事件,实现英雄发射子弹的功能
    (1)在game_items.py模块的顶部定义英雄飞机发射子弹事件的全局常量,代码如下:
HERO_FIRE_EVENT = pygame.USEREVENT + 2          # 英雄发射子弹事件
  • 1

   (2)在Hero类的初始化方法中的末尾,设置英雄发射子弹的定时器事件,代码如下:

def __init__(self, *groups):
	pygame.time.set_timer(HERO_FIRE_EVENT, 200)  # 创建玩家飞机之后每0.2秒激活一次发射子弹事件
  • 1
  • 2

   (3)在Hero类中定义fire方法并根据bullet_kind属性发射子弹,子弹之间间距为15

def fire(self, display_group):
        """"英雄飞机发射一轮新的子弹"""
        # 准备子弹要显示到的组
        groups = (display_group, self.bullets_group)

        # 创建新子弹并定位
        for i in range(3):
            bullet1 = Bullet(self.bullets_kind, *groups)
            y = self.rect.y - i * 15
			# 判断子弹类型
            if self.bullets_kind == 0:
                bullet1.rect.midbottom = (self.rect.centerx, y)
            else:
                bullet1.rect.midbottom = (self.rect.centerx - 20, y)
				
				# 再创建一颗子弹
                bullet2 = Bullet(self.bullets_kind, *groups)
                bullet2.rect.midbottom = (self.rect.centerx + 20, y)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

(4)在game.py模块的event_handler方法中监听到英雄飞机发射子弹定时器事件后,让英雄飞机调用发射子弹方法

# 必须在游戏未结束也没暂停才能执行的操作
if not self.is_game_over and not self.is_game_pause:
···
elif event.type == HERO_FIRE_EVENT:
        # 英雄飞机发射子弹定时事件
        	self.hero_sprite.fire(self.all_group)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  1. 子弹击中敌机
    在Game类中check_collide方法中实现子弹击中并摧毁敌机的功能
    子弹碰撞敌机的流程如图6所示:

在这里插入图片描述

图6 子弹碰撞敌机流程图
(1)在Game类check_collide方法的末尾增加子弹击中敌机的处理,代码如下:

			# 子弹和敌机的碰撞分析
            hit_enemies=pygame.sprite.groupcollide(self.enemies_group, self.hero_sprite.bullets_group, False, False, pygame.sprite.collide_mask)

            for enemy in hit_enemies:
                # 已经被摧毁的飞机不需要再处理
                if enemy.hp <= 0:
                    continue

                for bullet in hit_enemies[enemy]:
                    bullet.kill()  # 销毁子弹
                    enemy.hp -= bullet.damage  # 修改敌机生命值

                    if enemy.hp > 0:
                        continue  # 如果敌机没有被摧毁,则继续遍历下一个子弹

                    # 当前这一颗子弹已经把敌机摧毁
                    if self.hud_panel.increase_score(enemy.value):
                        self.create_enemies()

                    # 这个飞机已经被摧毁,不需要再遍历下一颗子弹了
                    break
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

(2)游戏的重置,销毁敌机和子弹,在Game类的reset_game方法中末尾重置,代码如下:

# 销毁所有的敌人飞机
        for enemy in self.enemies_group:
            enemy.kill()

        # 销毁所有子弹
        for bullet in self.hero_sprite.bullets_group:
            bullet.kill()

        # 重新创建飞机
        self.create_enemies()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

3.6.4 英雄飞机拾取道具

游戏过程中,每隔30秒会从游戏窗口上方随机位置向下飞出游戏道具

  1. 创建游戏道具
    在game_items.py中定义从GameSprite类继承的Supply类,代码如下:
class Supply(GameSprite):
    """道具精灵"""

    def __init__(self, kind, *group):
        """初始化道具属性"""
        image_name = 'bomb_supply.png' if kind == 0 else 'bullet_supply.png'
        super(Supply, self).__init__(image_name, 5, *group)

        self.kind = kind  # 道具类型
        self.wav_name = 'get_bomb.wav' if kind == 0 else 'get_bullet.wav'   # 道具的音效

        self.rect.bottom = SCREEN_RECT.h    # 道具的初始位置

    def update(self, *args):
        """修改道具位置"""
        if self.rect.y > SCREEN_RECT.h:   # 如果已经移动到屏幕之外则不需要继续移动
            return

        super(Supply, self).update(*args)

    def throw_supply(self):
        """投放道具"""
        self.rect.bottom = 0    # 移动道具到窗口顶部
        self.rect.x = random.randint(0, SCREEN_RECT.w - self.rect.w)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  1. 定时投放道具
    (1)在game_items.py模块的顶部定义全局常量记录投放道具的定时器事件,代码如下:
THROW_SUPPLY_EVENT = pygame.USEREVENT + 3        # 投放道具事件
  • 1

(2)在game.py的Game类中实现create_supplies方法

def create_supply(self):
    """初始化两个道具,并开启投放道具的定时器"""
    Supply(0, self.all_group, self.supplies_group)
    Supply(1, self.all_group, self.supplies_group)

    pygame.time.set_timer(THROW_SUPPLY_EVENT, 30000)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

在初始化方法的末尾调用

# 初始化道具
self.create_supply()
  • 1
  • 2

(3)在事件监听中监听到投放道具定时器事件后,让随机道具调用投放道具方法

elif event.type == THROW_SUPPLY_EVENT:
            # 随机投放一个道具
            supply = random.choice(self.supplies_group.sprites())
           	supply.throw_supply()
  • 1
  • 2
  • 3
  • 4
  1. 英雄飞机拾取道具
    在Game类的check_collide方法扩展实现英雄飞机拾取道具的功能
    (1)在game_items.py模块的顶部定义全局常量记录关闭子弹增强的定时器事件,代码如下:
BULLET_ENHANCED_OFF_EVENT = pygame.USEREVENT + 4    # 关闭子弹增强事件
  • 1

(2)在Game类check_collide方法的末尾增加英雄飞机拾取道具的处理,代码如下:

# 检查英雄飞机和道具的碰撞
    supplies = pygame.sprite.spritecollide(self.hero_sprite, self.supplies_group,
                                           False, pygame.sprite.collide_mask)

    if supplies:
        supply = supplies[0]
        self.player.play_sound(supply.wav_name)  # 根据道具类型播放捡道具的音效

        # 根据道具类型产生不同的行为
        if supply.kind == 0:
            self.hero_sprite.bomb_count += 1
            self.hud_panel.show_bomb(self.hero_sprite.bomb_count)

        else:
            self.hero_sprite.bullets_kind = 1  # 修改英雄子弹为双排子弹
            pygame.time.set_timer(BULLET_ENHANCED_OFF_EVENT, 20000)  # 20秒之后重新变为单排子弹

            # 移动道具到屏幕之下
            supply.rect.y = SCREEN_RECT.h
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

(3)在event_handler方法中监听到关闭子弹增强的定时器事件

		elif event.type == BULLET_ENHANCED_OFF_EVENT:
            # 玩家使用双排子弹的时间已经结束,需要恢复为单排子弹
            self.hero_sprite.bullets_kind = 0  # 恢复为单排子弹
            pygame.time.set_timer(BULLET_ENHANCED_OFF_EVENT, 0)  # 取消定时
  • 1
  • 2
  • 3
  • 4

3.7 音乐与音效

3.7.1 加载和播放背景音乐

(1)在game_music模块中定义MusicPlayer类,代码如下:

    """游戏音乐控制模块"""
import pygame
import os


class MusicPlayer(object):
    """音乐播放类"""

    res_path = './res/sound/'

    def __init__(self, music_file):
        """初始化音乐播放器"""
        # 加载背景音乐
        pygame.mixer.music.load(self.res_path + music_file)
        pygame.mixer.music.set_volume(0.1)

        # 初始化音效的字典
        self.sound_dict = {}

        files = os.listdir(self.res_path)
        for file in files:
            if file == music_file:      # 背景音乐不需要处理
                continue

            sound = pygame.mixer.Sound(self.res_path + file)
            self.sound_dict[file] = sound

    @staticmethod
    def play_music():
        """播放背景音乐"""
        pygame.mixer.music.play(-1)

    @staticmethod
    def pause_music(is_pause):
        """根据暂停状态决定是否要播放背景音乐"""
        if is_pause:
            pygame.mixer.music.pause()
        else:
            pygame.mixer.music.unpause()

    def play_sound(self, wav_name):
        """根据文件名,播放音效"""
        self.sound_dict[wav_name].play()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43

(2)在Game类初始化方法中调用音乐类的背景音乐播放方法,代码如下:

  # 音乐播放器
    self.player = MusicPlayer('game_music.ogg')
    self.player.play_music()
  • 1
  • 2
  • 3

(3)在Game类的event_handler方法中切换背景音乐状态

	else:
   	# 游戏还没结束,切换暂停状态
       self.is_game_pause = not self.is_game_pause
       self.player.pause_music(self.is_game_pause)  # 切换背景音乐状态
  • 1
  • 2
  • 3
  • 4

3.7.1 加载和播放音效

(1)发射子弹
在event_handler方法中找到监听发射子弹事件的分支,代码如下:

 elif event.type == HERO_FIRE_EVENT:
        # 英雄飞机发射子弹定时事件
        self.player.play_sound('bullet.wav')
        self.hero_sprite.fire(self.all_group)
  • 1
  • 2
  • 3
  • 4

(2)引爆炸弹
在event_handler方法中找到监听玩家按下字母B引爆炸弹的分支,代码如下:

# 必须在游戏未结束也没暂停才能执行的操作
            if not self.is_game_over and not self.is_game_pause:
                if event.type == pygame.KEYDOWN and event.key == pygame.K_b:
                    # 释放一颗炸弹,并修改炸弹数量
                    if self.hero_sprite.hp > 0 and self.hero_sprite.bomb_count > 0:
                        self.player.play_sound('use_bomb.wav')
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

(3)投放和拾取道具
在event_handler方法中找到监听投放道具事件的分支,代码如下:

elif event.type == THROW_SUPPLY_EVENT:
        # 随机投放一个道具
        self.player.play_sound('supply.wav')
        supply = random.choice(self.supplies_group.sprites())
        supply.throw_supply()
  • 1
  • 2
  • 3
  • 4
  • 5

(4)敌机爆炸
在check_collide方法中,找到检测敌机被子弹击中部分的代码,调整代码如下:

		# 当前这一颗子弹已经把敌机摧毁
           if self.hud_panel.increase_score(enemy.value):
             	self.player.play_sound('upgrade.wav')
              	self.create_enemies()
  • 1
  • 2
  • 3
  • 4

(5)英雄飞机爆炸
在check_collide方法中,找到检测英雄飞机和敌机碰撞部分的代码,调整代码如下:

			# 撞毁玩家飞机
            if collide_enemies:
                self.player.play_sound(self.hero_sprite.wav_name)
                self.hero_sprite.hp = 0	
  • 1
  • 2
  • 3
  • 4

4.实验结果

(1)游戏开始界面,如图7所示

在这里插入图片描述

图7 游戏界面

(2)游戏结束界面,如图8所示:
在这里插入图片描述

图8 游戏结束

(3)获取道具,子弹增强,如图9所示:
在这里插入图片描述

图9 子弹增强

4)释放炸弹,炸毁屏幕所有敌机,如图10所示
在这里插入图片描述

图10 炸弹炸毁敌机

(5)掉落道具和生命次数的增加,如图11所示:
在这里插入图片描述

图11 掉落道具和生命次数增加

5.总结和展望

(1)飞机大战游戏的完成参考了网上的视频和资料,在学习过程中,明显暴露了自己在学习上的不足,对python的学习不够深入,对pygame的学习也仅仅是皮毛。
(2)飞机大战的功能简单,希望在以后深入学习的过程中继续完善游戏,给游戏增添一些更好玩的功能,例如敌机释放子弹、获取道具增加玩家飞机等功能。
(3)在学习的过程中,不但认识到python语言的强大和简洁,而且学习到pygame面向对象设计的思想,对以后的学习很有帮助。

;