飞机大战-面向对象-pygame

飞机大战

最近学习了python的面向对象,对面向对象的理解不是很深刻。

面向对象是数据和函数的'打包整理',将相关数据和处理数据的方法集中在一个地方,方便使用和管理。

本着学习的目的,在网上找了这个飞机大战游戏的素材和相关代码,自己研究学习,加深对面向对象的理解。

python可以做游戏,最基本的一个第三方模块就是pygame,借助pygame可以实现2D和3D游戏的开发。

对python开发游戏感兴趣的园友请参考官方文档:pygame.doc

下面就开始学习了解对象思想吧,顺便学学pygame,娱乐一下。

游戏需求

# 飞机大战
<1> 玩机通过键盘操作我方飞机,我方飞机自动发射初级子弹。
<2> 敌机分三种:小型敌机、中型敌机、大型敌机,区别:速度不同,数量不同,外形不同,血值不同。
<3> 小型敌机速度快,数量多,一颗子弹必杀;大型敌机和中型敌机速度慢,数量少,需要多可子弹才能消灭。
<4> 小型敌机分值低,大型敌机和中型敌机分值高。
<5> 统计玩机得分和最高分记录。
<6> 随着得分的增加,提升游戏等级,增加游戏的难度:增加敌机速度和数量
<7> 任意敌机和我方飞机碰撞,则玩家挑战失败一次;玩家有三次挑战机会。三次机会用完结束游戏。
<8> 我方飞机和敌机毁灭时,动画效果要实现
<9> 游戏中,每隔30s有一次随机空投补给:全屏炸弹或超级子弹。
<10> 游戏开始,玩家自带三颗全屏炸弹,按空格键触发,消灭屏幕内所有敌机;
<11> 使用一次全屏炸弹,数量减一,当玩家的全屏炸弹数少于3颗时,通过空投补给全屏炸弹,最多携带3颗全屏炸弹。
<12> 玩家领到超级子弹时,有18s的使用时间限制;超级子弹发射数量是普通子弹的两倍。
<13> 大型敌机和中型敌机,要显示血量,标识生命值,生命值结束,敌机摧毁,玩机得分。
<14> 玩家可以通过暂停按钮暂停和继续游戏
<15> 游戏有开始界面和结束是否继续的界面。

需求分析

# 角色1:我方飞机、敌机(小型敌机、中型敌机、大型敌机)、子弹、补给(全屏炸弹、超级子弹)
# 角色2:设置类、面板类、游戏状态类

- 我方飞机
	- 继承pygame.sprite.Sprite
	- 本地保存设置类对象和 屏幕对象
	- 数据属性:surface\destory_surface\rect\speed\active\invincible\mask
    - 功能属性:上下左右移动、绘制、重置位置

- 敌机(小、中、大)
	- 继承pygame.sprite.Sprite
    - 本地保存设置类对象和 屏幕对象
	- 数据属性:surface\destory_surface\rect\speed\active\hit\mask\energy
    - 功能属性:向下移动、绘制、血槽、重置位置

- 子弹(子弹1、子弹2)
	- 继承pygame.sprite.Sprite
    - 本地保存设置类对象和 屏幕对象
	- 数据属性:surface\rect\speed\active\mask
    - 功能属性:向上移动、绘制、重置位置

- 补给(全屏炸弹、超级子弹)
	- 继承pygame.sprite.Sprite
    - 本地保存设置类对象和 屏幕对象
	- 数据属性:surface\rect\speed\active\mask
    - 功能属性:向下移动、绘制、重置位置

- 设置类
	- 数据属性:游戏基本参数设置
    - 功能属性:游戏加载、暂停、统计得分、补给设置等
    
- 面板类
	- 本地保存设置类对象和 屏幕对象、游戏状态对象
	- 数据属性:分数面板数据、暂停面板数据、全屏炸弹面板数据、玩家生命个数面板数据、开启|结束|继续面板等
    - 功能属性:绘制各种面板数据
    
- 面板类
	- 本地保存设置类对象
	- 数据属性:game_active\game_paused\game_start\game_level\game_score_level

