Qt学习总结——飞机大战小游戏制作
1. 需求分析
这篇文章写于2020年暑假,完成学校实训项目之后,对自己的项目实践做了一个总结,回顾整个项目的制作过程,同时也复习一下Qt的相关知识,总结项目制作过程中出现的不足之处。
如果有同学想尝试使用Qt制作飞机大战的小游戏,这里推荐两个教程(https://www.write-bug.com/article/1451.html)(https://baijiahao.baidu.com/s?id=1658135336435450610)。
第一个教程使用的是QGraphicsScene作为窗体制作,但是我对QWidget更加熟悉,所以只借用了其中的图片素材,如果有机会我还是愿意尝试用QGraphicsScene写这个程序,一些界面美化方面可能能做得更好
接下来开始对项目进行分析,飞机大战需要做到的基本要求有以下几点:
1.我方飞机和敌方飞机的显示和移动
2.我方飞机和敌方飞机可以发射子弹
3.子弹与飞机,飞机与飞机发生碰撞,飞机要受到伤害
以上是飞机大战的基本要求,而我在设计项目的时候再添加了几点特点。
1.游戏可以选择关卡和难度
2.我方飞机可以释放技能
3.随机掉落资源包
4.使用数据库制作排行榜供记录得分。
2. 类的设计
我们先实现飞机大战的基本需求,再去考虑后继增加的特殊需求。
1.子弹类Bullet
#ifndef BULLET_H
#define BULLET_H
#include <QString>
#include <QPixmap>
class Bullet
{
public:
Bullet(); //构造函数
QPixmap bullet; //我方飞机子弹图片
QPixmap EnemyBullet //敌方普通飞机子弹图片
QPixmap EnemyBullet2; //敌方Boss飞机子弹图片
int speed; //子弹运动速度
int x;
int y;
void updatePosition(); //我方飞机子弹运动函数
void EnemyUpdatePosition(); //敌方飞机子弹运动函数
void EnemyUpdatePositionLeft();
void EnemyUpdatePositionRight();
bool isFree; //子弹是否在游戏界面中(空闲)
};
#endif // BULLET_H
在设计子弹类时,我觉得无论是敌机子弹还是我方飞机子弹,其共性是较多的,所以我就直接将两者合为一个类,如果以后需要添加其他种类飞机子弹,同样可以写进这个类中。因为只有子弹的图片和运动方向不同,所以我写了不同的update函数表示不同子弹的运动。isFree作为一个标志判断子弹是否空闲,即子弹是否在界面中,根据这个标志判断飞机发射哪颗子弹。
类的具体实现如下:`
Bullet::Bullet()
{
bullet.load(":/image/images/mybullet.png"); //子弹图片加载
EnemyBullet.load(":/image/images/enemybullet.png");
EnemyBullet2.load(":/image/images/bossbullet.png");
speed = 10; //设置子弹速度
isFree = true; //设置状态为空闲
}
void Bullet::updatePosition(){
//我方飞机子弹更新函数
if(isFree)
return; //如果子弹为空闲,直接返回,否则向上运动
y-=5;
if(y<0)
isFree = true; //子弹超出游戏界面,返回空闲状态
}
void Bullet::EnemyUpdatePosition(){
//敌方飞机子弹更新函数
if(isFree)
return;
y+=5;
}
void Bullet::EnemyUpdatePositionLeft(){
//子弹偏左运动
if(isFree)
return;
x-=2;
y+=5;
}
void Bullet::EnemyUpdatePositionRight(){
//子弹偏右运动
if(isFree)
return;
x+=2;
y+=5;
}
2.我方飞机类Plane
#ifndef PLANE_H
#define PLANE_H
#include <QPoint>
#include <QPixmap>
#include <bullet.h>
class Plane
{
public:
Bullet myBullet[30]; //我方飞机子弹弹匣
Plane();
void shoot(); //飞机射击函数
QPixmap myplane; //我方飞机图片
int x;
int y;
int recored; //飞机射击时间标记
int interval; //飞机射击间隔
int life; //生命值
double skill; //技能值
};
#endif // PLANE_H
我方飞机类的设计中,interval是飞机射击的间隔,recored是标记射击时间,比如初始recored=0;当recored = interval时才可以发射子弹,发射之后recored又要归零,重新叠加。并且interval是我方飞机独有的,为之后的技能设计做铺垫。飞机的弹匣myBullet[30]是一个子弹类的数组,每次发射子弹都是从数组中进行选择
类的具体实现如下
#include "plane.h"
#include <cstdlib>
#include <iostream>
#include <fstream>
Plane::Plane()
{
myplane.load(":/image/images/myplane.png");
x = (500-myplane.width())*0.5; //设置飞机初始化位置
y = 600-myplane.height();
recored = 0;
life = 10;
isPlayed = false;
skill = 20;
interval = 30;
}
void Plane::shoot(){
//飞机射击函数
recored++;
if(recored<interval) //如果标记时间小于间隔,直接返回
return;
recored = 0; //否则重置标记时间,并发射一颗子弹
for(int i = 0;i<30;i++){
//选择空闲子弹进行发射
if(myBullet[i].isFree){
myBullet[i].x = x+40; //设定子弹发射的位置
myBullet[i].y = y-10;
myBullet[i].isFree = false; //改变子弹空闲状态
break;
}
}
}
关于子弹位置的初始化不是直接在飞机的x,y位置下进行发射,因为飞机图片由长度宽度,对象坐标点并不在图片中心,所以需要调整使得子弹在我们需要的位置射出。
3.敌方普通飞机EnemyPlane1
#ifndef ENEMYPLANE1_H
#define ENEMYPLANE1_H
#include <QPixmap>
#include <bullet.h>
#include <bomb.h>
class EnemyPlane1
{
public:
Bomb bomb; //飞机爆炸效果
EnemyPlane1();
int x;
int y;
int speed; //飞机运动速度
bool isFree; //飞机是否在游戏界面中(空闲)
bool isDestroyed; //飞机是否被摧毁
QPixmap enemy1; //普通飞机图片
int recored; //射击时间间隔标志
Bullet enemy1Bullet[30]; //子弹数组
void shoot(); //射击函数
void updatePosition(); //位置更新函数
};
#endif // ENEMYPLANE1_H
普通敌机类的设计基本思路跟我方飞机大致相同,但是我方飞机的移动是由玩家操控,而敌方飞机是自己运动,所以需要设置updatePosition函数,同时由于设计关卡的原因,飞机被摧毁后不可再生并且需要产生爆炸效果,所以添加了isDerstroyed标志判断是否被摧毁。
类的具体实现如下:
#include "enemyplane1.h"
EnemyPlane1::EnemyPlane1()
{
enemy1.load(":/image/images/enemyplane.png");
x = 0;
y = 0;
isFree = true;
isDestroyed = false;
speed = 1.5;
recored = 0;
}
void EnemyPlane1::updatePosition(){
//敌方飞机运动函数
if(isFree) //如果为空闲飞机直接返回
return;
y+=speed; //否则向下运动
if(y>600)
isDestroyed = true; //超出游戏界面设为摧毁
}
void EnemyPlane1::shoot(){
recored++;
if(recored<50)
return;
recored = 0;
for(int i=0;i<30;i++){
if(enemy1Bullet[i].isFree){
enemy1Bullet[i].x = x+20;
enemy1Bullet[i].y = y+40;
enemy1Bullet[i].isFree = false;
break;
}
}
}
4.敌方Boss飞机EnemyPlane2
#ifndef ENEMYPLANE2_H
#define ENEMYPLANE2_H
#include <QPixmap>
#include <bullet.h>
#include <bomb.h>
class EnemyPlane2
{
public:
EnemyPlane2();
Bomb bomb;
int x;
int y;
int speed;
bool isDestroyed;
bool isFree;
int life; //Boss类飞机的生命值
QPixmap enemy2; //Boss类飞机图片
int recored1; //射击时间间隔
Bullet enemy2Bullet1[30];
Bullet enemy2Bullet2[30];
Bullet enemy2Bullet3[30]; //Boss类飞机弹匣
void shoot(); //射击函数
void updatePosition(); //位置更新函数
};
Boss类飞机相对于普通飞机而言更复杂,首先是设定了生命值为10,普通飞机的生命值默认为1,即被子弹打中就摧毁,而Boss可以抗十下。其次就在于Boss飞机的射击分为左中右三条线,所以设定了三个弹匣,在子弹类中我设计了三个敌机子弹位置的更新函数。
类的具体实现如下:
#include "enemyplane2.h"
#include <QPixmap>
#include <bullet.h>
EnemyPlane2::EnemyPlane2()
{
enemy2.load(":/image/images/boss.png");
x = 0;
y = 0;
isFree = true;
isDestroyed = false;
speed = 1;
life = 10;
recored1 = 0;
}
void EnemyPlane2::updatePosition(){
if(isFree)
return;
y+=speed;
if(y>600)
isDestroyed = true;
}
void EnemyPlane2::shoot(){
//Boss飞机射击函数
recored1++;
if(recored1<20)
return;
recored1 = 0;
for(int i=0;i<30;i++){
if(enemy2Bullet1[i].isFree){
enemy2Bullet1[i].x = x+30;
enemy2Bullet1[i].y = y+20;
enemy2Bullet1[i].isFree = false;
break;
}
}
for(int i=0;i<30;i++){
if(enemy2Bullet2[i].isFree){
enemy2Bullet2[i].x = x+80;
enemy2Bullet2[i].y = y+20;
enemy2Bullet2[i].isFree = false;
break;
}
}
for(int i=0;i<30;i++){
if(enemy2Bullet3[i].isFree){
enemy2Bullet3[i].x = x+130;
enemy2Bullet3[i].y = y+20;
enemy2Bullet3[i].isFree = false;
break;
}
}
}
Boss飞机的射击函数与其他两个是差不多的,不过Boss飞机有三个弹匣,所以三个弹匣的循环都需要执行一遍。
5.爆炸效果类Bomb
#ifndef BOMB_H
#define BOMB_H
#define BOMB_PATH ":/image/images/bomb-%1.png" //设置图片路径
#include <QPixmap> //1%为可代替部分
#include <QVector>
#include <QString>
class Bomb
{
public:
Bomb();
void updateInfo(); //爆炸图片更新
int x;
int y;
QVector<QPixmap> bombPix; //爆炸图片数组
int recored; //爆炸时间标志
int index; //图片下标
bool isPlayde; //爆炸效果是否播放过
};
#endif // BOMB_H
实现爆炸效果动画实际上是用的七张图片在短时间内轮流播放而成的,所有爆炸图片存储在向量中,由updateInfo决定播放那张图片,当所有图片都播放完后,改变isPlayde的值(这里有拼写错误,我也是在总结的时候才发现的,所以后面的代码中这里都是isPlayde)。在updateInfo中,会对isPlayde进行判断,如果为false则播放,毕竟飞机只要炸一次就够了。
类的具体实现如下
#include "bomb.h"
Bomb::Bomb()
{
for(int i=0;i<7;i++){
QString str = QString(BOMB_PATH).arg(i);
bombPix.push_back(str);
} //初始化将所有图片添加值向量中
recored = 0;
index = 0;
x = 0;
y = 0;
isPlayde = false;
}
void Bomb::updateInfo(){
recored++; //设置播放间隔
if(recored<20)
return;
recored = 0;
index++; //设置播放图片位置
if(index>6)
isPlayde = true; //全部播放完毕改变状态
}
6.游戏界面设计(Easy)
做完了上述准备工作,我们可以进行游戏界面的设计了,我给这个界面命名为Easy,主要是我设置了几个关卡界面,这是其中之一,以这个为例来进行我们的游戏设计。
#ifndef EASY_H
#define EASY_H
#include <QWidget>
#include <plane.h>
#include <QPainter>
#include <QPaintEvent>
#include <bullet.h>
#include <QTimer>
#include <QMouseEvent>
#include <enemyplane1.h>
#include <enemyplane2.h>
#include <QLabel>
#define SCORE "Score: %1" //预定义得分字符串
#define LIFE "Life: %1" //预定义生命值字符串
class Easy : public QWidget
{
Q_OBJECT
public:
QLabel life;
QLabel score;
int Score;
Plane MyPlane;
EnemyPlane1 Enemy1[10]; //敌机数组
EnemyPlane2 Enemy2[2]; //Boss敌机数组
int EnemyRecored1; //敌机出场间隔
int EnemyRecored2; //Boss出场间隔
QTimer Timer; //设置QTimer定时器
void initial(); //游戏初始化
void startGame(); //游戏开始
void updatePositino(); //游戏信息更新函数
void paintEvent(QPaintEvent *E); //绘图事件
void mouseMoveEvent(QMouseEvent *E); //鼠标移动事件
void BossShow(); //Boss出场函数
void EnemyShow(); //敌机出场函数
void