1. Dotnet9首页
  2. C++
  3. Qt Widgets

QT 框架搭建,用最原始的方法实现简单的塔防游戏

QT 框架搭建,用最原始的方法实现简单的塔防游戏
作者 | 白家名
责编 | 王晓曼
出品 | CSDN博客

本文作者使用 QT 框架写了一个塔防游戏程序,该程序中实现了购买炮塔、炮塔升级、怪物按照设定路径移动、炮塔自动寻找范围内目标、朝目标怪物发射炮弹、爆炸效果、怪物走到家时我方生命值减少、方便添加关卡等功能。

运行效果:

QT 框架搭建,用最原始的方法实现简单的塔防游戏
QT 框架搭建,用最原始的方法实现简单的塔防游戏

这张截图中间的炮塔比较大,这是该炮塔多次升级后的结果。

炮塔升级后图片不会改变,但该炮塔的大小、位置、炮弹大小、攻击所产生的爆炸效果的大小、攻击力、攻击范围都会发生改变。

QT 框架搭建,用最原始的方法实现简单的塔防游戏
QT 框架搭建,用最原始的方法实现简单的塔防游戏
QT 框架搭建,用最原始的方法实现简单的塔防游戏

1 遗憾点

尽管我已经尽力地标准化这个程序了,但还是因为我对程序后面的步骤的认知不正确,以及由于各种各样的原因,还是遗留下来了很多的遗憾。

在写这个 demo 的后期,我意识到写这个程序已经花费了太长的时间,而且当时我被这个程序折磨的很是难受,为了节省时间尽快完工,在代码上我没有按照最标准的情况来写,且游戏内容也被我简化了许多。

使用 QT 框架写程序,一般好像都是使用各种控件堆起来,然后监听这些控件的信号。但我在程序中却使用了非常原始的办法,即判断鼠标的点击区域,这是因为我发现在connect函数里面,不论是将控件创建在栈区还是堆区都没有用(或许有解决办法,但我不会,从网上也没有找到)。这也就是说没有办法实现最基本的点击一个按钮创建其他按钮的功能,所以我还是用了最原始,也可能是最不标准的搞笑办法实现的相关功能。

2 方案选择

从 QT 的使用的来看,我可能只是一个十足的新手,但就程序而言,逻辑应该还是差不多的。

如果想要在代码中添加一个关卡,且使用预设的产生怪物方案的话,大概只需要在 mainwindow.cpp 中添加三十五行代码即可,这些代码的用途主要是用于监听进入关卡的按钮、设定怪物的初始位置和路径点、调用预设的产生怪物方案函数和添加新的地图数组。

其实这个添加关卡的方案是我当时想出来的三种方案中最次的一种,这种添加关卡的方案需要直接修改游戏界面类,也就是程序的主要代码,这应该是非常不好的。

而另外两种我设想的方案,读取文件中的内容构建关卡和将关卡相关代码写在开始界面的构造函数中。

第一种方案其实是最好的,但因为涉及到各种各样的文件操作,包括需要精确地移动文件指针、精确地读取到每个字符、字符串和数字之间的转化等,学习这些非常麻烦。并且当时我已经将获取关卡信息的方式设为了读取各个对象、数组的信息的方式,全部修改也非常麻烦,所以放弃。

后来,我想通过将关卡所需要的数据全部写在开始界面类构造函数的方式实现创建关卡,这种方式最起码可以将主程序和关卡信息分离,且原理很简单,就是向主游戏界面对象中传递几个参数即可。但这个时候,我遇到了一个非常致命的问题,这个问题是,在向connect传递lambda表达式做参数时,表达式内部始终没有办法使用函数外部的一个用于传递怪物路径信息的三级指针,而且还是编译失败,并且在我看来,程序中是不存在语法错误的。这个问题导致我花掉了整整一下午的时间都没有找到解决方案,遂放弃。

因为程序比较复杂,所以我为了完成这个程序大概花费了八天的时间之久。这让我认识到,在写复杂程序之时,基础非常重要。

3 编程步骤

1、编译环境:

Windows Qt 5.9.0 Qt Creator 4.3.0

2、思路:

将二维数组中防御塔空位的坐标保存到动态数组中,遍历这个数组并判断点击区域实现点击防御塔空位,其他的点击也类似。

怪物是根据一个路径点数组中的数据移动的,这个数组需要给出所有怪物的拐点,怪物一直向第一个路径点处移动。这里其实也可以写成像防御塔位置那样自动获取路径点的,这样的话就只需要人为设定怪物遇到交叉路口时的移动方向就好了,但是因为在我想到这个主意时已经写好这一部分了,所以也就懒得改了。

防御塔寻找目标怪物的规则:从后往前找范围内的怪物,如果目标怪物被删除或走出范围了,则重新找到范围内最后一个怪物,否则一直瞄准目标怪物。

