计算机软件实习日志(三)基于 A*搜索算法迷宫游戏开发


界面展示

主界面
在这里插入图片描述
选择关卡界面
在这里插入图片描述


前言

迷宫大师是本人的计算机软件实习的实验,是一个可视化的迷宫小游戏。可视化界面基于Qt5,使用Qt Creator开发。

项目主要有如下特点:

  1. 对Qt自带的控件进行了二次封装,以实现更加美观的游戏效果;
  2. 为了更方便的设计关卡,配套开发了可视化的迷宫地图编辑器;
  3. 迷宫游戏实现了文件的读写,实现了导出和读取玩家自制地图,并加入了通过DFS绘制迷宫出路的功能;

一、实验要求?

  1. 迷宫游戏是非常经典的游戏,在该题中要求随机生成一个迷宫,并求解迷宫;
  2. 要求查找并理解迷宫生成的算法,并尝试用两种不同的算法来生成随机的迷宫。
  3. 要求游戏支持玩家走迷宫,和系统走迷宫路径两种模式。
    玩家走迷宫通过键盘方向键控制,并在行走路径上留下痕迹;
    系统走迷宫路径要求基于 A*算法实现,输出走迷宫的最优路径并显示。
  4. 设计交互友好的游戏图形界面。

二、实验准备

  1. 理解什么是A-Star算法,并深刻学习此算法。
  2. 思考怎么生成迷宫。
  3. 利用Qt GUI框架对界面进行构建。

三、设计思路

设计图

在这里插入图片描述

1、A*算法的理解

A算法,A(A-Star)算法是一种静态路网中求解最短路径最有效的直接搜索方法,也是解决许多搜索问题的有效算法。算法中的距离估算值与实际值越接近,最终搜索速度越快。

算法描述
简化搜索区域

将待搜索的区域简化成一个个小方格,最终找到的路径就是一些小方格的组合。当然是可以划分成任意形状,甚至是精确到每一个像素点,这完全取决于你的游戏的需求。一般情况下划分成方格就可以满足我们的需求,同时也便于计算。
如下图区域,被简化成6*6的小方格。其中绿色表示起点,红色表示终点,黑色表示路障,不能通行。
在这里插入图片描述

简化地图
概述算法步骤

先描述A*算法的大致过程:

  1. 将初始节点放入到open列表中。
  2. 判断open列表。如果为空,则搜索失败。如果open列表中存在目标节点,则搜索成功。
  3. 从open列表中取出F值最小的节点作为当前节点,并将其加入到close列表中。
  4. 计算当前节点的相邻的所有可到达节点,生成一组子节点。对于每一个子节点:
    (1)如果该节点在close列表中,则丢弃它
    (2)如果该节点在open列表中,则检查其通过当前节点计算得到的F值是否更小,如果更小则更新其 F值,并将其父节点设置为当前节点。
    (3)如果该节点不在open列表中,则将其加入到open列表,并计算F值,设置其父节点为当前节点。
  5. 转到2步骤
进一步解释

初始节点,目标节点,分别表示路径的起点和终点,相当于上图的绿色节点和红色节点
F值,就是前面提到的启发式,每个节点都会被绑定一个F值
F值是一个估计值,用F(n) = G(n) + H(n) 表示,其中G(n)表示由起点到节点n的预估消耗,H(n)表示节点n到终点的估计消耗。H(n)的计算方式有很多种,比如曼哈顿H(n) = x + y,或者欧几里得式H(n) = sqrt(x^2 + y^2)。本例中采用曼哈顿式。
F(n)就表示由起点经过n节点到达终点的总消耗
为了便于描述,本文在每个方格的左下角标注数字表示G(n),右下角数字表示H(n),左上方数字表示F(n)。具体如何计算请看下面的一个例子

具体寻路过程

接下来,我们严格按照A*算法找出从绿色节点到红色节点的最佳路径
首先将绿色节点加入到open列表中
接着判断open列表不为空(有起始节点),红色节点不在open列表中
然后从open列表中取出F值最小的节点,此时,open列表中只有绿色节点,所以将绿色节点取出,作为当前节点,并将其加入到close列表中
计算绿色节点的相邻节点(暂不考虑斜方向移动),如下图所示的所有灰色节点,并计算它们的F值。这些子节点既没有在open列表中,也没有在close列表中,所以都加入到open列表中,并设置它们的父节点为绿色节点

F值计算方式:

以绿色节点右边的灰色节点为例,
G(n) = 1,从绿色节点移动到该节点,都只需要消耗1步
H(n) = 3,其移动到红色节点需要消耗横向2步,竖向一步,所以共消耗3步(曼哈顿式)
F(n) = 4 = G(n) + H(n)

2、开发思路

使用Qt GUI框架

Qt是一个成熟的跨平台的C++图形用户界面应用程序框架。选择Qt的作为GUI框架相对于选择OpenGL可以节省很多不必要的开发时间,提升开发效率的同时保证稳定性。Qt提供了一些工具类,比如QString,QDebug等。笔者选择使用Qt自带的这些类而不是C++本身提供的string,cin,cout这些工具类,以保证程序内部信息流交互更加方便。

封装图片按钮类 ImgButton

我们手动封装一个自己的图片按钮类:ImgButton;这个类继承QPushButton。
利用这个类可以自己用Photoshop制作的按钮图片素材,我们可以实现比Qt自带的QPushButton更美观的按钮,还可为按钮添加音效。
部分按钮素材如下:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

主窗口(标题界面)MainWindow

主窗口将选关窗口和地图编辑窗口作为数据成员,便于进行与这些界面间的切换。

封装地图图块类 Tile

我们继承QPushButton封装一个地图图块类Tile,之后利用这个类和图片素材绘制迷宫地图。利用网上的图像素材,我制作的地图图块如下(按顺序为墙、路):
在这里插入图片描述
在这里插入图片描述

迷宫数据工具类 MazeData

设计迷宫数据文件:Unicode编码的文本文件,第一行n为迷宫大小(迷宫宽高相等),之后n行是迷宫地图信息。

如:

15
000000000000020
010111011111010
010101010101010
010101110101110
010000010100010
011111010111010
010001000001010
010101111101010
010100000101010
010111110111010
010000010000010
011111011111010
000001000100010
011111110111110
030000000000000

为了方便我们在编辑窗口和游玩窗口快速读写迷宫文件,设计了一个迷宫数据工具类MazeData,利用二维数组来保存迷宫数据。

地图编辑窗口 MapEditWindow

加入地图编辑窗口,既方便笔者自己制作迷宫关卡,也为玩家提供了扩展游戏玩法和难度的可能性。通过封装的Tile类,可以实现鼠标点击绘制自己喜爱的地图。

选关窗口 ChooseLevelWindow

选关窗口将游戏窗口为该类的数据成员,便于窗口间切换。除了内置关卡,笔者在选关窗口增加了读取地图的按钮,以期拓展游戏的可玩性。

游戏窗口 PlayWindow

游戏窗口是游戏的核心窗口。游戏窗口实现玩家移动功能、判定是否存在通路功能、绘制通路功能、游戏倒计时功能,还增加了成功或者失败时的简单动画。