提取功能

提取游戏功能背后对应的pygame功能

  • 游戏背景图、背景音乐 - 显示、图片、音效功能
  • 绘制我方飞机、实现移动 - 事件监听功能
  • 绘制敌机
  • 我机-敌机碰撞检测 - 碰撞检测功能
  • 我机死亡后重置-5秒无敌模式 - 自定义时间事件
  • 增加普通子弹
  • 子弹-敌机碰撞检测
  • 绘制血槽能量值 - 几何图形功能
  • 得分统计面板 - 字体功能
  • 全屏炸弹面板功能
  • 游戏难度等级
  • 游戏暂停-继续 - 光标设置图像功能
  • 空投补给-全屏炸弹和超级子弹 - 自定义时间事件
  • 普通子弹和超级子弹的切换 - 自定义时间事件

运行环境

- win10
- python3.8
- pygame1.9.6
- pycharm2019.3

实现思路

  • 采用脚本的方式运行整个游戏,游戏运行在一个主函数内,在这个主函数内实例化各种对象。
  • 然后通过一个主循环,主循环内有两个功能:监测事件,更新对象
  • 监测事件,触发相应功能函数的执行,改变对象属性
  • 更新屏幕,功能1:根据游戏状态,更新对象位置并绘制;功能2:对象间碰撞判断(可以优化)
  • 每个对象的类,在不同的文件中实现
  • 所有图片素材、音效素材、字体素材单独文件夹管理存放
  • 增加设置类,保存游戏设置相关的参数和方法,在大多数对象内保存设置类对象,方便直接获取游戏相关参数
  • 增加面板类,分担设置类的压力;面板类负责管理游戏中各种面板按钮标题绘制时需要的数据和方法。
  • 增加游戏状态类,判断游戏是否处于:开始、活动、结束 等状态。

程序结构

脚本的方式

Aircraft-Battle/	# 游戏根目录
|-- fonts			# 字体文件夹
|-- images			# 图片文件夹
|-- sounds			# 音乐文件夹
|-- board.py		# 面板类
|-- bullet.py		# 子弹类
|-- enemy.py		# 敌机类
|-- game_functions.py		# 游戏方法,包括事件监测、屏幕更新等等
|-- game_status.py		# 游戏状态类
|-- main.py			# 游戏入口函数,主函数
|-- my_ship.py		# 我方飞机类
|-- settings.py		# 配置类
|-- supply.py		# 空投补给类
|-- recorded.txt        # 最高分记录
|-- readme.md

功能实现

飞机喷气动画

任何视频动画效果都是一帧一帧顺出来的,pygame也不例外。喷气效果就是两张图片不停的切换实现的。

游戏全局设置了一个刷新频率,clock.tick(60), 在这样一个速度下切换两张局部不同的图,肉眼很难分辨。

这就需要一个在不改变全局刷新频率,实现局部延迟的需求

  • 在全局设置一个时间延迟变量,每次刷新都减一,减到零后再从100开始累减。
  • 根据这个延迟变量,设置切换的频率,每隔几秒切换一次图片。
delay = 100
switch_image = True

while 1:
   delay -= 1
   if delay == 0:
       delay = 100

   if not (delay % 5):		# 每个5帧切换一次
           switch_image = not switch_image

   if switch_image:
       """开始切换"""
  • 类推:飞机催化时毁灭的动画效果、敌机毁灭的动画效果、击中敌机的动画效果都是采用这个策略实现的。

游戏暂停功能

游戏暂停功能其实很简单,所谓的暂停其实就是让对象们不要动起来,实现的方式就是不更新对象的位置。

可以通过一个检测事件,当玩家在暂停按钮处点击了鼠标左键,则将游戏状态参数paused设置为True,

根据这个参数,判断何时更新对象位置合适不更新对象位置。

一个小的注意点是,暂停时要将某些事件音效关闭。

判断鼠标在指定位置

比如,在游戏结束时,鼠标点击重新开始按钮,开始游戏。那如何判断鼠标在重新开始位置呢?

