飞机大战

最近学习了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-优化子弹生成函数, 微调游戏等级]

代码量统计如下

飞机大战-面向对象-pygame的更多相关文章

  1. day 4 飞机大战-面向对象

    1.飞机类 #-*- coding:utf-8 -*- import pygame import time from pygame.locals import * class HeroPlane(ob ...

  2. 15 飞机大战:pygame入门、python基础串连

    0 pygame模块的导入 import pygame导入pygame包 使用pygame.init()导入pygame的所有模块.只有导入模块pygame才能使用. 使用pygame.quit()卸 ...

  3. python版飞机大战代码简易版

    # -*- coding:utf-8 -*- import pygame import sys from pygame.locals import * from pygame.font import ...

  4. python飞机大战

    '''新手刚学python,仿着老师敲的代码.1.敌方飞机只能左右徘徊(不会往下跑)并且不会发射子弹.2.正在研究怎么写计分.3.也参考了不少大佬的代码,但也仅仅只是参考了.加油!''' import ...

  5. 基于pygame实现飞机大战【面向过程】

    一.简介 pygame 顶级pygame包 pygame.init - 初始化所有导入的pygame模块 pygame.quit - uninitialize所有pygame模块 pygame.err ...

  6. 一、利用Python编写飞机大战游戏-面向对象设计思想

    相信大家看到过网上很多关于飞机大战的项目,但是对其中的模块方法,以及使用和游戏工作原理都不了解,看的也是一脸懵逼,根本看不下去.下面我做个详细讲解,在做此游戏需要用到pygame模块,所以这一章先进行 ...

  7. 飞机大战编写以及Java的面向对象总结

    面向对象课程完结即可编写一个简单的飞机大战程序.我觉得我需要总结一下 飞机大战中类的设计: 父类:FlyingObject(抽象类) 接口:Award .Enemy 子类:Hero.Bullet.Ai ...

  8. 用面向对象的编程方式实现飞机大战小游戏,java版

    概述 本文将使用java语言以面向对象的编程方式一步一步实现飞机大战这个小游戏 本篇文章仅供参考,如有写的不好的地方或者各位读者哪里没看懂可以在评论区给我留言 或者邮件8274551712@qq.co ...

  9. Python飞机大战实例有感——pygame如何实现“切歌”以及多曲重奏?

    目录 pygame如何实现"切歌"以及多曲重奏? 一.pygame实现切歌 初始化路径 尝试一 尝试二 尝试三 成功 总结 二.如何在python多线程顺序执行的情况下实现音乐和音 ...

随机推荐

  1. 从谷歌 GFS 架构设计聊开去

    伟人说:“人多力量大.” 尼古拉斯赵四说:“没有什么事,是一顿饭解决不了的!!!如果有,那就两顿.” 研发说:“需求太多,人手不够.” 专家说:“人手不够,那就协调资源,攒人头.” 释义:一人拾柴火不 ...

  2. 曹工说Redis源码(3)-- redis server 启动过程完整解析(中)

    文章导航 Redis源码系列的初衷,是帮助我们更好地理解Redis,更懂Redis,而怎么才能懂,光看是不够的,建议跟着下面的这一篇,把环境搭建起来,后续可以自己阅读源码,或者跟着我这边一起阅读.由于 ...

  3. 如何在Vue中优雅的使用防抖节流

    1. 什么是防抖节流 防抖:防止重复点击触发事件 首先啥是抖? 抖就是一哆嗦!原本点一下,现在点了3下!不知道老铁脑子是不是很有画面感!哈哈哈哈哈哈 典型应用就是防止用户多次重复点击请求数据. 代码实 ...

  4. 关于Tkinter的介绍

    Introduction to Tkinter 原英文教程地址zetcode.com In this part of the Tkinter tutorial, we introduce the Tk ...

  5. django实现自定义登陆验证

    django实现自定义登陆验证 自定义装饰器函数和类 from utils.http import HttpResponseUnauthorized from django.views import ...

  6. MTK Android 计算器Calculator输入暗码!77!+,启动工厂测试apk

    Android8.0 计算器Calculator输入暗码!77!+,启动工厂测试apk 路径: packages/apps/ExactCalculator/src/com/android/calcul ...

  7. vue技术栈进阶(02.路由详解—基础)

    路由详解(一)--基础: 1)router-link和router-view组件 2)路由配置 3)JS操作路由

  8. sparkRdd driver和excuter

    //1 从内存中创建makeRdd,底层实现就是parallelize val rdd=sc.makeRDD(Array(1,2,"df",55)) //2 从中创建paralle ...

  9. git获取特定的commit

    git reset --hard [commit_id]

  10. synchronized的锁是针对多线程的

    synchronized的锁是针对多线程的,从线程的角度去思考才能真正弄明白. Java的synchronized使用方法总结 1. 把synchronized当作函数修饰符时 这也就是同步方法,那这 ...