当前位置: 首页 > news >正文

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语言初学者,如果你有任何疑问或建议,欢迎随时留言讨论!让我们一起学习,共同进步!

相关文章:

  • 91.首次使用Maui的体验与建议 C#例子 Maui例子
  • 系统思考:局部最优与全局失衡
  • windows远程服务器数据库的搭建和远程访问(Mysql忘记密码通过Navicat连接记录解密密码)
  • 量化交易之数学与统计学基础2.3——线性代数与矩阵运算 | 线性方程组
  • 加速LLM大模型推理,KV缓存技术详解与PyTorch实现
  • 车辆检测新突破:VFM-Det 如何用大模型提升识别精度
  • Spring 框架中的常见注解讲解
  • Chromium 134 编译指南 - Android 篇:配置depot_tools(四)
  • 工业控制「混合架构」PK大战 —— 神经网络 + MPC vs 模糊 PID+MPC 的场景选型与实战指南
  • vscode 个性化
  • 深入探讨互联网大厂Java核心技术与架构设计
  • C++继承(上)
  • conda管理python环境
  • Walrus 与 Pudgy Penguins 达成合作,为 Web3 头部 IP 引入去中心化存储
  • 字节暑期实习-网络运维工程师面经
  • 规划权重和全局优化器逻辑处理
  • Copilot for Excel 一键词云分析与情绪分析
  • 第四部分:实用应用开发
  • C# 类成员的访问:内部与外部
  • 嵌入式开发高频面试题全解析:从基础编程到内存操作核心知识点实战
  • 美乌矿产协议预计最早于今日签署
  • 中方拟解除对5名欧洲议会议员制裁?外交部:望中欧立法机构相向而行
  • 美参议院通过新任美国驻华大使任命,外交部回应
  • 4月制造业PMI为49%,比上月下降1.5个百分点
  • 中国人保一季度业绩“分化”:财险净利增超92%,寿险增收不增利
  • 解读|特朗普“助攻”下加拿大自由党“惨胜”,卡尼仍需克服“特鲁多阴影”