我们可以使用pygame.rect.collidepoint(event.pos)

elif event.type == MOUSEMOTION:
    if ab_board.pause_rect.collidepoint(event.pos):  # 鼠标在pause_rect内,返回True
        pass

碰撞检测功能

运动只是第一步,碰撞检测才是大多数游戏的灵魂。只有碰撞检测实现了,才有可能实现更过的业务逻辑。

敌机和子弹的碰撞、我机和敌机的碰撞,我们都借助pygame中的精灵类(Sprite)帮我们实现。

我们将所有敌机对象都放在一个大Group精灵组内,然后每种类型的敌机单独放在各自的精灵组内。

这样做碰撞检测时,我们只需要将我机对象和大Group精灵组判断是否碰撞,子弹和敌机也类似。

collide_obj = pygame.sprite.spritecollide(
    sprite, group, False, collide=pygame.sprite.collide_mask)

if collide_obj:
    pass
  • spritecollide 是一个精灵和一个精灵组的碰撞检测方法,返回一个列表,列表内是发生碰撞的精灵组成员

  • 参数False背后对应的参数是,是否将发生碰撞的精灵从精灵组内移出,False表示不移出。这里我们不移出,但会做一些逻辑判断,将这个精灵的active属性设为False,然后重置该静灵位置,不需要再次删除和创建。

  • 参数collide表示,指定碰撞检测机制,这里通过图像不透明部分的重叠来检测碰撞,这种碰撞检测机制需要被检测的对象有一个mask数据属性,self.mask = pygame.mask.from_surface(self.image)

# 补充:
# pygame的碰撞检测机制有很多
- 通过矩形  collide_rect(left, right),返回布尔值,判断矩形位置知否重叠,left和right是两个精灵
- 通过圆形	collide_circle(left, right),返回布尔值,判断圆心和半径,需要精灵有rect和radius属性
- 通过图像不透明面积 collide_mask(left, right) mask判断,需要精灵有rect和mask属性


# pygame的碰撞检测方式
- 一对多 spritecollide(sprite, group, dokill, collided=None),
		返回碰撞的精灵列表,存放group内被sprite碰撞到的精灵
- 多对多 groupcollide(groupa, groupb, dokilla, dokillb, collided=None),
		返回字典,key:groupa内发生碰撞的精灵,value:groupb内被key撞的精灵

- 一对多 spritecollideany(sprite, group, collided=None),
		返回一个group内找到的第一个被撞的精灵,如果没有返回None

控制玩家飞机移动

玩家移动飞机操作,可以交给pygame.event的事件监测实现,不过我们这里通过pygame.key实现的。

因为有这么一个'规则':比如键盘事件,偶然的事件采用event,频繁的事件采用key。

其实这样是有道理的,因为pygame.event在处理键盘事件时,内部采用key的一些方法。直接采用key更快一点。

def check_me_move(me, ab_state):
    """检测我方飞机移动事件"""
    if ab_state.game_active and not ab_state.game_paused:
        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()

定时功能

比如每隔30s一个空投,超级子弹18s使用时间,我机重生时5s无敌时间,都需要使用定时功能。

定时功能,可以通过pygame提供的自定义事件实现,pygame提供 USEREVENT,用户可以自定义自己的事件。

注意:每自定义一个事件,USEREVENT + 1

比如,我们自定义上述三个功能的自定义事件

import pygame
from pygame.locals import *

# 定义事件
SUPPLY_INTERVAL = USEREVENT
SUPPER_BULLET_TIME = USEREVENT + 1
INVINCIBLE_TIME = USEREVENT + 1

# 触发自定义时间事件
pygame.time.set_timer(SUPPLY_INTERVAL, 30 * 1000)  # 注意时间单位事毫秒
pygame.time.set_timer(SUPPER_BULLET_TIME, 18 * 1000) 
pygame.time.set_timer(INVINCIBLE_TIME, 5 * 1000) 