该程序中防御塔发射子弹的位置始终处于防御塔的正中心,不会随防御塔的旋转而改变,只是子弹的运动方向会始终跟随目标怪物。另外,因为我不想写了,没有在程序中添加子弹图片的旋转,一律使用的正方形图片作为子弹图片。这两点也是该程序不足的地方。

下面展示程序的主要代码。

mainwindow.h 主要游戏界面类头文件:

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QWidget>
#include <QPainter>     //画家
#include <QMouseEvent>  //鼠标事件
#include <Qtimer>       //定时器
#include "defensetowerpit.h"    //防御塔坑类
#include "selectionbox.h"       //选择框类
#include "defetowerparent.h"    //防御塔父类
#include "monster.h"    //怪物类
#include <QLabel>

class MainWindow : public QWidget
{
//    Q_OBJECT
private:
    QVector<DefenseTowerPit*> TowerPitVec;  //防御塔坑数组
    SelectionBox* SelBox = new SelectionBox("C:/Users/ASUS/Desktop/image/选择框.png"); //选择框类

    QVector<DefeTowerParent*> DefeTowerVec; //重要防御塔父类数组

    QVector<Monster*> 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<ExploStr*> 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 <QDebug>
#include "globalstruct.h"       //选择框按钮全局结构
#include <cmath>               //数学
#include "greenturret.h"        //绿色防御塔类
#include "fireturret.h"         //火防御塔类
#include "lightturret.h"        //光炮防御塔类
#include "bigturret.h"          //大炮防御塔类
#include <QPushButton>

//鼠标点击区域宏
#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("楷体", 110));
    victorylable->setText(QString("游戏胜利"));
    victorylable->hide();

    QTimer* timer2 = new QTimer(this);      //用于插入怪物定时器
    timer2->start(2000);

    connect(timer2,&QTimer::timeout,[=]()
    {
        //根据关卡编号确定执行怪物的路径方案
        switch (LevelNumber)
        {
        case 0:
        {
            //设置路径点
            CoorStr* Waypointarr1[] = {new CoorStr(X40(8, 6)/*X40是两个参数,为X坐标和Y坐标*/), new CoorStr(X40(2, 6)), new CoorStr(X40(2, 13)), new CoorStr(X40(19, 13)), new CoorStr(X40(19, 9)), new CoorStr(homecoor->x, homecoor->y)};
            CoorStr* Waypointarr2[] = {new CoorStr(X40(20, 5)), new CoorStr(X40(14, 5)), new CoorStr(X40(14, 9)), new CoorStr(X40(8, 9)), new CoorStr(X40(8, 6)), new CoorStr(X40(2, 6)),
                                             new CoorStr(X40(2, 13)), new CoorStr(X40(19, 13)), new CoorStr(X40(19, 9)), new CoorStr(homecoor->x, homecoor->y)}; //最后的路径点设为家
            //怪物的四个起始点
            CoorStr staco[] = {CoorStr(8, 0), CoorStr(20, 0), CoorStr(8, -1), CoorStr(20, -1)};

            //每条路径的结点个数
            int PathLength[] = {sizeof(Waypointarr1)/sizeof(CoorStr*), sizeof(Waypointarr1)/sizeof(CoorStr*)};

            IrodMonsProgDefa(Waypointarr1, Waypointarr2, staco, PathLength, victorylable);   //使用预设的两条路产生怪物方案

            break;
        }
        case 1:
        {
            //设置路径点
            CoorStr* Waypointarr1[] = {new CoorStr(X40(4, 8)), new CoorStr(X40(20, 8)), new CoorStr(X40(20, 13)), new CoorStr(X40(6, 13)), new CoorStr(homecoor->x, homecoor->y)};
            CoorStr* Waypointarr2[] = {new CoorStr(X40(11, 8)), new CoorStr(X40(20, 8)), new CoorStr(X40(20, 13)), new CoorStr(X40(6, 13)), new CoorStr(homecoor->x, homecoor->y)};

                                      //怪物的四个起始点
            CoorStr staco[] = {CoorStr(4, 0), CoorStr(11, 0), CoorStr(4, -1), CoorStr(11, -1)};

            //每条路径的结点个数
            int PathLength[] = {sizeof(Waypointarr1)/sizeof(CoorStr*), sizeof(Waypointarr1)/sizeof(CoorStr*)};

            IrodMonsProgDefa(Waypointarr1, Waypointarr2, staco, PathLength, victorylable);   //使用预设的两条路产生怪物方案
            break;
        }

        case 2:
        {
            //设置路径点
            CoorStr* Waypointarr1[] = {new CoorStr(X40(12, 9)), new CoorStr(X40(8, 9)), new CoorStr(X40(8, 0)), new CoorStr(homecoor->x, homecoor->y)};
            CoorStr* Waypointarr2[] = {new CoorStr(X40(12, 9)), new CoorStr(X40(16, 9)), new CoorStr(X40(16, 0)), new CoorStr(homecoor->x, homecoor->y)};

            //怪物的四个起始点
            CoorStr staco[] = {CoorStr(12, 16), CoorStr(12, 16), CoorStr(12, 17), CoorStr(12, 18)};

            //每条路径的结点个数
            int PathLength[] = {sizeof(Waypointarr1)/sizeof(CoorStr*), sizeof(Waypointarr1)/sizeof(CoorStr*)};

            IrodMonsProgDefa(Waypointarr1, Waypointarr2, staco, PathLength, victorylable);   //使用预设的两条路的产生怪物方案
            break;
        }
        }
    });

//    setMouseTracking(true);

