飞机大战
最近学习了 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 - 优化子弹生成函数, 微调游戏等级]
代码量统计如下
来源: https://www.cnblogs.com/the3times/p/12818350.html