# 捕捉自定义事件, 终止事件
for event in pygame.event.get():
    if event.type == QUIT:
        sys.exit()
        
    elif event.type == SUPPLY_INTERVAL:
        pass
    
    elif event.type == SUPPER_BULLET_TIME:
        pygame.time.set_timer(SUPPER_BULLET_TIME, 0) 
        
   	elif event.type == INVINCIBLE_TIME:
       	pygame.time.set_timer(INVINCIBLE_TIME, 0) 

绘制血槽功能

绘制血槽,标识敌机的能量值,对于大型敌机和中型敌机,子弹击中只能减少敌机的一个生命值;

当敌机的生命值耗尽时,敌机毁灭,玩家得分。

可以通过敌机当前的生命值和初始生命值相除,得到一个比例,这个比例是敌机的当前血量百分比。

在敌机顶部一定位置画一条宽度为2的线,一条固定长度的黑线表示血槽,另一条可变长度的线表示剩余血量。

当血量百分比大于0.2时画绿线,否则显示红线。

此处画线使用:pygame.draw.line(Surface, color, start_pos, end_pos, width=1)

子弹位置更新

本游戏中,子弹的操作比较特殊。游戏开始先实例化普通子弹和超级子弹,存放在两个列表中。

普通子弹4颗,从飞机顶部发射;超级子弹8颗,从飞机两侧同时发射两颗。

每隔10帧,将子弹列表中的一个子弹位置重置,其他帧绘制更新子弹位置并绘制,效果就是子弹不停的射出。

def blit_bullet(ab_settings, bullets, me):
    # 每个10帧,重置一个子弹的位置
    if not (ab_settings.delay % 10):
        ab_settings.bullet_sound.play()
        if ab_settings.is_double_bullet:
            bullets[ab_settings.bullet2_index].reset((me.rect.centerx - 33, me.rect.centery))
            bullets[ab_settings.bullet2_index + 1].reset((me.rect.centerx + 30, me.rect.centery))
            ab_settings.bullet2_index = (ab_settings.bullet2_index + 2) % ab_settings.bullet2_num
        else:
            bullets[ab_settings.bullet1_index].reset((me.rect.centerx, me.rect.top - 5))
            ab_settings.bullet1_index = (ab_settings.bullet1_index + 1) % ab_settings.bullet1_num
    for bullet in bullets:
        bullet.move()
        bullet.blitme()

自定义鼠标图像

绘制心爱的图像,位置设为鼠标所在位置;再将鼠标隐藏。

pygame.mouse.set_visible(False)
mouse_image = pygame.image.load('images/check.png').convert_alpha()
mouse_rect = mouse_image.get_rect()
mouse_rect = pygame.mouse.get_pos()
screen.blit(mouse_image, mouse_rect)

补充

# 游戏图标,需要 .ico文件,32x32,不要convert_alpha(),一定要在set_mode()之前
# 背景图片一定要在set_mode()之后,即一定要在有了screen之后才能image.load()加载图片
# 鼠标显示设置关闭使用完毕后,记得还原设置。
# 相关的数据放在一块,比如面板的绘制操作,将image和rect尽可能在board.init中;绘制动作在board下的方法中
# 得分统计,千分位显示,使用 format(1234567, ',')  --> 1,234,567

总结

  • pygame游戏动画是一帧一帧刷出来的,图像在屏幕上绘制的先后顺序很重要。

  • 通过事件监测,对象位置更新、绘制屏幕,实现基本游戏动画的制作

  • 游戏采用面向对象的方式,实在太合适了;可扩展性极强,一个属性判断就可以扩展出很多功能。

  • pygame的事件监测和精灵类的非常好用,碰撞检测机制很有用

  • 面向对象,对象的属性和方法息息相关;本质还是面向过程,好处是数据使用起来更加方便灵活。

  • 有了对象,就有了对象的数据和方法,自己的能力也同时得到极大的提升。

程序源码

游戏源码和素材

fork my on Github

Update to [V3-优化子弹生成函数, 微调游戏等级]

代码量统计如下

猜你喜欢

转载自www.cnblogs.com/the3times/p/12818350.html