    //显示防御塔范围按钮
    QPushButton* disranpush = new QPushButton(this);
    disranpush->setStyleSheet("color:black");
    disranpush->setGeometry(20,160, 150, 45);
    disranpush->setFont(QFont("微软雅黑", 12));
    disranpush->setText("显示全部范围");

    connect(disranpush,&QPushButton::clicked,[=]()
    {//通过改变标识令防御塔攻击范围显示或关闭
        if (DisplayAllRange)
        {
            DisplayAllRange = false;
            disranpush->setText("显示全部范围");
        }
        else
        {
            DisplayAllRange = true;
            disranpush->setText("隐藏全部范围");
        };
        update();
    });

    //金钱标签
    moneylable->move(20, 40);       //位置
    setStyleSheet("color:white");   //设置颜色
    moneylable->setFont(QFont("微软雅黑", 24));             //设置金钱标签属性
    moneylable->setText(QString("金钱:%1").arg(money));    //显示金钱信息

    //生命值标签
    QLabel *lifelable = new QLabel(this);
    lifelable->setGeometry(20, 100, 220, 40);   //设置控件位置和大小
    lifelable->setFont(QFont("微软雅黑", 24));
    lifelable->setText(QString("生命:%1").arg(life));

    //定时器每时每刻执行防御塔父类数组的活动函数
    QTimer* timer = new QTimer(this);
    timer->start(120);