地图编辑窗口类包含了本程序的另一个难点:A*搜索迷宫通路(笔者采用了利用栈的非递归的实现)。这个算法使用在此类的findPosssibleWay成员函数里。算法的思路如下:

  1. 首先起点入栈pos_stack,只要pos_stack中有值,说明还有待遍历的位置,继续遍历
  2. 进入循环体,说明该点被遍历,该点加入possible_path栈
  3. 判断该点是否为出口,如果是,已经发现一条可行路线,返回true`,函数结束
  4. 如果循环向下执行,说明不是终点,将该点标记为已经走过
  5. 从该点探索与该点连接的,其他可走的位置,入栈
  6. 如果没有任何点入栈,说明是死路或者正在回退中,进行一步回溯,即出栈
  7. 如果离开循环体,说明没有通路,返回false

以上为各模块功能介绍及设计思路,具体实现见源代码。


四、源代码

以下仅放上部分源代码,如想要完整代码请联系邮箱:zlb15185602324@qq.com 。

chooselevelwindow.h代码如下:

#ifndef CHOOSELEVELWINDOW_H
#define CHOOSELEVELWINDOW_H

#include <QMainWindow>
#include "playwindow.h"

namespace Ui {
class ChooseLevelWindow;
}

class ChooseLevelWindow : public QMainWindow
{
    Q_OBJECT

public:
    //游玩窗口
    PlayWindow * playWindow = NULL;

    explicit ChooseLevelWindow(QWidget *parent = nullptr);
    ~ChooseLevelWindow();

    void paintEvent(QPaintEvent *);

private:
    Ui::ChooseLevelWindow *ui;

signals:
    void backBtnClicked();
};

#endif // CHOOSELEVELWINDOW_H

imgbutton.h代码如下:

#ifndef IMGBUTTON_H
#define IMGBUTTON_H

#include <QPushButton>
#include <QSound>

class ImgButton : public QPushButton
{
    Q_OBJECT
public:
    QString normalImgPath; //保存默认未按下时的图片路径
    QString pushedImgPath; //保存默认按下时的图片路径

    //构造方法,若按下和普通状态图像相同可以缺省按下时的图片路径
    ImgButton(QString normalImgPath, QString pushedImgPath = "");

    void jumpUp(); //按钮向上移动动画
    void jumpDown(); //按钮向下移动动画
    void mousePressEvent(QMouseEvent *); //重写鼠标按下事件
    void mouseReleaseEvent(QMouseEvent *); //重写鼠标释放事件

private:
    QSound * sound = NULL; //按钮音效

signals:

};

#endif // IMGBUTTON_H

mainwindow.h代码如下:

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include "chooselevelwindow.h"
#include "mapeditwindow.h"

QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    //选关窗口
    ChooseLevelWindow * chooseWindow = NULL;
    //编辑地图窗口
    MapEditWindow * editWindow = NULL;

    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

    //重写paintEvent,绘制背景图
    void paintEvent(QPaintEvent *);

private:
    Ui::MainWindow *ui;
};
#endif // MAINWINDOW_H

mapeditwindow.h代码如下:

#ifndef MAPEDITWINDOW_H
#define MAPEDITWINDOW_H

#include <QMainWindow>
#include <QVector>
#include "tile.h"

namespace Ui {
class MapEditWindow;
}

class MapEditWindow : public QMainWindow
{
    Q_OBJECT

public:
    explicit MapEditWindow(QWidget *parent = nullptr);
    ~MapEditWindow();

    //绘制背景图
    void paintEvent(QPaintEvent * e);

private:
    Ui::MapEditWindow *ui;
    int mazeWidth = 15; //迷宫宽度(大小),默认15
    Tile * tiles[31][31]; //图块

    void createTiles();//在窗口加载地图图块
    QVector< QVector<int> > createMaze(int width); //利用DFS随机生成迷宫地图

signals:
    void backBtnClicked();
};

#endif // MAPEDITWINDOW_H

mazedata.h代码如下:

#ifndef ABSTRACTMAZE_H
#define ABSTRACTMAZE_H

#include <QString>

class MazeData
{
public:
    int mazeData [31][31];//状态:0为墙,1为普通通路,2为出发点,3为目的地
    int mazeSize;//迷宫的大小

    MazeData();
    MazeData(const int & mSize, const int mData[31][31]);
    void loadMaze(const QString & path);//根据文件路径读取迷宫
    void saveMaze(const QString & path);//保存迷宫数据文件到指定位置
};

#endif // ABSTRACTMAZE_H

playwindow.h代码如下:

#ifndef PLAYWINDOW_H
#define PLAYWINDOW_H

#include <QMainWindow>
#include <QSound>
#include <QTimer>
#include "mazedata.h"
#include "tile.h"

namespace Ui {
class PlayWindow;
}

class PlayWindow : public QMainWindow
{
    Q_OBJECT

public:
    int lastTime; //剩余时间

    PlayWindow(QString dataPath);
    ~PlayWindow();

    void paintEvent(QPaintEvent *); //绘制背景图
    void keyPressEvent(QKeyEvent *);//重写keyPressEvent监听键盘事件

    //非递归深度优先搜索实现寻路,如果有通路,返回true
    bool findPossibleWay(bool drawPath); //参数设置true,会绘制可行路线,否则不绘制

private:
    Ui::PlayWindow *ui;
    MazeData * mazeData = NULL; //迷宫数据工具
    Tile * tiles[31][31]; //地图图块
    int posX, posY, startX, startY, endX, endY; //当前位置、起点和终点的坐标
    bool isStopped = false; //游戏是否结束
    QSound * winSound = NULL, * failSound = NULL; //胜利和失败音效
    QTimer *  timer1 = NULL; //关卡计时器

    void stopGame(bool isWon); //结束游戏函数,参数表示是否胜利

signals:
    void backBtnClicked();
};

#endif // PLAYWINDOW_H

tile.h代码如下:

#ifndef TILE_H
#define TILE_H

#include <QPushButton>
#include <map>

class Tile : public QPushButton
{
    Q_OBJECT
public:
    const static int WALL = 0;
    const static int PATH = 1;
    const static int STARTING = 2;
    const static int ENDING = 3;

    int status; //状态:0为墙,1为普通通路,2为出发点,3为目的地
    int isEditable;
    std::map<int, QString> statusToPath;

    //构造方法,默认不可以编辑(即按钮无效)
    //如果可以编辑,点击按钮会切换图片,用于编辑地图
    Tile(int status, bool isEditable = false);

    void mousePressEvent(QMouseEvent * e);
    void setSize(int Size);
    void changeStatus();//无参改变状态,状态+1;如果比3大,会重置为0
    void changeStatus(int status);//指定状态

signals:

};

#endif // TILE_H

chooselevelwindow.cpp代码如下:

#include "chooselevelwindow.h"
#include "ui_chooselevelwindow.h"

#include <QPainter>
#include <QTimer>
#include <QLabel>
#include <QStyle>
#include <QFileDialog>
#include <QDebug>
#include "imgbutton.h"

ChooseLevelWindow::ChooseLevelWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::ChooseLevelWindow)
{
    ui->setupUi(this);

    //返回按钮
    ImgButton * backBtn = new ImgButton(":/res/backBtn_1.png", ":/res/backBtn_2.png");
    backBtn->setParent(this);
    backBtn->move(600+34, this->height()*0.8+60);
    backBtn->show();
    connect(backBtn, &ImgButton::clicked, [=](){
        QTimer::singleShot(100, [=](){
            emit(backBtnClicked());
        });
    });

    //读取地图按钮
    ImgButton * loadBtn = new ImgButton(":/res/loadBtn_1.png", ":/res/loadBtn_2.png");
    loadBtn->setParent(this);
    loadBtn->move(450+34, this->height()*0.8+60);
    loadBtn->show();
    connect(loadBtn, &ImgButton::clicked, [=](){
        QTimer::singleShot(100, [=](){
            QString path = QFileDialog::getOpenFileName(this,"读取迷宫地图","./","(*.mazedata)");
            //返回值不为空说明成功选取迷宫文件
            if(path!=NULL)
            {
                this->hide();
                playWindow = new PlayWindow(path);
                playWindow->setGeometry(this->geometry());
                playWindow->show();

                //监听游戏窗口的返回按钮点击事件
                connect(playWindow, &PlayWindow::backBtnClicked, [=](){
                    playWindow->hide();
                    setGeometry(playWindow->geometry());
                    delete playWindow;
                    playWindow = NULL;
                    this->show();
                });
            }
        });
    });

    //关卡选择按钮生成
    for (int i = 0; i < 16; i++)
    {
        ImgButton * tempBtn = new ImgButton(":/res/chooseBtn_1.png", ":/res/chooseBtn_2.png");
        tempBtn->setParent(this);
        tempBtn->move(145+(i%4)*150, 90+(i/4)*100);
        tempBtn->show();

        //绑定按钮点击事件
        connect(tempBtn, &ImgButton::clicked, [=](){
            tempBtn->jumpDown();
            tempBtn->jumpUp();
            QTimer::singleShot(100, [=](){
                this->hide();
                playWindow = new PlayWindow(QString(":/mazedata/").append(QString::number(i+1)).append(".mazedata"));
                playWindow->setGeometry(this->geometry());
                playWindow->show();

                //监听游戏窗口的返回按钮点击事件
                connect(playWindow, &PlayWindow::backBtnClicked, [=](){
                   playWindow->hide();
                   setGeometry(playWindow->geometry());
                   delete playWindow;
                   playWindow = NULL;
                   this->show();
                });
            });
        });

        //设置按钮上的数字标签
        QLabel * label = new QLabel;
        label->setParent(this);
        label->setFixedSize(tempBtn->width(),tempBtn->height());
        label->setText(QString::number(i+1));
        label->setAlignment(Qt::AlignHCenter | Qt::AlignVCenter); //设置居中
        label->move(145+(i%4)*150, 90+(i/4)*100);
        label->setAttribute(Qt::WA_TransparentForMouseEvents,true);  //鼠标事件穿透
        label->setFont(QFont("黑体",13));
        //label->setStyleSheet("color:white;");
    }
}

ChooseLevelWindow::~ChooseLevelWindow()
{
    delete ui;
}

void ChooseLevelWindow::paintEvent(QPaintEvent *e)
{
    QPainter painter(this);
    QPixmap pix;
    pix.load(":/res/background2.jpg");
    painter.drawPixmap(0,0,pix);
}

  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值