Qt -DFS可视化
博客主页:【夜泉_ly】
本文专栏:【暂无】
欢迎点赞👍收藏⭐关注❤️
目录
- 前言
- 关于如何sleep
- 实现思路
- Pixmaps
- pixmaps.h
- pixmaps.cpp
- MapSquare
- mapsquare.h
- mapsquare.cpp
- dfsthread
- dfsthread.h
- dfsthread.cpp
- run dfs
- 其他
- Widget
- Unit
- 其他
Qt -DFS算法可视化
完整代码:https://gitee.com/Ye_Quan/my-cpp-project/tree/master/Qt-experiments/dfs-visualizer
前言
本来,我是在搞我的战棋小游戏的,
如果大家玩过战棋应该知道,
当我们点击一个单位时,
一般会显示出一片区域,
表示这个单位的移动范围(以及可攻击目标等等)。
我当时就想到了用DFS和BFS,
然后就写了。
之后我想,
既然已经在游戏中实现了DFS,
那能不能以现有代码为基础,
写一个DFS算法可视化呢?
感觉很有意思,
所以有了本文。
当然,虽然标题叫做DFS可视化,
但是代码部分大多是在对战棋游戏的实现进行试验,
因此存在很多‘多余’的设计,大家看看就行。
关于如何sleep
既然是可视化,
那必定不能等DFS完了再更新地图状态,
因此我决定在每次更新了一个格子的状态后,
就暂停一会儿再继续DFS。
结果问题来了,
怎么做到每次都暂停一会儿?
我本来想直接在DFS中直接加sleep就好了:
然后程序卡了,
DFS完了才更新界面。
我以为是没用信号触发的问题,
改成用信号触发地格更新:
结果还是不行。
为什么?
最开始我以为和Linux中多线程竞争锁的问题类似,
大概描述一下就是:
抢锁 - 我抢到了! - 我释放 - 欸我离得近,我再抢!
然后发现了一个本质的区别:
这里只有一个线程啊🤣!
问题出在事件队列:
当前这个DFS就是事件队列的一个事件,
而DFS发送的信号所触发的事件,
不过是去事件队列后面排队罢了,
那现在执行的是谁?
还是DFS!
我DFS没跑完,
你们后面的事件都别想跑!
所以用QTimer?
我这是DFS。。。
如果用QTimer,那得搞个stack,
然后用类似非递归版的DFS来做吧?
我最终的解决方案是,
使用子线程跑DFS,
这样就不会阻塞主线程的事件队列了。
实现思路
那么大致分为几个模块
首先是图片模块,
这里搞的单例模式,
用于加载图片给所有人用。
为什么提前加载?
因为不这样做会很卡。。。
然后是地图格模块,
我感觉地图格在战棋游戏中还挺重要的,
所以把很多属性存进去了,
这里只是个DFS可视化,
所有只保留关键的一些属性。
然后是子线程模块,
主要作用是进行DFS和发送信号。
最后就是widget主窗口,
进行初始化地图,处理用户交互等工作。
Pixmaps
比较简单,比较作用就是加载和存储图片。
注意这里不能用饿汉模式,
图片的加载必须放在QApplication对象创建之后。
pixmaps.h
#ifndef PIXMAPS_H
#define PIXMAPS_H
#include <QPixmap>
class Pixmaps {static Pixmaps *getInstance();
private:Pixmaps();Pixmaps(const Pixmaps&) = delete;static void* _instance;
public:QPixmap map0, map1, map2, map3, dst, src;const int map_width = 100, map_height = 100,unit_width = 80, unit_height = 80;
};
#endif // PIXMAPS_H
pixmaps.cpp
#include "pixmaps.h"
void* Pixmaps::_instance = nullptr; // 这个不能放.h -- 会造成重定义问题// 报错会提示在其他包含了这个.h文件中, 重定义了_instance
Pixmaps* Pixmaps::getInstance(){if(_instance == nullptr){// 加锁。。。懒得加了if(_instance == nullptr){_instance = new Pixmaps;}}return (Pixmaps*)_instance;
}Pixmaps::Pixmaps(): map0("://image/map0.png"), map1("://image/map1.png"), map2("://image/map2.png"), map3("://image/map3.png"), map_src("://image/map_src.png"), map_dst("://image/map_dst.png")
{map0 = map0.scaled(map_width, map_height, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);map1 = map1.scaled(map_width, map_height, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);map2 = map2.scaled(map_width, map_height, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);map3 = map3.scaled(map_width, map_height, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);map_dst = map_dst.scaled(map_width, map_height, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);map_src = map_src.scaled(map_width, map_height, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
}
单例,只是为了让别人更方便的用图片罢了,
所以不用搞太复杂 - - 至少这里不用。
MapSquare
地格类
mapsquare.h
#ifndef MAPSQUARE_H
#define MAPSQUARE_H
#include <QLabel>
#include "pixmaps.h"class MapSquare : public QLabel
{Q_OBJECT
public:MapSquare(QWidget *parent = nullptr);enum UNIT_TYPE{UNIT_TYPE_EMPTY = 0,UNIT_TYPE_PLAYER1 = 1,UNIT_TYPE_PLAYER2 = 2};enum STATUS{STATUS_UNCHECKABLE = -1,STATUS_EMPTY = 0,STATUS_MOVE = 1,STATUS_ATTACK = 2,};void setStatus(STATUS new_status, bool update_pixmap = true);void setUnitType(UNIT_TYPE new_type);void setIndex(int r, int c);void setSrc(int r = -1, int c = -1);void setUnit(void* unit = nullptr);STATUS status(){return _status;}UNIT_TYPE unit_type(){return _unit_type;}int r(){return index_r;}int c(){return index_c;}int src_r() {return _src_r;}int src_c() {return _src_c;}void* unit() {return _unit;}
private:UNIT_TYPE _unit_type;STATUS _status;int index_r, index_c;int _src_r = -1, _src_c = -1;void* _unit = nullptr;
};
#endif // MAPSQUARE_H
嗯,基本是直接cv过来的,enum都没改,
毕竟要改的话太麻烦了。
这里保留了我们需要用的,
_unit_type,即地格上面有什么,
无、阵营1(这里就是起点)、阵营2(这里就是终点)。
_status,即地格的状态,
不可达、空
、可抵达
、可攻击
(这里用来表示DFS找到的路径)。
r和c,表示地格在地图中的坐标。
_src_r、_src_c、_unit,这里不用管。
mapsquare.cpp
#include "mapsquare.h"MapSquare::MapSquare(QWidget *parent): QLabel(parent) {int w = Pixmaps::getInstance()->map_width,h = Pixmaps::getInstance()->map_height;setGeometry(-w, -h, w, h);setStatus(STATUS_EMPTY);_unit_type = UNIT_TYPE_EMPTY;
}void MapSquare::setStatus(STATUS new_status, bool update_pixmap){_status = new_status;if(update_pixmap){if(_status == STATUS_UNCHECKABLE){setPixmap(Pixmaps::getInstance()->map0);} else if(_status == STATUS_EMPTY){setPixmap(Pixmaps::getInstance()->map1);} else if(_status == STATUS_MOVE){setPixmap(Pixmaps::getInstance()->map2);} else if(_status == STATUS_ATTACK){setPixmap(Pixmaps::getInstance()->map3);}}
}void MapSquare::setUnitType(UNIT_TYPE new_type) {_unit_type = new_type;
}void MapSquare::setIndex(int r, int c) {index_r = r;index_c = c;
}void MapSquare::setSrc(int r, int c) {_src_r = r;_src_c = c;
}void MapSquare::setUnit(void *unit) {_unit = unit;
}
都是简单的赋值,应该不必多说吧。
dfsthread
用于DFS的线程,本篇的核心。
dfsthread.h
#ifndef DFSTHREAD_H
#define DFSTHREAD_H#include "mapsquare.h"
#include <QObject>
#include <QWidget>
#include <QThread>
#include <QWaitCondition>
#include <QMutex>class DfsThread : public QThread
{Q_OBJECT
public:explicit DfsThread(QWidget *parent = nullptr);void init(QVector<QVector<MapSquare*>>& _map, int _r, int _c, int _max_step);public:void pause();void resume();void oneStep(bool);protected:void run() override;bool dfs(int r, int c, int step, int max_step);signals:void updateSquare(int r, int c, MapSquare::STATUS status);void dstNotFound();private:QVector<QVector<MapSquare*>>* map; // 这里要不断更改指向, 所以不能用引用QVector<QVector<bool>> visited;int src_r, src_c, max_step;int r_map, c_map;static const int dx[4];static const int dy[4];
private:bool one_step = false;bool paused = false;QMutex mutex;QWaitCondition pauseCond;
};#endif // DFSTHREAD_H
讲讲成员变量:
map用来存地图。
visited用来标记当前走过的路径。
src_r, src_c 就是起点坐标。
max_step 是最多走多少步。
r_map, c_map 是地图行、列的数量。
dx,dy 用于控制方向。
嗯,后面几个是用来控制线程的,
为了达到暂停、单步执行的效果,
用到了锁和条件变量。
dfsthread.cpp
run dfs
void DfsThread::run()
{{QMutexLocker locker(&mutex);while (paused) {pauseCond.wait(&mutex);}}if (!map) return;if (false == dfs(src_r, src_c, 0, max_step)){emit dstNotFound();}
}
先检查被暂停没有,然后开始DFS。
bool DfsThread::dfs(int r, int c, int step, int max_step)
{{QMutexLocker locker(&mutex);while (paused) {pauseCond.wait(&mutex);}}
同样,每次DFS前检查一下被暂停没有。
if (step > max_step) return false;if (r < 0 || r >= r_map || c < 0 || c >= c_map) return false;if (visited[r][c]) return false;const QVector<QVector<MapSquare*>>& mapRef = *map;if (mapRef[r][c]->status() == MapSquare::STATUS_UNCHECKABLE) return false;visited[r][c] = true; // 标记为已访问
对当前地格是否可走进行判定,
如果可走,在visited中标记。
接下来的地格将不包括:
// 检查是否找到目标 (不同阵营)if (mapRef[r][c]->unit_type() != MapSquare::UNIT_TYPE_EMPTY &&mapRef[r][c]->unit_type() != mapRef[src_r][src_c]->unit_type()) {// 找到目标!emit updateSquare(r, c, MapSquare::STATUS_ATTACK);mapRef[r][c]->setSrc(src_r, src_c);QThread::msleep(300);return true; // 成功找到路径}
如果找到目标,
发出信号将地格 标记为STATUS_ATTACK
,
并开始返回。
if(mapRef[r][c]->status() != MapSquare::STATUS_MOVE){emit updateSquare(r, c, MapSquare::STATUS_MOVE);mapRef[r][c]->setSrc(src_r, src_c);if(one_step) {pause();} else {QThread::msleep(50); // 暂停一段时间}}
如果没找到,
发出信号将地格 标记为STATUS_MOVE
,
并判断是否是单步执行,
如果是,直接暂停线程,
如果不是,那么休眠50ms,
展现出地格是一步步被改变的,
而不是一下就dfs完了。
for (int i = 0; i < 4; i++) {int x = dx[i] + r;int y = dy[i] + c;if (dfs(x, y, step + 1, max_step)) {// 到了这里, 说明找到了// 那么接着修改地格状态, 并返回 trueemit updateSquare(r, c, MapSquare::STATUS_ATTACK);QThread::msleep(300);return true;}}// 恢复状态visited[r][c] = 0;return false;
}
其他
#include "dfsthread.h"
#include <QDebug>const int DfsThread::dx[4] = {0, 0, 1, -1};
const int DfsThread::dy[4] = {1, -1, 0, 0};DfsThread::DfsThread(QWidget *parent): QThread(parent), map(nullptr)
{
}void DfsThread::init(QVector<QVector<MapSquare*>>& _map, int _r, int _c, int _max_step){map = &_map;src_r = _r;src_c = _c;max_step = _max_step;r_map = _map.size();c_map = _map[0].size();// 这里每次都得重新设置一下visited = QVector<QVector<bool>>(r_map, QVector<bool>(c_map, false));
}void DfsThread::pause(){QMutexLocker locker(&mutex);paused = true;
}void DfsThread::resume(){QMutexLocker locker(&mutex);paused = false;pauseCond.wakeAll();
}void DfsThread::oneStep(bool o){one_step = o;
}
我试了一下,
似乎不加锁和条件变量也行,
不过为了安全还是加上吧。
Widget
UI界面史中史,设计得就是依托。
完整的可以去我的代码仓库看,
这里就不列出来了。
Unit
struct Unit : public QPushButton{Unit(QWidget *parent=nullptr): QPushButton(parent){}int r = -1, c = -1;int move_range = 5;MapSquare::UNIT_TYPE unit_type;
};
这个类本来单独在一个文件里的,被我合过来了。
关于这个类,我写了几个函数:
void initUnit(Unit* unit, MapSquare::UNIT_TYPE unit_type);
void moveUnit(Unit* unit, int r, int c);
void connectUnit(Unit* &unit);
一个是初始化,
一个是移动,
一个是连接信号和槽(继承自按钮,可点击)。
initUnit :
void Widget::initUnit(Widget::Unit *unit, MapSquare::UNIT_TYPE unit_type)
{if(unit_type == MapSquare::UNIT_TYPE_PLAYER1)unit->setIcon(Pixmaps::getInstance()->map_src);else if(unit_type == MapSquare::UNIT_TYPE_PLAYER2)unit->setIcon(Pixmaps::getInstance()->map_dst);elseqDebug() << "初始化单位为未定义类型";unit->setIconSize(QSize(80, 80));unit->setStyleSheet("border : transparent;");unit->unit_type = unit_type;
}
传入一个Unit*,和单位类型,
就能初始化一个单位。
moveUnit
void Widget::moveUnit(Widget::Unit *unit, int r, int c)
{if(unit->r != -1 && unit->c != -1) {map[unit->r][unit->c]->setUnitType(MapSquare::UNIT_TYPE_EMPTY);map[unit->r][unit->c]->setUnit();}unit->setGeometry(map[r][c]->geometry());unit->r = r;unit->c = c;unit->raise();map[r][c]->setUnitType(unit->unit_type);map[r][c]->setUnit(unit);
}
首先得判断是否刚初始化,
如果已经设置过坐标,
那么得先把原地格的状态清空,
(感觉还能封装,地格可以提供一个状态清空函数)
然后才能移动单位,
并重新设置相关属性。
connectUnit
void Widget::connectUnit(Widget::Unit* &unit)
{connect(unit, &QPushButton::clicked, this, [&](){qDebug() << "unit clicked ...";if(dfsThreadIsRunning()) return;MapSquare::STATUS _status = map[unit->r][unit->c]->status();if(_status == MapSquare::STATUS_EMPTY) {resetMap(); // 重置地图dfsThread.init(map, unit->r, unit->c, unit->move_range); // 每次都要初始化dfsThread.start(); // 不能用run, 会阻塞主线程} else {resetMap(); // 重置地图}});
}
单位被点击,
如果当前地格状态为空,
则开始DFS。
如果地格状态为其他的,
暂时不做处理。
其他
connect(&dfsThread, &DfsThread::updateSquare, this, [this](int r, int c, MapSquare::STATUS status) {qDebug() << "子线程来信号了!" << status;map[r][c]->setStatus(status);
});
这个放在构造函数里,
子线程发来信号,
主线程进行地格的状态更新。
希望本篇文章对你有所帮助!并激发你进一步探索编程的兴趣!
本人仅是个C语言初学者,如果你有任何疑问或建议,欢迎随时留言讨论!让我们一起学习,共同进步!