    connect(timer,&QTimer::timeout,[=]()
    {
        //防御塔寻找目标怪物的规律:找到最后一个怪物作为目标,目标丢失后找再继续找最后一个目标
        for (auto defei : DefeTowerVec)      //遍历防御塔
        {
            if (!defei->GetAimsMonster())   //目标怪物为空时从后往前遍历怪物数组寻找目标怪物
            {
                for(int i = MonsterVec.size() - 1; i >= 0; i--)
                    //这里以防御塔中心店和怪物中心点判断
                    if (DistBetPoints(defei->GetUpLeftX() + 40, defei->GetUpLeftY() + 40,
                        MonsterVec.at(i)->GetX() + (MonsterVec.at(i)->GetWidth() >> 1),
                        MonsterVec.at(i)->GetY() + (MonsterVec.at(i)->GetHeight() >> 1)) <= defei->GetRange())
                    {
                        defei->SetAimsMonster(MonsterVec.at(i));    //设置防御塔的目标怪物
                        break;  //找到后跳出循环
                    }
            }
            else                //当前防御塔拥有目标且怪物在防御塔范围之内时时攻击怪物
                if (DistBetPoints(defei->GetUpLeftX() + 40, defei->GetUpLeftY() + 40,
                    defei->GetAimsMonster()->GetX() + (defei->GetAimsMonster()->GetWidth() >> 1),
                    defei->GetAimsMonster()->GetY() + (defei->GetAimsMonster()->GetHeight() >> 1)) <= defei->GetRange())
                {
                    //根据每个防御塔的目标怪物计算旋转角度
                    defei->SetRotatAngle(
                        atan2
                        (
                            defei->GetAimsMonster()->GetY()/* + (defei->GetAimsMonster()->GetHeight() >> 1)*/ - defei->GetUpLeftY() + 40,
                            defei->GetAimsMonster()->GetX()/* + (defei->GetAimsMonster()->GetWidth() >> 1)*/ - defei->GetUpLeftX()
                        ) * 180 / 3.1415 );

                     defei->InterBullet();           //拥有目标时一直创建子弹
                }

            //每次循环都判断目标怪物距离防御塔的距离,写在上面可能会不太好
            if (defei->GetAimsMonster())    //目标怪物不为空
                if (DistBetPoints(defei->GetUpLeftX() + 40, defei->GetUpLeftY() + 40,
                    defei->GetAimsMonster()->GetX() + (defei->GetAimsMonster()->GetWidth() >> 1),
                    defei->GetAimsMonster()->GetY() + (defei->GetAimsMonster()->GetHeight() >> 1)) > defei->GetRange())
                        defei->SetAimsMonster(NULL);     //超过距离将目标怪物设为空
        }

        //防御塔子弹移动
        for (auto defei : DefeTowerVec)
            defei->BulletMove();

        //怪物移动
        for (auto Moni = MonsterVec.begin(); Moni != MonsterVec.end(); Moni++)
            if((*Moni)->Move()) //怪物走到终点
            {
                delete *Moni;
                MonsterVec.erase(Moni);         //怪物走到终点则删除这个怪物

                life--;                         //我们的生命数量-1
                lifelable->setText(QString("生命:%1").arg(life));

                if (life <= 0) this->close();   //生命值为0时关闭该窗口

                break;
            }

        //判断子弹击中怪物
        for (auto defei : DefeTowerVec)  //防御塔
        {
            auto &tbullvec = defei->GetBulletVec();    //临时存储子弹
            for (auto bullit = tbullvec.begin(); bullit != tbullvec.end(); bullit++)    //子弹
                for (auto monit = MonsterVec.begin(); monit != MonsterVec.end(); monit++)//怪物
                    if ((*bullit)->GetX() + (defei->GetBulletWidth() >> 1) >= (*monit)->GetX() && (*bullit)->GetX() <= (*monit)->GetX() + (*monit)->GetWidth() &&
                       (*bullit)->GetY() + (defei->GetBulletHeight() >> 1) >= (*monit)->GetY() && (*bullit)->GetY() <= (*monit)->GetY() + (*monit)->GetHeight())
                    {   //击中怪物时
                        tbullvec.erase(bullit);     //删除子弹

                        (*monit)->SetHealth((*monit)->GetHealth() - defei->GetAttack());      //敌人血量 = 本身血量减去 当前炮塔攻击力

                        //将击中的怪物中心的坐标插入到爆炸效果数组
                        ExploEffectCoor.push_back(new ExploStr(CoorStr((*monit)->GetX() + ((*monit)->GetWidth() >> 1), (*monit)->GetY() + ((*monit)->GetHeight() >> 1)),
                                                                defei->GetExplRangeWidth(), defei->GetExplRangeHeight()));

                        if ((*monit)->GetHealth() <= 0) //生命值为空时
                        {
                            //判断一下其他防御塔的目标怪物是否和当前防御塔消灭的怪物重复,如果重复,则将那一个防御塔的目标怪物也设为空
                            for (auto defei2 : DefeTowerVec)
                                if (defei2->GetAimsMonster() == *monit)
                                    defei2->SetAimsMonster(NULL);

                            MonsterVec.erase(monit);    //删除怪物
                            money += RewardMoney;       //击败怪物增加金钱
                            moneylable->setText(QString("金钱:%1").arg(money));//刷新标签
                        }

                        //这里不能将防御塔目标怪物设为空,因为防御塔的子弹攻击到的怪物不一定是该防御塔的目标怪物
                        goto L1;
                    }
            L1:;
        }

        //显示爆炸效果
        for (auto expli = ExploEffectCoor.begin(); expli != ExploEffectCoor.end(); expli++)
        {
            if((*expli)->index >= 8)           //爆炸效果显示完后将该效果从数组中删除
            {
                ExploEffectCoor.erase(expli);
                break;
            }

            (*expli)->index++;                 //将要显示的图片的序号++
        }

        update();   //绘图
    });
}

//预设的两条路产生怪物方案
void MainWindow::IrodMonsProgDefa(CoorStr** Waypointarr1, CoorStr** Waypointarr2, CoorStr* staco, int* PathLength, QLabel* victorylable)
{
    /*两条路径*/
    CoorStr** pointarr[] = {Waypointarr1, Waypointarr2};

    /*插入怪物*/
    if(counter >= 1 && counter <= 12)
    {//插入小恐龙*
        InsterMonster(0, 0, 1); //第几条路径、第几个起始点、怪物编号
    }
    else if(counter > 12 && counter <= 34)
    {//在、两路插入小恐龙和鲨鱼
        InsterMonster(0, 0, 1);
        InsterMonster(1, 1, 2);
    }
    else if (counter > 34 && counter <= 35)
    {//两路插入巨龙
        InsterMonster(0, 0, 3);
        InsterMonster(1, 1, 3);
    }
    else if (counter > 35 && counter <= 52)
    {//两路插入狮子、狮子、小恐龙
        InsterMonster(0, 2, 4);
        InsterMonster(0, 0, 4);
        InsterMonster(1, 1, 1);
    }
    if(counter > 52 && counter <= 56)
    {//插入巨龙
        InsterMonster(0, 0, 3);
        InsterMonster(1, 1, 3);
    }
    if (counter > 52 && counter <= 71)
    {//插入鲨鱼、蜗牛、小恐龙、狮子
        InsterMonster(0, 2, 2);
        InsterMonster(0, 0, 5);
        InsterMonster(1, 3, 1);
        InsterMonster(1, 1, 4);
    }

    if (counter > 71 && MonsterVec.empty())   //时间大于71且怪物数组为空时游戏胜利
        victorylable->show();

    counter++;          //计数器+1
    update();
}

