简介:一款基于C++与QT实现的老鼠走迷宫游戏完整工程,包含可直接运行的EXE及全部源代码,适合学习GUI开发、迷宫生成算法和寻路逻辑的开发者参考。项目支持随机迷宫生成与手动自定义迷宫布局,覆盖深度优先、Prim等生成思路以及A*寻路等关键知识点;迷宫数据使用二维数组存储,界面操作通过事件驱动响应,绘制与刷新则借助QT的2D绘图能力。压缩包共53个文件,以8个cpp、7个头文件、7个ui界面文件为核心构成完整工程,另有27个dll与编译好的可执行文件,解压即可体验或二次修改,包体24.31MB。目前已有291人学习下载。通过阅读源码,可以掌握QT窗口与事件响应、迷宫建模、路径搜索等实践方法,获得一套可直接运行的项目模板,适合课程设计或算法实验;也可通过修改地图数据扩展关卡难度。
1. 老鼠走迷宫游戏:从随机迷宫生成到 EXE 发布,这套 C++/Qt 源码值得照着做一遍
想同时练到 C++ 数据结构、Qt 界面绘制、文件读写和软件发布,老鼠走迷宫是最高性价比的练习项目:规模不大,但正好把随机迷宫生成、自定义迷宫设计、键盘鼠标交互、以及最终打包成 EXE 发布这一整条链路走通。很多初学者卡在跑通例子后就停了,缺少一个能把 QPainter、事件循环、DLL 依赖串起来的东西。这篇文章按实际开发顺序拆解一套基于 C++/Qt 的迷宫游戏:先选迷宫生成算法,再画界面和老鼠,最后用 windeployqt 打包;同时把我在 Qt 5.15.2 下载安装后遇到的几个典型编译和运行问题写成踩坑记录。如果你刚学完 Qt 基础、想拿一个完整项目去巩固,或者准备在简历上放一个带 EXE 可运行的程序,这条路径很适合。
2. 迷宫生成算法选型:递归回溯为什么比 Prim / Kruskal 更适合这个游戏
2.1 为什么默认选递归回溯:完美迷宫、通道长、代码只有二十行
首先要区分“完美迷宫”:任意两点之间只有唯一一条路径,不存在环路,也不存在孤立区域。老鼠走迷宫如果存在多个路径,玩家虽然可以绕路,但自动求解会变复杂;如果存在孤立区域,玩家可能永远无法到达终点。递归回溯(DFS)生成的是标准完美迷宫,而且它倾向于生成比较“长”的单通道,视觉上更像传统迷宫。Prim 和随机 Kruskal 也能生成完美迷宫,但它们的通道分支多、转折频繁,放在小网格里看起来像一堆短线,不够直观。如果你是做迷宫游戏,而不是做迷宫学术研究,递归回溯是默认选择。
另外,递归回溯的代码非常短。核心思路是:把迷宫看成二维网格,所有格子初始为墙,从某个奇数坐标开始,每次向未访问的相邻格(隔两格)移动,打通中间的墙,并递归下去。由于路径是深度优先,它访问完一个分支后才退回来,自然形成了长的走廊。时间复杂度 O(N),N 为格子数,即使 101x101 的迷宫也能瞬间生成。我一般用QVector<QVector<int>>存迷宫,0 表示路,1 表示墙,行数和列数都设为奇数,这样每个路格的上下左右至少有一格空间来做墙。场地四周默认是墙,防止老鼠越界。如果只用相邻格深搜而不留间隔,生成的结果像布料纹理,不是传统迷宫。
从实现成本看,递归回溯只需要一个递归函数和一个方向数组;Prim 需要维护候选墙集合;Kruskal 需要并查集。对于 C++ 初学者,把并查集理解透已经比迷宫本身难了。所以这个项目我强烈建议用递归回溯。
| 算法 | 迷宫类型 | 分支数 | 代码量 | 生成速度 | 适合场景 |
|---|---|---|---|---|---|
| 递归回溯 | 完美迷宫 | 少,长通道 | 约20行 | 很快 | 本游戏 |
| Prim | 完美迷宫 | 多,短通道 | 约40行 | 快 | 需要更多分支 |
| Kruskal | 完美迷宫 | 多,随机性强 | 约50行 | 稍慢 | 学术研究 |
2.2 递归回溯的核心代码:用二维数组存墙,从 (1,1) 开始奇数格深搜
下面给出一个可以在 Qt 项目里直接使用的生成函数。它假设maze是一个QVector<QVector<int>>,行数和列数都是奇数,初始值全部为 1,起点取 (1,1)。这里没有用到 Qt 的随机数类,而是用标准库的mt19937,因为我们需要保存种子来实现迷宫复现。
class MazeGenerator { public: int rows, cols; QVector<QVector<int>> maze; std::mt19937 rng; MazeGenerator(int r, int c) : rows(r), cols(c), maze(r, QVector<int>(c, 1)) {} void resetMaze() { for (int x = 0; x < rows; ++x) for (int y = 0; y < cols; ++y) maze[x][y] = 1; } void generate(int x, int y) { // 将当前路格标记为 0 表示可走 maze[x][y] = 0; // 四个方向:上、右、下、左,每次随机打乱 QVector<QPair<int,int>> dirs = { {0, 2}, {2, 0}, {0, -2}, {-2, 0} }; std::shuffle(dirs.begin(), dirs.end(), rng); for (const auto &d : dirs) { int nx = x + d.first; int ny = y + d.second; if (nx >= 1 && nx < rows - 1 && ny >= 1 && ny < cols - 1 && maze[nx][ny] == 1) { // 打通当前格到目标格之间的墙 maze[x + d.first / 2][y + d.second / 2] = 0; generate(nx, ny); } } } };逻辑说明:每次递归从当前格(x, y)出发,随机打乱四个方向,然后看隔两格的相邻格是否还在迷宫范围内并且仍未访问。如果满足条件,先把两格之间的墙置为 0,再递归进入。这个“隔两格”的技巧是为了保证墙体厚度一致,否则迷宫看起来像蜂窝煤,既不好画也不像路。因为迷宫是墙和路交替排列,起点必须选在奇数坐标(1,1),终点一般选在右下角的奇数坐标,比如(rows-2, cols-2)。这样能确保起点终点都在路格上。
参数说明:rows和cols必须是奇数,比如 41 和 31,这样四周可以留一圈墙壁,且内部路格与墙格交替。rng是随机数引擎,建议用std::mt19937,种子可以由外部传入。递归深度可能达到上千层,Win 和 Linux 默认栈一般够用;如果你把迷宫调到 201x201,遇到栈溢出就该改成显式栈的非递归版本。d.first / 2和d.second / 2会得到 0 或 1,用于计算中间墙坐标。
2.3 随机种子与“新迷宫”按钮:让 QRandomGenerator 和 std::mt19937 配合
Qt 提供了QRandomGenerator,但它的全局实例不适合在这里单独完成所有生成。我一般是拿它来生成一个真正的随机种子,再交给std::mt19937使用:
void MazeWidget::newMaze() { // 从全局随机源取一个 32 位整数作为种子 quint32 seed = QRandomGenerator::global()->generate(); generator.rng.seed(seed); generator.resetMaze(); generator.generate(1, 1); currentSeed = seed; seedSpinBox->setValue(seed); update(); }这里把currentSeed展示在界面的QSpinBox上,用户想看同一张迷宫,就把这个数字填回去。在seedSpinBox的valueChanged信号里,重新做种子赋值、重置和生成即可。注意QRandomGenerator::global()->generate()返回的是quint32,而std::mt19937::seed()需要result_type,可以直接接受quint32。
这里有一个初学者常踩的坑:std::mt19937 rng;默认使用固定种子 5489,如果忘记重新 seed,每次运行生成的迷宫都一模一样。很多人在 Qt 里写std::shuffle,看到结果不变,以为是算法问题,其实只是没换种子。还有一个细节:std::shuffle要求随机数生成器满足均匀分布,而QRandomGenerator是全局对象,如果直接在shuffle里传QRandomGenerator::global(),类型不匹配;所以标准做法还是用std::mt19937。
如果担心递归深度,我一般把generate改为非递归版本:用一个栈保存当前格子,循环执行同样的“随机方向、打墙、入栈”逻辑。这个改动不影响迷宫形态,只是把系统调用栈变成堆上的QStack,超大规模迷宫时更安全。但本项目 41x31 的尺寸完全不需要。
3. 用 QPainter 画出迷宫与老鼠:坐标换算、键盘事件和自定义编辑的实现
3.1 自定义 QWidget 的 paintEvent:用 QPainter 填充墙体,避免用 400 个 QLabel
很多 Qt 初学者遇到迷宫,第一反应是放一个 QGridLayout,然后往每个格子塞 QLabel,再设置背景色。这种方案在小迷宫里可以工作,但迷宫稍大(比如 41x31 就是 1271 个格子)就会变得非常慢,而且代码里全是 UI 控件,后面做编辑模式会很痛苦。正确做法是继承 QWidget,在 paintEvent 里用 QPainter 把整张迷宫一次性画出来。
我一般把每个单元格的实际像素固定为CELL_SIZE = 20,窗口大小就是cols * CELL_SIZE和rows * CELL_SIZE。在 paintEvent 中遍历二维数组,遇到墙就 fillRect:
void MazeWidget::paintEvent(QPaintEvent *) { QPainter painter(this); painter.fillRect(rect(), QColor("#FFF8E7")); // 背景米色 for (int x = 0; x < cols; ++x) { for (int y = 0; y < rows; ++y) { if (maze[x][y] == 1) { painter.fillRect(x * CELL_SIZE, y * CELL_SIZE, CELL_SIZE, CELL_SIZE, QColor("#4A3B32")); } } } // 起点用绿色圆角矩形,终点用红色圆角矩形 painter.fillRect(startX * CELL_SIZE, startY * CELL_SIZE, CELL_SIZE, CELL_SIZE, QColor("#2E7D32")); painter.fillRect(endX * CELL_SIZE, endY * CELL_SIZE, CELL_SIZE, CELL_SIZE, QColor("#C62828")); // 老鼠:黄色圆形 painter.setPen(Qt::NoPen); painter.setBrush(QColor("#F9A825")); painter.drawEllipse(mouseX * CELL_SIZE + 2, mouseY * CELL_SIZE + 2, CELL_SIZE - 4, CELL_SIZE - 4); }逻辑说明:fillRect的坐标是像素坐标,所以要用数组下标乘CELL_SIZE。起点和终点即使被错误地设置在墙上,绘制时也会覆盖在墙上面,这样可以提醒用户这里被标记为不可走。老鼠画在最后,保证它始终可见。每次数据变化后调用update(),Qt 会合并重绘请求;不要用repaint(),它会强制同步刷新,连续按键时容易卡顿。
参数说明:CELL_SIZE可以根据窗口大小动态调整,也可以在构造函数里固定。为了省事,我一般直接在构造函数里setFixedSize(cols * CELL_SIZE, rows * CELL_SIZE),这样省去 resizeEvent 里重新计算坐标的麻烦。如果你想让窗口可缩放,就必须在 resizeEvent 里重新计算CELL_SIZE,并把所有绘制改为基于比例坐标,复杂度会上升。练手项目不建议一开始就做自适应。
3.2 键盘控制老鼠移动:keyPressEvent 里的碰撞检测和边界判断
老鼠走迷宫如果是玩家手动控制,键盘事件是最直接的交互方式。我们需要继承 QWidget 并重写 keyPressEvent:
void MazeWidget::keyPressEvent(QKeyEvent *event) { if (isAutoMode || !gameStarted) { QWidget::keyPressEvent(event); return; } int dx = 0, dy = 0; switch (event->key()) { case Qt::Key_Up: dy = -1; break; case Qt::Key_Down: dy = 1; break; case Qt::Key_Left: dx = -1; break; case Qt::Key_Right: dx = 1; break; default: return; } int nx = mouseX + dx; int ny = mouseY + dy; // 边界检查 if (nx < 0 || nx >= cols || ny < 0 || ny >= rows) return; // 墙体碰撞检查 if (maze[nx][ny] == 1) return; mouseX = nx; mouseY = ny; update(); if (mouseX == endX && mouseY == endY) { emit playerReachedGoal(); } }逻辑说明:键盘每按一次,老鼠移动一格。isAutoMode用于在自动寻路时屏蔽手动操作,否则自动寻路和玩家抢控制权,界面会很乱。gameStarted表示是否已经开始游戏,如果还在设计迷宫阶段,方向键不应该移动老鼠。到达终点后发送信号,主窗口可以弹出“通关”提示,也可以自动跳到下一关。
参数说明:这里的mouseX、mouseY是格子坐标,不是像素坐标。边界检查必须在访问maze[nx][ny]之前,否则数组越界会直接崩溃。如果你希望支持按住方向键连续移动,可以配合QTimer,每次超时调用同一个移动函数;但这样手感会有点肉,通常不是迷宫游戏的首选。我更推荐每按一次走一步,难度的核心在迷宫本身而不是操作响应。
3.3 自定义设计迷宫:mousePressEvent 左键挖墙/砌墙,右键设起点终点
“自定义设计迷宫”是这个项目区别于普通练习作品的亮点。我把它做成了编辑模式:在编辑模式下,鼠标左键点击任意格子,翻转墙/路;右键点击格子设置起点或终点,点击一次是起点,再点一次是终点。设计完成后退出编辑模式,就能开始玩自己设计的迷宫。
void MazeWidget::mousePressEvent(QMouseEvent *event) { if (!editMode) return; int gx = event->pos().x() / CELL_SIZE; int gy = event->pos().y() / CELL_SIZE; if (gx < 0 || gx >= cols || gy < 0 || gy >= rows) return; if (event->button() == Qt::LeftButton) { // 翻转当前格:0 变 1,1 变 0 maze[gx][gy] = (maze[gx][gy] == 0) ? 1 : 0; } else if (event->button() == Qt::RightButton) { // 第一次右键设起点,第二次设终点,循环 if (rightClickTurn == 0) { startX = gx; startY = gy; rightClickTurn = 1; } else { endX = gx; endY = gy; rightClickTurn = 0; } } update(); }逻辑说明:左键的“翻转”操作虽然简单,但要记住:如果起点或终点被翻成墙,用户必须重新用右键设置一次,否则游戏开始后isReachable()会判定无解。右键的rightClickTurn用 int 记录下一次设置谁,这个状态机在单鼠标操作下比较顺手。也可以做成两个独立按钮,一个“设置起点”,一个“设置终点”,各自保存当前点击坐标,避免误操作。
参数说明:event->pos()返回的是窗口内像素坐标,除以CELL_SIZE得到格子下标。编辑模式下,老鼠应该隐藏,我给MazeWidget加了一个showMouse布尔变量,在编辑模式下设为 false,退出编辑模式再设为 true。保存自定义迷宫时,我用文本文件按行写:
- 第一行:
rows cols - 第二行:
startX startY endX endY - 之后每行是
cols个 0/1 数字,用空格分隔。
这个格式简单直观,用QTextStream读写非常方便。如果你需要关卡序列,就在文件头上再加一行关卡编号和名称。
4. 把 Qt 游戏打包成 EXE:windeployqt 步骤与 DLL 依赖的几个检查点
4.1 构建套件选择:MSVC 还是 MinGW,Debug 和 Release 的坑
在 Qt Creator 里新建项目时,默认会看到 MSVC 和 MinGW 两套构建套件。如果你只需要在自己电脑上跑,选哪个都行;但要把 EXE 发给别人,选择会直接影响依赖体积。MSVC 版本打包出来的 EXE 需要目标机器安装 VC++ 运行库,而 MinGW 版本依赖libgcc_s_seh-1.dll、libstdc++-6.dll、libwinpthread-1.dll,这些 DLL 可以直接拷贝到 EXE 目录。我一般选择 MinGW,因为它在“拷贝 DLL”方面比 VC 运行库更简单,不需要让用户单独装一个 Redistributable。
构建配置方面,务必切到 Release,不要发 Debug 给朋友。Debug 版本的 Qt DLL 名字带 d 后缀,比如Qt5Cored.dll,体积大几十倍,而且运行性能差。在 Qt Creator 左下角选择 Release 后,先执行“构建 -> 清理”再重新构建。项目路径不要包含中文,也不要放在带空格的目录里(比如C:\Users\张三\Desktop),否则后面windeployqt处理有概率出问题。我见过不少人因为在中文路径下编译,出现一堆奇怪错误,换到英文路径就正常了。这是玄学,但确实常见。
| 构建套件 | 依赖 | 发布体积 | 目标机器要求 |
|---|---|---|---|
| MSVC 2019 64bit | msvcp140.dll, vcruntime140.dll | 较小(不含 VC redist 时) | 需装 VC++ 2015-2022 Redistributable |
| MinGW 8.1 64bit | libgcc_s_seh-1.dll, libstdc++-6.dll | 略大 | 只需拷贝 DLL,免安装 |
4.2 windeployqt 打包一条龙:目标机器缺少 Qt 环境也能跑
在 Release 构建完成后,产物目录一般是build-MazeGame-Desktop_Qt_5_15_2_MinGW_64_bit-Release\release\MazeGame.exe。打开命令行,进入这个目录,然后执行对应 Qt 版本的windeployqt.exe:
cd /d D:\Projects\build-MazeGame-Desktop_Qt_5_15_2_MinGW_64_bit-Release\release D:\Qt\5.15.2\mingw81_64\bin\windeployqt.exe MazeGame.exe逻辑说明:windeployqt会扫描 EXE 导入的 Qt 模块,自动把Qt5Core.dll、Qt5Gui.dll、Qt5Widgets.dll、platforms/qwindows.dll等复制到同一目录。执行后,整个 release 文件夹就是可分发的最小包。你可以把整个文件夹压缩发给别人,别人机器上即使没有 Qt 也能运行。这一步是“含 EXE 文件”的发布版必须做的,如果你只是把Debug目录里的 exe 拷出来,一定会缺 Qt 插件。
参数说明:如果直接运行windeployqt提示命令不存在,是因为 Qt 的 bin 目录不在 PATH 中,写完整路径即可。执行完成后,检查有没有生成platforms子目录,里面必须有qwindows.dll。如果没有,程序在别人双击时会报qt.qpa.plugin错误。windeployqt不会复制 MinGW 编译器运行时,所以还要从C:\Qt\Tools\mingw810_64\bin拷贝三个 DLL 到 exe 目录。
提示:发布目录里不要混入任何名字带 d 的 DLL,比如
Qt5Cored.dll。这类 Debug 库会和 Release 主程序冲突,出现“无法定位程序输入点”的典型报错。
4.3 手动检查依赖:用 Dependencies 工具找出还缺哪个 DLL
即使执行了windeployqt,也可能因为开发机上装了多个 Qt 版本而漏掉某些 DLL,或者把编译器运行时漏了。我一般会用开源工具 Dependencies(原 Dependency Walker 的现代替代)打开MazeGame.exe,检查是否有红色标记的缺失模块。
常见缺失项:
libgcc_s_seh-1.dll、libstdc++-6.dll、libwinpthread-1.dll:MinGW 编译器运行时,从C:\Qt\Tools\mingw810_64\bin拷贝。vcruntime140.dll、msvcp140.dll:MSVC 运行库,目标机器需要安装 VC++ 2015-2022 Redistributable。
无法定位程序输入点这个报错,通常不是因为 DLL 缺失,而是因为 DLL 版本不对。比如系统 PATH 里有一个旧版的Qt5Core.dll,程序运行时不从 exe 目录加载,反而去 PATH 里找。解决方法是:在 exe 目录下新建qt.conf,强制 Qt 优先使用当前目录的插件和库:
[Paths] Platforms=./platforms这个文件对发布包很重要,可以防止用户解压到其他目录时,程序读了系统环境变量里的 Qt 版本。更彻底的办法是双击运行前,在命令行里执行set PATH=%CD%;%PATH%,让当前目录的 DLL 优先。发布时,我会把 exe、所有 DLL、platforms目录、qt.conf打成一个 zip,在干净虚拟机上测试一次,确认没有依赖系统 Qt。
5. 常见问题排查:QT 版本不兼容、platform 插件缺失、中文路径踩坑记录
5.1 编译期报错:cannot mix incompatible Qt library (version ex50601)
现象:在 Qt Creator 中打开别人的maze_game.pro,编译时出现:
FATAL: cannot mix incompatible Qt library (version ex50601) with this library原因:项目文件里可能指定了不匹配的 Qt 头文件路径,或者你机器上的 qmake 和编译器不是同一套 Qt。比如用 Qt 5.15.2 的 MinGW 编译器去编译一个引用 Qt 5.6 头文件的项目,就会报这个。更常见的是,.pro文件里手写了INCLUDEPATH += C:\Qt\5.12\include,而界面右下角选择的实际上 Qt 5.15.2。
解决:打开.pro文件,删除所有手写的 Qt 路径,只保留Qt += core gui widgets。然后在 Qt Creator 里执行“构建 -> 清理”,删除 shadow build 目录,重新执行 qmake。确认当前构建套件里的 Qt 版本和实际安装版本一致。我一般统一用 QT 5.15.2 + MinGW 8.1 64bit,这套组合在下载安装时一起选上,就不容易混。
5.2 运行期崩溃:qt.qpa.plugin: could not find the qt platform plugin "windows"
现象:编译通过,双击 Release 文件夹里的MazeGame.exe,窗口一闪而过或提示:
qt.qpa.plugin: Could not find the Qt platform plugin "windows" in ""原因:最常见的是没有执行windeployqt,导致platforms目录缺失。另一种情况是,你手动拷贝了 32 位的qwindows.dll,但 exe 是 64 位;或者程序启动时,当前工作目录和 exe 目录不一致,Qt 找不到插件。
解决:先执行一次windeployqt MazeGame.exe,确保platforms/qwindows.dll在 exe 同级。如果已经存在,检查位数:用 Dependencies 打开qwindows.dll,确认是 x64。没有工具的话,在命令行运行dumpbin /headers qwindows.dll。最后,保留 4.3 节提到的qt.conf,内容指定Platforms=./platforms,强迫 Qt 在当前目录找插件,这能解决大多数目录切换问题。
提示:不要直接把系统 Qt 安装目录里的
plugins整个复制过来,里面有大量 theme、imageformats 插件,带过来反而可能因为版本混用引起新的崩溃。
5.3 中文迷宫文件读取乱码:源文件编码与 QTextStream 编码设置
现象:自定义迷宫保存成maze.txt,再读回来时中文关卡名显示成乱码,甚至打开文件直接失败。
原因:Windows 上 Qt 源文件默认编码和文件编码可能不同。MSVC 环境下,源文件中的中文字符串默认按系统代码页(GBK)处理,如果你把源文件保存成 UTF-8,MSVC 会读成乱码。而 MinGW 通常按 UTF-8 处理。写迷宫文件时如果用QFile::writeString默认编码,读的时候再用另一种编码,就会得到乱码。
解决:统一用 UTF-8。读写文件时,用QTextStream显式设置编码:
QFile file(path); if (!file.open(QIODevice::WriteOnly)) return; QTextStream out(&file); out.setCodec("UTF-8"); out << mazeName << "\n";同时让 MSVC 知道源文件是 UTF-8。我习惯在.pro里加一段:
msvc { QMAKE_CXXFLAGS += /utf-8 }MinGW 不需要。如果你的源文件已经是用旧编码写的,最简单的方式是在 Qt Creator 里把文件重新保存为UTF-8 with BOM,MSVC 会自动识别 BOM。文件保存路径也不要写中文,程序运行时用QStandardPaths::AppDataLocation拿到稳定目录,而不是依赖当前目录。
5.4 工具链差异:同一份代码在 MSVC 下编译不过、MinGW 下却正常
现象:在 MinGW 下正常编译运行,切到 MSVC 后,报错C2872: 'byte' ambiguous symbol,或者提示QRandomGenerator不是std成员。
原因:MSVC 对包含顺序敏感,比如先#include <windows.h>再#include <QPainter>,windows.h会定义byte宏,与std::byte冲突。另外,Qt 5.15 的QRandomGenerator需要开启 C++17 支持,MSVC 默认可能不是 C++17。MinGW 的 GCC 对新特性的默认支持更宽松,所以在 MinGW 下没有触发。
解决:在.pro里加CONFIG += c++17。调整头文件顺序,把 Qt 头文件放在最前面,Windows 相关头文件放在最后。如果项目依赖QRandomGenerator,还要确保 MSVC 的 SDK 版本包含该头文件,Windows 10 SDK 一般没问题。我日常开发用 MinGW,发布前用 MSVC 验证一遍雷同代码,这样两边都能过,发布给不同用户更安心。
5.5 调试器起不来:CDB 或 GDB 配置错误导致无法启动程序
现象:在 Qt Creator 中按 F5,提示“无法启动程序,系统找不到指定的文件”,或者“调试器未安装”。
原因:构建套件选的是 MSVC,但调试器只装了 GDB;或者 MinGW 套件配了 CDB。Qt Creator 在 Windows 上,MSVC 首选 CDB,MinGW 首选 GDB,它们不能混用。另一个原因是,你选择的是 Release 构建,但 Qt Creator 尝试附加调试器,Release 优化会让断点无法命中,有些人会误以为调试器坏了。
解决:打开“工具 -> 选项 -> Kits -> 调试器”,确认当前套件对应路径。MinGW 调试器通常为C:\Qt\Tools\mingw810_64\bin\gdb.exe。MSVC 需要安装 Windows SDK 的 Debugging Tools,然后在 Qt Creator 中手动添加 CDB 路径。平时调试用 Debug 模式,只有打包发布才切 Release。如果切换后仍然报错,把构建目录里的.user文件删掉,重新配置一次套件,通常能解决自动检测不到调试器的问题。
6. 进阶技巧:用 BFS 验证迷宫连通性,并实现自动寻路回放
6.1 BFS 连通性验证:自定义迷宫绘图完成后先跑一遍,防止无解
自定义迷宫最大的风险是:玩家画了半天,起点和终点之间根本没有道路。所以我在保存和开始游戏之前,都会跑一遍 BFS 验证。
bool MazeWidget::isReachable() { QQueue<QPoint> queue; QVector<QVector<bool>> visited(rows, QVector<bool>(cols, false)); queue.enqueue(QPoint(startX, startY)); visited[startX][startY] = true; int dx[4] = {0, 1, 0, -1}; int dy[4] = {1, 0, -1, 0}; while (!queue.isEmpty()) { QPoint cur = queue.dequeue(); if (cur.x() == endX && cur.y() == endY) return true; for (int i = 0; i < 4; ++i) { int nx = cur.x() + dx[i]; int ny = cur.y() + dy[i]; if (nx >= 0 && nx < cols && ny >= 0 && ny < rows && maze[nx][ny] == 0 && !visited[nx][ny]) { visited[nx][ny] = true; queue.enqueue(QPoint(nx, ny)); } } } return false; }逻辑说明:从起点向四个方向扩散,走不到终点就返回 false。这个函数在“开始游戏”按钮触发时调用,如果无解就弹窗提示,不让玩家进入一个不可能通关的局。随机迷宫因为算法保证连通,正常不会触发,但如果你在自定义模式下误操作,这就是后悔药。
6.2 自动寻路回放:用 QTimer 按 BFS 路径逐步移动老鼠
我们可以把 BFS 的父节点记录做出来,然后从终点回溯得到路径。用QTimer每隔 100 毫秒把老鼠移动到路径下一个格子,就实现了“自动走迷宫”的演示效果。
QTimer *timer = new QTimer(this); connect(timer, &QTimer::timeout, this, [=]() { if (pathIndex >= pathList.size()) { timer->stop(); emit playerReachedGoal(); return; } mouseX = pathList[pathIndex].x(); mouseY = pathList[pathIndex].y(); ++pathIndex; update(); }); timer->start(100);逻辑说明:我通常把isAutoMode在回放开始前设为 true,回放结束后恢复 false。否则用户按下方向键,老鼠位置和pathList就会错位,看起来像抽风。回放速度可以接一个QSlider,改变timer->start(interval)里的间隔,但不建议小于 50 毫秒,否则视觉上变成瞬移,失去了“走迷宫”感。
我自己做这一类 Qt 练手项目时,最大的教训是:不要等写完再验证,而是每加一个功能都保留“一键校验”的入口。迷宫是游戏,但它背后全是数据和状态,把校验当游戏功能本身的一部分,能省下大量调试时间。希望帮到你。
本文还有配套的精品资源,点击获取