![5b7a810e37d2481a08e09db0b537f1f3.png](https://i-blog.csdnimg.cn/blog_migrate/62fa90463e92009b4e084004a0f96b78.jpeg)
作者 | 白家名
责编 | 王晓曼
出品 | CSDN博客
本文作者使用 QT 框架写了一个塔防游戏程序,该程序中实现了购买炮塔、炮塔升级、怪物按照设定路径移动、炮塔自动寻找范围内目标、朝目标怪物发射炮弹、爆炸效果、怪物走到家时我方生命值减少、方便添加关卡等功能。
运行效果:
![3fc64e93810cd759bca5d02006842061.png](https://i-blog.csdnimg.cn/blog_migrate/d7118e6579bc4af20f53c89cc1e98d30.jpeg)
![bcbbadc3a3b011e7be2cc2db26d687b2.png](https://i-blog.csdnimg.cn/blog_migrate/cb6f4993d436d5bb7dfe8e0356fe871b.jpeg)
这张截图中间的炮塔比较大,这是该炮塔多次升级后的结果。
炮塔升级后图片不会改变,但该炮塔的大小、位置、炮弹大小、攻击所产生的爆炸效果的大小、攻击力、攻击范围都会发生改变。
![c7dbfddd3f51e6c53e7fc250ba43f6fc.png](https://i-blog.csdnimg.cn/blog_migrate/9570817a2cbac7d0a86222c9367958ae.jpeg)
![f8a5ee0353ad6587cae652ca9738c065.png](https://i-blog.csdnimg.cn/blog_migrate/b06c8afed398d1e810301b4f1d46da92.jpeg)
![0cde2add4434e4f14792adc28bfb1ac9.png](https://i-blog.csdnimg.cn/blog_migrate/8283941f616f7e8d9d4756f72219bb6b.jpeg)
![1e653376dafc9bff0e6e19c15be77949.png](https://i-blog.csdnimg.cn/blog_migrate/05de46c08b12371d113c860d236c4d78.jpeg)
遗憾点
尽管我已经尽力地标准化这个程序了,但还是因为我对程序后面的步骤的认知不正确,以及由于各种各样的原因,还是遗留下来了很多的遗憾。
在写这个 demo 的后期,我意识到写这个程序已经花费了太长的时间,而且当时我被这个程序折磨的很是难受,为了节省时间尽快完工,在代码上我没有按照最标准的情况来写,且游戏内容也被我简化了许多。
使用 QT 框架写程序,一般好像都是使用各种控件堆起来,然后监听这些控件的信号。但我在程序中却使用了非常原始的办法,即判断鼠标的点击区域,这是因为我发现在connect函数里面,不论是将控件创建在栈区还是堆区都没有用(或许有解决办法,但我不会,从网上也没有找到)。这也就是说没有办法实现最基本的点击一个按钮创建其他按钮的功能,所以我还是用了最原始,也可能是最不标准的搞笑办法实现的相关功能。
![b10546e2552f74f41d46011871fd5208.png](https://i-blog.csdnimg.cn/blog_migrate/27fff8a90ac9989b2974b524dbcd625b.jpeg)
方案选择
从 QT 的使用的来看,我可能只是一个十足的新手,但就程序而言,逻辑应该还是差不多的。
如果想要在代码中添加一个关卡,且使用预设的产生怪物方案的话,大概只需要在 mainwindow.cpp 中添加三十五行代码即可,这些代码的用途主要是用于监听进入关卡的按钮、设定怪物的初始位置和路径点、调用预设的产生怪物方案函数和添加新的地图数组。
其实这个添加关卡的方案是我当时想出来的三种方案中最次的一种,这种添加关卡的方案需要直接修改游戏界面类,也就是程序的主要代码,这应该是非常不好的。
而另外两种我设想的方案,读取文件中的内容构建关卡和将关卡相关代码写在开始界面的构造函数中。
第一种方案其实是最好的,但因为涉及到各种各样的文件操作,包括需要精确地移动文件指针、精确地读取到每个字符、字符串和数字之间的转化等,学习这些非常麻烦。并且当时我已经将获取关卡信息的方式设为了读取各个对象、数组的信息的方式,全部修改也非常麻烦,所以放弃。
后来,我想通过将关卡所需要的数据全部写在开始界面类构造函数的方式实现创建关卡,这种方式最起码可以将主程序和关卡信息分离,且原理很简单,就是向主游戏界面对象中传递几个参数即可。但这个时候,我遇到了一个非常致命的问题,这个问题是,在向connect传递lambda表达式做参数时,表达式内部始终没有办法使用函数外部的一个用于传递怪物路径信息的三级指针,而且还是编译失败,并且在我看来,程序中是不存在语法错误的。这个问题导致我花掉了整整一下午的时间都没有找到解决方案,遂放弃。
因为程序比较复杂,所以我为了完成这个程序大概花费了八天的时间之久。这让我认识到,在写复杂程序之时,基础非常重要。
![f3359af61b049ed38cca5e1169c4670e.png](https://i-blog.csdnimg.cn/blog_migrate/fd4e92d8027e8b4ff2410bdddfeac833.jpeg)
编程步骤
1、编译环境:
Windows Qt 5.9.0 Qt Creator 4.3.0
2、思路:
将二维数组中防御塔空位的坐标保存到动态数组中,遍历这个数组并判断点击区域实现点击防御塔空位,其他的点击也类似。
怪物是根据一个路径点数组中的数据移动的,这个数组需要给出所有怪物的拐点,怪物一直向第一个路径点处移动。这里其实也可以写成像防御塔位置那样自动获取路径点的,这样的话就只需要人为设定怪物遇到交叉路口时的移动方向就好了,但是因为在我想到这个主意时已经写好这一部分了,所以也就懒得改了。
防御塔寻找目标怪物的规则:从后往前找范围内的怪物,如果目标怪物被删除或走出范围了,则重新找到范围内最后一个怪物,否则一直瞄准目标怪物。
该程序中防御塔发射子弹的位置始终处于防御塔的正中心,不会随防御塔的旋转而改变,只是子弹的运动方向会始终跟随目标怪物。另外,因为我不想写了,没有在程序中添加子弹图片的旋转,一律使用的正方形图片作为子弹图片。这两点也是该程序不足的地方。
下面展示程序的主要代码。
mainwindow.h 主要游戏界面类头文件:
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include
#include //画家
#include //鼠标事件
#include //定时器
#include "defensetowerpit.h" //防御塔坑类
#include "selectionbox.h" //选择框类
#include "defetowerparent.h" //防御塔父类
#include "monster.h" //怪物类
#include
class MainWindow : public QWidget
{
// Q_OBJECT
private:
QVector TowerPitVec; //防御塔坑数组
SelectionBox* SelBox = new SelectionBox("C:/Users/ASUS/Desktop/image/选择框.png"); //选择框类
QVector DefeTowerVec; //重要防御塔父类数组
QVector MonsterVec; //怪物数组
void paintEvent(QPaintEvent *); //绘图事件
void mousePressEvent(QMouseEvent *); //鼠标点击事件
// void mouseMoveEvent(QMouseEvent *); //鼠标移动事件
void DrawMapArr(QPainter&); //用于画出地图函数
void DrawSelectionBox(QPainter&); //用于画出选择框
void DrawDefenseTower(QPainter&); //画出防御塔
void DrawMonster(QPainter&); //画出怪物
void DrawRangeAndUpgrade(QPainter&); //画出防御塔攻击范围和升级按钮
void DrawExplosion(QPainter&); //画出爆炸效果
int DisplayRangeX, DisplayRangeY; //用于记录显示范围的防御塔的坐标
bool DisplayRange = false; //用于显示防御塔攻击范围
struct ExploStr //爆炸效果结构
{
CoorStr coor; //记录爆炸效果的坐标
int index = 1; //记录要显示的图片文件的序号
int ExplRangeWidth; //爆炸效果宽高
int ExplRangeHeight;
//爆炸效果需要初始化坐标、效果宽高
ExploStr(CoorStr fcoor, int width, int height) : coor(fcoor), ExplRangeWidth(width), ExplRangeHeight(height) {}
};
QVector ExploEffectCoor; //爆炸效果坐标数组
int money = 1200; //记录金钱
QLabel *moneylable = new QLabel(this); //显示金钱标签控件
inline bool DeductionMoney(int); //判断金钱是否足够并刷新标签
int life = 10; //生命数量
int counter = 0; //用于产生怪物的计数器
const int RewardMoney = 26; //每次击败怪物获得的金钱数量
CoorStr *homecoor = new CoorStr(0, 0); //记录家的坐标,从地图中自动获取
void IrodMonsProgDefa(CoorStr**, CoorStr**, CoorStr*, int*, QLabel*); //预设两条路产生怪物方案函数
const int LevelNumber; //标识关卡
bool DisplayAllRange = false; //标识是否显示所有防御塔的攻击范围
public:
MainWindow(int); //构造
~MainWindow;
};
#endif //MAINWINDOW_H
mainwindow.cpp 主要游戏界面类函数实现:
#include "mainwindow.h"
#include
#include "globalstruct.h" //选择框按钮全局结构
#include //数学
#include "greenturret.h" //绿色防御塔类
#include "fireturret.h" //火防御塔类
#include "lightturret.h" //光炮防御塔类
#include "bigturret.h" //大炮防御塔类
#include
//鼠标点击区域宏
#define MouClickRegion(X, Width, Y, Height)
(ev->x >= (X) && ev->x <= (X) + (Width) &&
ev->y >= (Y) && ev->y <= (Y) + (Height))
//计算两点之间距离宏
#define DistBetPoints(X1, Y1, X2, Y2)
abs(sqrt((((X1) - (X2)) * ((X1) - (X2))) + (((Y1) - (Y2)) * ((Y1) - (Y2)))))
//用于方便通过格子确定路径点坐标
#define X40(X, Y) ((X) - 1) * 40 + 10, ((Y) - 1) * 40 + 10
//插入怪物 路径点数组名、怪物初始坐标、怪物编号
#define InsterMonster(PathNum, StaCoorNum, MonsterId)
MonsterVec.push_back(new Monster(pointarr[PathNum], PathLength[PathNum], X40(staco[StaCoorNum].x, staco[StaCoorNum].y), MonsterId));
//判断金钱是否足够并刷新标签
inline bool MainWindow::DeductionMoney(int money)
{
if (this->money - money < 0) return true; //判断金钱是否足够
this->money -= money; //扣除金钱
moneylable->setText(QString("金钱:%1").arg(this->money)); //刷新标签文本
return false;
}
//构造
MainWindow::MainWindow(int LevelNumber) : LevelNumber(LevelNumber)
{
//设置固定窗口大小
setFixedSize(1040, 640);
//设置标题
setWindowTitle("游戏界面");
//提示胜利标签
QLabel *victorylable = new QLabel(this);
victorylable->move(176, 180);
victorylable->setFont(QFont("楷体