//根据数组画出地图函数
//由绘图函数调用
void MainWindow::DrawMapArr(QPainter& painter)
{
    //地图数组  第一关
    int Map_1[16][26] =
    {
        0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 1, 1, 3, 6, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 1, 1, 6, 6, 0, 0, 0, 0, 0, 0, 3, 6, 1, 1, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 3, 6, 1, 1, 3, 6, 0, 0, 0, 0, 0, 0, 6, 6, 1, 1, 3, 6, 0, 0, 0,
        0, 0, 0, 0, 0, 6, 6, 1, 1, 6, 6, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 6, 6, 0, 0, 0,
        0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0,
        0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 3, 6, 0, 1, 1, 0, 0, 0, 0, 3, 6, 0, 0, 0, 0, 0,
        0, 1, 1, 0, 3, 6, 0, 1, 1, 0, 6, 6, 0, 1, 1, 0, 3, 6, 0, 6, 6, 0, 0, 0, 0, 0,
        0, 1, 1, 0, 6, 6, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 6, 6, 1, 1, 1, 1, 1, 1, 5, 1,
        0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1,
        0, 1, 1, 0, 3, 6, 0, 0, 3, 6, 0, 0, 3, 6, 0, 0, 3, 6, 1, 1, 3, 6, 0, 0, 0, 0,
        0, 1, 1, 0, 6, 6, 0, 0, 6, 6, 0, 0, 6, 6, 0, 0, 6, 6, 1, 1, 6, 6, 0, 0, 0, 0,
        0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0,
        0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    };
    //第二关
    int Map_2[16][26] =
    {
        0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 3, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 6, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 3, 6, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 6, 6, 1, 1, 0, 0, 3, 6, 0, 1, 1, 0, 0, 3, 6, 0, 0, 3, 6, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 1, 1, 0, 0, 6, 6, 0, 1, 1, 0, 0, 6, 6, 0, 0, 6, 6, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0,
        0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 3, 6, 0, 0, 0, 0, 3, 6, 0, 0, 0, 0, 3, 6, 1, 1, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 6, 6, 0, 0, 0, 0, 6, 6, 0, 0, 0, 0, 6, 6, 1, 1, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 5, 1, 3, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 1, 1, 6, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    };

    int Map_3[16][26] =
    {
        0, 0, 0, 0, 0, 3, 6, 1, 1, 1, 1, 5, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 6, 6, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 1, 1, 3, 6, 0, 0, 3, 6, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 1, 1, 6, 6, 0, 0, 6, 6, 1, 1, 3, 6, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 3, 6, 0, 0, 1, 1, 6, 6, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 6, 6, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 1, 1, 3, 6, 0, 0, 3, 6, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 1, 1, 6, 6, 0, 0, 6, 6, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 3, 6, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 6, 6, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 3, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 6, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    };

    int Map[16][26];    //用于拷贝不同的关卡数组

    switch (LevelNumber)
    {
    case 0:
        memcpy(Map, Map_1, sizeof(Map));
        break;
    case 1:
        memcpy(Map, Map_2, sizeof(Map));
        break;
    case 2:
        memcpy(Map, Map_3, sizeof(Map));
        break;
    default:
        break;
    }

    for (int j = 0; j < 16; j++)
        for (int i = 0; i < 26; i++)
        {
            switch (Map[j][i])
            {
            case 0:     /*草地*/
                painter.drawPixmap(i * 40, j * 40, 40, 40,
                    QPixmap(":/image/GrassBlock.png"));
                break;
            case 1:     /*地面*/
                painter.drawPixmap(i * 40, j * 40, 40, 40,
                    QPixmap(":/image/GroundBlock.png"));
                break;
            case 3:     /*防御塔坑*/
                painter.drawPixmap(i * 40, j * 40, 80, 80,
                    QPixmap(":/image/StoneBrick.png"));
                //防御塔坑坐标初始化塔坑坐标,插入到塔坑对象数组
                TowerPitVec.push_back(new DefenseTowerPit(i * 40, j * 40));
                break;
            case 5:     //家在循环中也输出地面
                painter.drawPixmap(i * 40, j * 40, 40, 40,
                    QPixmap(":/image/GroundBlock.png"));
                homecoor->x = i * 40, homecoor->y = j * 40;
                break;
            }
        }

    painter.drawPixmap(homecoor->x, homecoor->y, 80, 80,
        QPixmap(":/image/home.png")); //最后画出家
}

//画出选择框
void MainWindow::DrawSelectionBox(QPainter& painter)
{
    //显示选择框
    if (!SelBox->GetDisplay())
        return;

   //画出选择框
    painter.drawPixmap(SelBox->GetX(), SelBox->GetY(), SelBox->GetWidth(), SelBox->GetHeight(),
        QPixmap(SelBox->GetImgPath()));

    //画出子按钮
    SubbutStr *ASubBut = SelBox->GetSelSubBut();    //接收子按钮结构数组
    for (int i = 0; i < 4; i++)
        painter.drawPixmap(ASubBut[i].SubX, ASubBut[i].SubY, ASubBut[i].SubWidth, ASubBut[i].SubHeight,
            QPixmap(ASubBut[i].SubImgPath));

    painter.setPen(QPen(Qt::yellow, 6, Qt::SolidLine));     //设置画笔
    painter.drawRect(QRect(SelBox->GetX() + 95, SelBox->GetY() + 95, 80, 80));
}

