博客主页:【夜泉_ly】
本文专栏:【暂无】
欢迎点赞👍收藏⭐关注❤️
目录
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)) {
// 到了这里, 说明找到了
// 那么接着修改地格状态, 并返回 true
emit 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);
else
qDebug() << "初始化单位为未定义类型";
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语言初学者,如果你有任何疑问或建议,欢迎随时留言讨论!让我们一起学习,共同进步!