//画出防御塔
void MainWindow::DrawDefenseTower(QPainter& painter)
{
    //画出防御塔
    for (auto defei : DefeTowerVec)  //遍历防御塔数组
    {
        //画出底座
        painter.drawPixmap(defei->GetUpLeftX(), defei->GetUpLeftY(), 80, 80, QPixmap(defei->GetBaseImgPath()));

        //画出所有防御塔的攻击范围
        if(DisplayAllRange)
            painter.drawEllipse(QPoint(defei->GetUpLeftX() + 40, defei->GetUpLeftY() + 40), defei->GetRange(), defei->GetRange());

        //画出所有防御塔子弹
        for (auto bulli : defei->GetBulletVec())
            painter.drawPixmap(bulli->coor.x, bulli->coor.y, defei->GetBulletWidth(), defei->GetBulletHeight(),QPixmap(defei->GetBulletPath()));

        //画出防御塔
        painter.translate(defei->GetUpLeftX() + 40, defei->GetUpLeftY() + 40);          //设置旋转中心
        painter.rotate(defei->GetRotatAngle());             //旋转角度
        painter.translate(-(defei->GetUpLeftX() + 40), -(defei->GetUpLeftY() + 40));    //原点复位
        painter.drawPixmap(defei->GetX(), defei->GetY(), defei->GetWidth(), defei->GetHeight(), QPixmap(defei->GetDefImgPath())/*图片路径*/);

        painter.resetTransform();   //重置调整
    }
}

//画出怪物
void MainWindow::DrawMonster(QPainter& painter)
{
    for (auto moni : MonsterVec)//画出怪物
        painter.drawPixmap(moni->GetX(), moni->GetY(), moni->GetWidth(), moni->GetHeight(), QPixmap(moni->GetImgPath()));
}

//画出防御塔和升级按钮
void MainWindow::DrawRangeAndUpgrade(QPainter& painter)
{
    //根据条件画出防御塔攻击范围和升级按钮
    for (auto defei : DefeTowerVec)
        if(defei->GetUpLeftX() == DisplayRangeX && defei->GetUpLeftY() == DisplayRangeY && DisplayRange)
        {   //画出防御塔攻击范围
            painter.setPen(QPen(Qt::red));  //使用红色画出范围
            painter.drawEllipse(QPoint(DisplayRangeX + 40, DisplayRangeY + 40), defei->GetRange(), defei->GetRange());
            painter.drawPixmap(DisplayRangeX + 10, DisplayRangeY - 80, 60, 60, QPixmap(":/image/UpgradeBut1.png"));
        }
}

//画出爆炸效果
void MainWindow::DrawExplosion(QPainter& painter)
{
    //显示所有爆炸效果
    for (auto expli : ExploEffectCoor)
            painter.drawPixmap(expli->coor.x - (expli->ExplRangeWidth >> 1), expli->coor.y - (expli->ExplRangeHeight >> 1),
                expli->ExplRangeWidth, expli->ExplRangeHeight, QPixmap(QString(":/image/ExplosionEffect%1.png").arg(expli->index)));
}

//绘图事件
void MainWindow::paintEvent(QPaintEvent *)
{
    QPainter painter(this);     //创建画家类

    painter.setRenderHint(QPainter::Antialiasing);    //设置抗锯齿

    DrawMapArr(painter);        //画出地图

    DrawDefenseTower(painter);  //画出防御塔和子弹

    DrawMonster(painter);       //画出怪物

    DrawRangeAndUpgrade(painter);//画出攻击范围和升级按钮

    DrawExplosion(painter);     //画出爆炸效果

    DrawSelectionBox(painter);  //画出选择框
}

//鼠标点击事件
void MainWindow::mousePressEvent(QMouseEvent *ev)
{
    if (ev->button() != Qt::LeftButton)
        return;

    //判断升级按钮的点击
    if (DisplayRange)
    {
        if (MouClickRegion(DisplayRangeX + 10, 60 , DisplayRangeY - 80, 60))
        {
            //设置防御塔宽高,攻击力,微调坐标
            for (auto defei : DefeTowerVec)
                if (defei->GetUpLeftX() == DisplayRangeX && defei->GetUpLeftY() == DisplayRangeY && DisplayRange)
                {
                    if (DeductionMoney(200)) return;        //升级防御塔需要花费200

                    defei->SetAttack(defei->GetAttack() + 20);          //每次升级防御塔攻击力+20
                    defei->SetWidthHeight(defei->GetWidth() + 12, defei->GetHeight() + 6);   //防御塔宽高增加
                    defei->SetXY(defei->GetX() - 6, defei->GetY() - 3); //调整防御塔坐标
                    defei->SetAimsMonster(NULL);                        //将防御塔目标设为空
                    defei->SetRange() += 14;                            //设置防御塔的攻击范围
                    defei->SetExplRangeWidthHeight(defei->GetExplRangeWidth() + 5, defei->GetExplRangeHeight() + 5); //设置防御塔攻击怪物所产生的爆炸效果宽高
                    defei->SetBulletWidthHeight(defei->GetBulletWidth() + 5, defei->GetBulletHeight() + 5);          //设置子弹宽高
                    break;
                }

            SelBox->SetDisplay(false);      //取消显示新建防御塔框
            DisplayRange = false;           //取消显示自己
            update();
            return;
        }
    }

    //判断选择框四个子按钮的点击
    SubbutStr *ASubBut = SelBox->GetSelSubBut();
    for (int i = 0; i < 4; i++)
        if (MouClickRegion(ASubBut[i].SubX, ASubBut[i].SubWidth, ASubBut[i].SubY, ASubBut[i].SubHeight) && SelBox->GetDisplay())
        {
            SelBox->SetDisplay(false);      //取消显示选择框

            //根据点击的不同的按钮,将防御塔子类插入到防御塔父类数组
            switch (i)
            {
            case 0: //绿瓶
                if (DeductionMoney(100)) return;    //扣除金钱
                DefeTowerVec.push_back(new GreenTurret(SelBox->GetX() + 110, SelBox->GetY() + 112, SelBox->GetX() + 95, SelBox->GetY() + 95, 72, 46));
                break;
            case 1: //火瓶
                if (DeductionMoney(160)) return;
                DefeTowerVec.push_back(new FireTurret(SelBox->GetX() + 110, SelBox->GetY() + 112, SelBox->GetX() + 95, SelBox->GetY() + 95, 72, 46));
                break;
            case 2: //光炮
                if (DeductionMoney(240)) return;
                DefeTowerVec.push_back(new LightTurret(SelBox->GetX() + 110, SelBox->GetY() + 112, SelBox->GetX() + 95, SelBox->GetY() + 95, 76, 50));
                break;
            case 3: //大炮
                if (DeductionMoney(400)) return;
                DefeTowerVec.push_back(new BigTurret(SelBox->GetX() + 110, SelBox->GetY() + 104, SelBox->GetX() + 95, SelBox->GetY() + 95, 80, 70));
                break;
            default:
                break;
            }

            update();
            return;
        }

    //遍历所有塔坑
    for (auto APit : TowerPitVec)
        //判断点击塔坑
        if (MouClickRegion(APit->GetX(), APit->GetWidth(), APit->GetY(), APit->GetHeight()))
        {
            DisplayRange = false;               //降防御塔的升级选择显示关闭
            for (auto defei : DefeTowerVec)      //遍历数组判断防御塔坐标和点击坑坐标重合则返回
                if(defei->GetUpLeftX() == APit->GetX() && defei->GetUpLeftY() == APit->GetY())
                {
                    DisplayRangeX = defei->GetUpLeftX(), DisplayRangeY = defei->GetUpLeftY();   //记录要显示攻击范围的防御塔的坐标
                    DisplayRange = true;        //显示防御塔攻击范围
                    return;
                }

            SelBox->CheckTower(APit->GetX(), APit->GetY());  //选中防御塔,选择框显示
            update();

            return;
        }

    DisplayRange = false;           //取消显示防御塔攻击范围
    SelBox->SetDisplay(false);      //取消显示选择框

    update();
}

//鼠标移动事件  测试炮台旋转
//void MainWindow::mouseMoveEvent(QMouseEvent *ev)
//{
//    for(auto defei : DefeTowerVec)
//        defei->SetRotatAngle(atan2(ev->y() - defei->GetUpLeftY() + 40, ev->x() - defei->GetUpLeftX()) * 180 / 3.1415);     //计算炮塔旋转的度数

//    update();
//}


//析构释放内存
MainWindow::~MainWindow()
{
    //释放防御塔坑指针数组TowerPitVec
    for (auto it = TowerPitVec.begin(); it != TowerPitVec.end(); it++)
    {
        delete *it;
        *it = NULL;
    }

    //释放选择框类SelBox
    delete SelBox;
    SelBox = NULL;

    //释放防御塔父类指针数组DefeTowerVec
    for (auto it = DefeTowerVec.begin(); it != DefeTowerVec.end(); it++)
    {
        delete *it;
        *it = NULL;
    }

    //释放怪物数组MonsterVec
    for (auto it = MonsterVec.begin(); it != MonsterVec.end(); it++)
    {
        delete *it;
        *it = NULL;
    }

    //释放爆炸效果指针数组ExploEffectCoor
    for (auto it = ExploEffectCoor.begin(); it != ExploEffectCoor.end(); it++)
    {
        delete *it;
        *it = NULL;
    }

    delete homecoor;
}

monster.h 怪物类头文件:

#ifndef MONSTER_H
#define MONSTER_H

#include <QVector>
#include <QString>
#include "globalstruct.h"   //坐标结构

//怪物类
class Monster
{
private:
    QVector<CoorStr*> Waypoint;  //存储怪物路径点数组
    int mx, my;                  //怪物坐标
    int mwidth, mheight;         //怪物宽高
    QString ImgPath;             //怪物图片路径
    int id;                      //怪物编号
    int health;                  //怪物生命值
    int mspeed = 10;       //怪物移动速度

public:
    //参数:路径点数组、路径点的个数、怪物初始坐标、怪物宽度、怪物图片路径
    Monster(CoorStr **pointarr, int arrlength, int x, int y, int fid);  //构造

    bool Move();            //怪物移动函数

    int GetX() const;       //获取横坐标
    int GetY() const;       //获取横坐标
    int GetWidth() const;   //获取宽
    int GetHeight() const;  //获取高
    QString GetImgPath() const; //获取图片路径
    int GetId() const;      //获取编号
    int GetHealth() const;  //获取生命值
    void SetHealth(int);    //设置生命值
};

#endif // MONSTER_H

monster.cpp 怪物类函数实现:

#include "monster.h"
#include <QDebug>

//怪物类函数实现
Monster::Monster(CoorStr **pointarr, int arrlength, int x, int y, int fid) :
    mx(x), my(y), id(fid)
{
    for(int i = 0; i < arrlength; i++)      //将传进来的数组插入到Waypoint动态数组
        Waypoint.push_back(pointarr[i]);


    //确定不同怪物的生命值
    switch (id)
    {
    case 1: //小恐龙怪1
        health = 100;   //生命值
        mwidth = 64, mheight = 64;  //宽高
        ImgPath = "C:/Users/ASUS/Desktop/image/怪物1.png";
        break;
    case 2: //鱼怪2
        health = 120;
        mwidth = 86, mheight = 64;
        ImgPath = "C:/Users/ASUS/Desktop/image/怪物2.png";
        break;
    case 3: //飞龙怪3
        health = 650;
        mwidth = 160, mheight = 120;
        ImgPath = "C:/Users/ASUS/Desktop/image/怪物3.png";
        break;
    case 4: //狮子怪4
        health = 310;
        mwidth = 98, mheight = 70;
        ImgPath = "C:/Users/ASUS/Desktop/image/怪物4.png";
        break;
    case 5: //蜗牛怪5
        health = 200;
        mwidth = 90, mheight = 76;
        ImgPath = "C:/Users/ASUS/Desktop/image/怪物5.png";
        break;
    default:
        break;
    }
}

//怪物按设定路径点移动
bool Monster::Move()
{
    if(Waypoint.empty())
        return true;

    //如果第一个路径点的y小于怪物原本的路径点,则怪物向下走
    if (Waypoint.at(0)->y > my) //下
    {
        my += mspeed;
        return false;
    }

    if (Waypoint.at(0)->x < mx) //左
    {
        mx -= mspeed;
        return false;
    }

    if (Waypoint.at(0)->x > mx) //右
    {
        mx += mspeed;
        return false;
    }

    if (Waypoint.at(0)->y < my) //上
    {
        my -= mspeed;
        return false;
    }

    //如果怪物的坐标和路径点坐标几乎重合,则删除这个路径点
//    if (Waypoint.at(0)->y >= my && Waypoint.at(0)->y <= my + 14 && Waypoint.at(0)->x >= mx && Waypoint.at(0)->x <= mx + 14)
    if (Waypoint.at(0)->y == my && Waypoint.at(0)->x == mx)
    {
        delete Waypoint.begin();                //释放坐标点内存
        Waypoint.erase(Waypoint.begin());       //从数组中删除

//        return false;
    }

    return false;
}


int Monster::GetX() const       //获取横坐标
{
    return mx;
}

int Monster::GetY() const       //获取横坐标
{
    return my;
}

int Monster::GetWidth() const   //获取宽
{
    return mwidth;
}

int Monster::GetHeight() const  //获取高
{
    return mheight;
}

QString Monster::GetImgPath() const //获取图片路径
{
    return ImgPath;
}

int Monster::GetId() const      //获取编号
{
    return id;
}

int Monster::GetHealth() const  //获取生命值
{
    return health;
}

void Monster::SetHealth(int fhealth)//设置生命值
{
    health = fhealth;
}

版权声明:本文为CSDN博主「白家名」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

原文链接:https://blog.csdn.net/qq_46239972/article/details/106073498

【END】

原文出处:微信公众号【白家名 CSDN】

原文链接:https://mp.weixin.qq.com/s/j7wJdR4UTfSpFzRzkdjIBA

本文观点不代表Dotnet9立场,转载请联系原作者。

发表评论

登录后才能评论