☰
Qt/C++迷宫游戏开发:DFS生成地图与QPainter渲染实战
2026/10/7 14:19:02 网站建设 项目流程

简介:一份基于Qt/C++的迷宫游戏完整工程,适合入门游戏开发人员或Qt学习者研习,也可作为课程设计参考。资源通过QGraphicsView构建2D场景,实现迷宫随机生成、角色移动与通关路径计算;迷宫生成涉及DFS或Prim算法,寻路结合回溯或A*思路,覆盖图形界面、数据结构与算法综合应用。压缩包共27个文件,包含18张迷宫与素材PNG、3个C++源文件、2个头文件,以及UI界面、资源文件和Qt工程配置,整体仅333KB,代码精简适合阅读。工程内角色贴图、路径提示图片与迷宫瓦片齐全,支持键盘控制人物移动,界面布局及规模调整逻辑清晰,便于二次开发,并涉及错误处理与关卡状态管理机制。目前已有961人学习浏览,压缩包内附带完整可运行工程与多尺寸迷宫素材,可直接编译运行或在此基础上扩展关卡、计时等功能,兼具学习与实战价值。

1. 迷宫游戏与 Qt/C++ 这个组合,到底在解决什么问题

想把 C++ 从语法层面推向“能看见、能交互”的实战,迷宫游戏几乎是性价比最高的练手项目。你用 Qt Widgets 搭一个窗口,用 C++ 标准库负责地图生成,再用 QPainter 把格子画出来,一套标准的 Qt 游戏开发闭环就完整了。这个标题里最关键的点不是“迷宫”本身,而是“生成”:迷宫地图必须由代码实时算出来,不能手摆墙体数组,否则就失去了算法训练的意义。适合的人群很明确:学过结构体、数组、递归,但还没碰过 Qt 事件循环和绘图 API 的 C++ 入门者。项目跑通后,你会同时掌握随机算法、坐标换算、键盘事件和界面刷新的基本套路,这比单纯刷十道算法题更让人有“做出来一个东西”的实感。下面从地图生成开始,一层层把代码拆开讲。

2. 用 DFS 递归回溯生成迷宫:先把“可解地图”跑出来

做迷宫游戏最忌讳一上来就画界面,然后往二维数组里随机填墙。随机墙生成的地图大概率没有一条从起点到出口的完整通路,玩两把就开始怀疑人生。正确顺序是先在逻辑层生成一张“保证连通、保证有解”的地图,再考虑怎么把它画出来。这里从业界到开源小游戏,最常用的方案就是把地图切成网格,每个格子维护自己的四面墙,通过不断拆墙连通整张图。

2.1 数据结构:二维 Cell 数组还是像素级墙体

先做一个关键决定:地图用什么结构存。有人习惯直接用int maze[rows][cols],0 表示通道、1 表示墙,然后按照像素把每个格子画成方块。这种做法的坑很隐蔽——墙壁本身占了完整的一格,玩家角色尺寸若小于格子,渲染时就会出现“人站在墙里”的视觉歧义;若让角色占满整格,迷宫尺寸又会被撑得巨大。我一般直接用 Cell 结构体数组:

struct Cell { bool visited = false; // 生成阶段是否访问过 bool walls[4] = { true, true, true, true }; // 上0 右1 下2 左3 };

每个 Cell 只描述自己这一格的四堵墙,墙的“厚度”完全交给渲染层决定。这样算法层和画图层彻底解耦,改格子大小不用动生成逻辑,改生成逻辑也不影响绘制代码。walls数组按下右上左的顺序编号,是为了后面写方向数组时能直接用同一套下标,减少if判断。

如果你以后想做更复杂的迷宫,比如带楼层、带传送门的地图,这个结构也很容易扩展——加一个type枚举就行。

2.2 递归回溯算法:核心代码与随机化处理

地图生成算法里,递归回溯(Recursive Backtracker)是代码量最少、最容易讲清楚的一种。它的本质是带随机性的深度优先搜索:每次从当前格子随机挑一个没访问过的邻居,拆掉中间的墙走过去;没有未访问邻居就原路退回。最终所有格子都被访问,且任意两格之间只有唯一路径,这种迷宫在游戏里叫“完美迷宫”,非常适合做关卡。

#include <vector> #include <algorithm> #include <random> class MazeGenerator { public: int rows, cols; std::vector<std::vector<Cell>> grid; std::mt19937 rng; MazeGenerator(int r, int c, unsigned seed = std::random_device{}()) : rows(r), cols(c), rng(seed) { grid.resize(rows); for (auto& row : grid) row.resize(cols); } void generate() { dfs(0, 0); } private: const int dr[4] = { -1, 0, 1, 0 }; const int dc[4] = { 0, 1, 0, -1 }; void dfs(int r, int c) { grid[r][c].visited = true; std::vector<int> dirs = { 0, 1, 2, 3 }; std::shuffle(dirs.begin(), dirs.end(), rng); for (int d : dirs) { int nr = r + dr[d]; int nc = c + dc[d]; if (nr < 0 || nr >= rows || nc < 0 || nc >= cols) continue; if (grid[nr][nc].visited) continue; // 打通当前格与邻居之间的墙 grid[r][c].walls[d] = false; grid[nr][nc].walls[(d + 2) % 4] = false; dfs(nr, nc); } } };

逻辑说明:std::shuffle每次把四个方向随机打乱,保证地图不是固定规律;(d + 2) % 4是方向反转的快速写法:你拆掉了当前格的上墙,邻居的下墙也要拆掉。这个技巧建议记牢,后面画墙和移动判断还会反复用。

参数方面,rows和cols建议起步设成 21×15,原因是宽高为奇数时,起点和出口可以放在对角格,视觉比例最舒服;seed如果传固定值,比如MazeGenerator gen(21, 15, 20240501),每次跑出来的迷宫都一样,方便复盘问题;不传就用系统随机种子,每局都不同。开发调试阶段我强烈建议先固定种子,否则改渲染代码时地图不断变,你根本没法定焦 bug。

2.3 并查集思路对比:Kruskal 风格迷宫与随机性控制

递归回溯生成的地图有一个明显特征:走廊偏长、分支偏少,玩起来像在走一条拉长的胡同。想做出更像传统迷宫图、分支均匀的效果,可以换用并查集(Union-Find)配合随机边的思路,本质上就是随机 Kruskal 算法——把所有相邻格子之间的墙视为“候选边”,随机挑一条,若这条边连接的两个格子尚不连通,就拆掉它并合并集合。

struct DSU { std::vector<int> parent; DSU(int n) : parent(n) { for (int i = 0; i < n; ++i) parent[i] = i; } int find(int x) { while (parent[x] != x) { parent[x] = parent[parent[x]]; x = parent[x]; } return x; } bool unite(int a, int b) { a = find(a); b = find(b); if (a == b) return false; parent[a] = b; return true; } };

生成时,把所有相邻格子的编号对收集起来,洗牌后逐个尝试合并:

std::vector<std::pair<int, int>> edges; for (int r = 0; r < rows; ++r) { for (int c = 0; c < cols; ++c) { int idx = r * cols + c; if (r > 0) edges.push_back({ idx, idx - cols }); // 上邻居 if (c > 0) edges.push_back({ idx, idx - 1 }); // 左邻居 } } std::shuffle(edges.begin(), edges.end(), gen.rng); DSU dsu(rows * cols); for (auto& [a, b] : edges) { if (!dsu.unite(a, b)) continue; // 把 a、b 之间的墙拆掉:先算两格的行列,再按差值判断方向 }

这段代码比递归回溯多了十几行,但生成的地图特征差异明显:没有特别长的单一路径,分支密集,难度感知更“均匀”。两种算法生成的迷宫对比如下:

维度递归回溯(DFS)并查集(Kruskal)
路径特征长走廊多、分支少分支多、路径短
代码量少中等
生成速度快略慢(边数多)
难度手感容易绕远路路径更近

实际做游戏,我常把两种算法都保留,让玩家选难度时切换,这样同一个渲染层代码能复用,游戏内容量立刻翻倍。需要提醒的是,并查集拆墙时,格子索引到行列的换算别写错:idx / cols是行,idx % cols是列,两格行差为 1 拆上下墙,列差为 1 拆左右墙。

3. 用 QPainter 画迷宫与键盘控制:渲染层的最小实现

地图数据生成好了,下一步是让它出现在窗口里。这一步很多人第一次接触会懵:Qt 里到底该用什么组件画图?其实 Qt Widgets 模块给出的答案非常朴素——继承QWidget,重写paintEvent,然后在事件回调里拿QPainter画线条和色块。整个迷宫游戏的渲染用这一套就够,完全不需要上 QML 或 QGraphicsView。

3.1 弹 QWidget 还是上 QGraphicsView:选型一句话

如果你的迷宫只有几十格,角色就一个方块,没有地图缩放、没有拖拽、没有多物体碰撞,那么QGraphicsView就是杀鸡用牛刀。它虽然自带场景管理和图元动画,但引入的坐标系统转换和 Item 生命周期管理,对小项目反而是负担,调试成本明显上升。QWidget +paintEvent的方式,所有绘制逻辑在一处,状态全在成员变量里,出问题可以直接打断点看坐标。

3.2 坐标换算与墙体绘制:cellSize 参数决定一切

画迷宫前先确定三件事:每个格子多少像素、整个窗口多大、留多少边距。这三者在代码里是一体的:

class MazeWidget : public QWidget { Q_OBJECT public: const int cellSize = 20; // 每个迷宫格子的像素边长 const int margin = 12; // 边框留白,防止墙线被窗口边缘裁掉 MazeWidget(MazeGenerator* gen, QWidget* parent = nullptr) : QWidget(parent), gen(gen) { int w = gen->cols * cellSize + margin * 2; int h = gen->rows * cellSize + margin * 2; setFixedSize(w, h); } protected: void paintEvent(QPaintEvent*) override; private: MazeGenerator* gen; };

setFixedSize直接锁定窗口尺寸,避免用户拖拽窗口导致格子比例失真。20 像素的格子是我在 1080P 屏幕上试过的舒适值,再小到 14 像素,墙体线条和角色方块会挤在一起,玩起来费眼。

绘制墙体时,一个容易忽略的细节是:每个格子画上墙和左墙,右墙和下墙留给相邻格子去画,这样每条线最多画一次。窗口最外圈再单独补两条边界线:

void MazeWidget::paintEvent(QPaintEvent*) { QPainter painter(this); painter.fillRect(rect(), QColor("#F8F6F0")); QPen pen(QColor("#3A3A3A"), 2); pen.setCapStyle(Qt::FlatCap); painter.setPen(pen); for (int r = 0; r < gen->rows; ++r) { for (int c = 0; c < gen->cols; ++c) { const Cell& cell = gen->grid[r][c]; int x = margin + c * cellSize; int y = margin + r * cellSize; if (cell.walls[0]) // 上墙 painter.drawLine(x, y, x + cellSize, y); if (cell.walls[3]) // 左墙 painter.drawLine(x, y, x, y + cellSize); if (c == gen->cols - 1 && cell.walls[1]) // 右边界列 painter.drawLine(x + cellSize, y, x + cellSize, y + cellSize); if (r == gen->rows - 1 && cell.walls[2]) // 下边界行 painter.drawLine(x, y + cellSize, x + cellSize, y + cellSize); } } // 画玩家角色:一个圆角方块 int px = margin + gen->playerCol * cellSize + 3; int py = margin + gen->playerRow * cellSize + 3; painter.fillRect(px, py, cellSize - 6, cellSize - 6, QColor("#E8613C")); }

逻辑说明:wallPen的宽度给 2,能压住浅色背景让墙体更醒目。玩家方块内缩 3 像素是为了视觉上不与墙线重叠。这段代码里最值得留意的是所有坐标都是“格子下标 × cellSize + margin”,这是整个绘制系统唯一的坐标换算公式,写一次以后不再改。

如果你想让迷宫界面更好看,还可以把出口格子先用浅绿色填上、起点用浅蓝色填上,只需要在循环里多画两个fillRect,优先级放在画墙之前。

3.3 键盘移动响应:方向键与步数统计的联动

Qt 窗口要想接收键盘事件,核心是重写keyPressEvent。但这里有个新手常踩的坑:光重写事件不设置焦点,窗口根本收不到按键。解决方案是构造后调用setFocusPolicy(Qt::StrongFocus),让窗口能通过鼠标点击获得焦点。

void MazeWidget::keyPressEvent(QKeyEvent* event) { int d = -1; switch (event->key()) { case Qt::Key_Up: d = 0; break; case Qt::Key_Right: d = 1; break; case Qt::Key_Down: d = 2; break; case Qt::Key_Left: d = 3; break; default: QWidget::keyPressEvent(event); return; } // 检查目标格是否越界、中间是否有墙 int nr = gen->playerRow + dr[d]; int nc = gen->playerCol + dc[d]; if (nr < 0 || nr >= gen->rows || nc < 0 || nc >= gen->cols) return; if (gen->grid[gen->playerRow][gen->playerCol].walls[d]) return; gen->playerRow = nr; gen->playerCol = nc; gen->stepCount++; update(); // 触发重绘,不能用 repaint() }

update()和repaint()的差别是血泪教训:repaint()会立刻同步重绘,连续按键时界面容易闪;update()把重绘请求合并到 Qt 事件循环里,同一帧内多次调用只绘制一次,既防闪烁又不卡输入。这里还要注意,越界与撞墙是两种失败场景,但处理方式相同——直接return,不做任何状态修改。

方向数组dr、dc在MazeWidget里我建议复制一份,不要依赖生成器类里的私有成员。两个类各维护一套方向数组虽然冗余,但避免了访问控制带来的麻烦,编译更干净。

4. 游戏逻辑分离与状态管理:别让界面代码越权

迷宫生成和基础渲染跑通后,最容易写成一团浆糊的是游戏状态逻辑。有个非常典型的坏味道:玩家位置、步数、是否过关全写成MazeWidget的成员变量,渲染、输入、状态混在一起。一旦想加“下一关”按钮,你就得在界面类里到处找变量初始化代码,改完一处漏一处。正确做法是把“游戏状态”单独抽成一个类,界面只负责显示和转发输入。

4.1 逻辑层与渲染层分离:MazeGame 类设计

class MazeGame { public: MazeGenerator* gen; int playerRow, playerCol; // 玩家当前格 int exitRow, exitCol; // 出口格 int stepCount = 0; // 已走步数 bool finished = false; // 是否已通关 MazeGame(MazeGenerator* generator) : gen(generator) { playerRow = 0; playerCol = 0; exitRow = gen->rows - 1; exitCol = gen->cols - 1; } bool tryMove(int d) { if (finished) return false; int nr = playerRow + dr[d]; int nc = playerCol + dc[d]; if (nr < 0 || nr >= gen->rows || nc < 0 || nc >= gen->cols) return false; if (gen->grid[playerRow][playerCol].walls[d]) return false; playerRow = nr; playerCol = nc; stepCount++; if (nr == exitRow && nc == exitCol) finished = true; return true; } };

逻辑说明:tryMove返回bool,界面拿到返回值后决定要不要刷新步数 UI,而不是界面自己去判断撞没撞墙。finished一旦为 true,后续按键全部忽略,避免通关后角色乱跑。这种设计的价值在加计时器时立刻体现:计时只归逻辑层管,界面层只管显示。

4.2 碰撞检测与移动判定:在网格上做,别在像素上做

有人会在keyPressEvent里读取角色像素坐标,然后计算“下一个像素位置是否有墙”。这是把简单问题复杂化——像素坐标需要反过来换算成格子下标,还要考虑角色尺寸和墙线宽度,任何一侧算错 1 像素,角色就会卡墙。我见过的最别扭的 bug 就是角色走到墙边无法前进,但视觉上离墙还有一两像素缝。

正确做法永远是:角色只存在于逻辑网格坐标(playerRow, playerCol),每次移动判断walls[d]是否为 true,与渲染层像素完全无关。渲染时再临时把网格坐标换算成像素坐标去画。这样两个世界彻底隔离,就算你把 cellSize 从 20 改成 40,移动判定依然正确。

4.3 计时、步数与过关判定:QElapsedTimer 的正确打开方式

计时器这块新手常见的错法是QTime::currentTime()在通关时取一次时间、开局时取一次时间,然后相减。这在系统时间被手动修改或网络校时后会出现负数或跳变,体验很怪。Qt 为此提供了专门的高精度单调计时器QElapsedTimer,它的计时不受系统时间调整影响。

// 游戏开始时 QElapsedTimer timer; timer.start(); // 每次界面刷新时,把已用时间转成文本显示 qint64 ms = timer.elapsed(); int totalSeconds = static_cast<int>(ms / 1000); int minutes = totalSeconds / 60; int seconds = totalSeconds % 60; QString timeText = QString("%1:%2") .arg(minutes, 2, 10, QLatin1Char('0')) .arg(seconds, 2, 10, QLatin1Char('0'));

elapsed()返回自start()以来的毫秒数,具体单位在 Qt 5 里是高精度计时器的原始 tick,Qt 提供elapsed()时已经帮你换算成毫秒。.arg(minutes, 2, 10, QLatin1Char('0'))的意思是用 0 填充到两位整数,这比手动拼if (seconds < 10) ...要简洁得多。

另外,步数与时间应该同时作为过关统计显示在窗口顶部。我一般习惯用QLabel显示第 X 步 | 用时 Y,每次移动后调用updateInfo()同步更新。这里要特别小心:更新 UI 文本的代码不能放在paintEvent里,因为绘制事件和按键事件不在同一触发节奏里,容易重复刷新。

4.4 重置与下一关:状态清理的常见漏网之鱼

加了“重新开始”按钮后,最常见的翻车是:迷宫重置了,但计时器没归零;或者计时器归零了,步数还停在上一局。解决思路是把重置逻辑收敛到MazeGame::reset()一个方法里:

void MazeGame::reset() { gen->generate(); // 重新生成地图 playerRow = 0; playerCol = 0; stepCount = 0; finished = false; // 计时器由界面层再次 start() }

按钮的槽函数里只做两件事:调用game->reset(),再调用ui->timeLabel->setText("0:00")。注意不要在槽函数里重新new一个MazeGame,那样所有指向旧对象的指针全部失效,调试起来如同掉进黑匣子。用reset()复用同一个对象,界面持有的指针永远有效。

“下一关”按钮则是把rows、cols按比例加 4 或者缩小 cellSize,调整后调用同一个reset()。因为生成逻辑在reset()里,换尺寸只是改参数,状态清理的坑自然不会踩到。

5. Qt 环境踩坑记录与排查:从编译报错到部署失败

环境问题在 Qt 项目里占掉了新手至少三分之一的时间,而且很多报错信息特别具有迷惑性。这一章把我做 Qt 迷宫游戏时踩过的、以及帮别人排查过的高频问题逐个拆开,每条都按现象、原因、解决三步写,可以直接对照排查。如果你用的是 Qt 5.15.2 配 MSVC2019_64 的套件,前三节几乎一定会遇到至少一个。

5.1 现场一:error: dependent 找不到 Qt 头文件

现象:Qt Creator 构建输出栏弹出一长串,核心内容形如:-1: error: dependent '..\..\..\..\..\..\qt\5.15.2\msvc2019_64\include\qtwidgets\qwidget.h' does not exist,且常见于换过电脑、移动过项目目录或重装过 Qt 之后。

原因:这个报错是 Qt Creator 的构建系统在告诉你,INCLUDEPATH里依赖的 Qt 头文件路径是相对路径,而相对路径的起点变了,找不到了。通常是 .pro 文件里手动写过 Qt 安装路径,或者项目目录整体移动到另一个 Qt 版本不同的机器上,旧路径失效。它不属于代码语法错误,纯属环境配置漂移。

解决:第一步,工具 → 选项 → 构建套件 → 选中当前 Kit,确认编译器、Qt 版本这两个下拉框是否都指向存在的路径,建议重新手动选择一次 Qt 5.15.2 的路径。第二步,找到项目构建目录,例如build-Maze-Desktop_Qt_5_15_2_MSVC2019_64-Debug,直接删除整个文件夹。第三步,回到 Qt Creator,菜单栏 构建 → 清理,然后执行 qmake,最后重新构建。这个“删构建目录再 qmake”的操作能解决九成以上的路径残留问题,属于性价比最高的后悔药。

5.2 现场二:编译通过,但 exe 双击没反应,提示缺 DLL

现象:在 Qt Creator 里按运行一切正常,但从构建目录直接双击Maze.exe,系统弹窗提示找不到Qt5Core.dll、Qt5Widgets.dll等文件。

原因:Qt Creator 运行时会把 Qt 的 DLL 目录临时加入PATH环境变量,所以程序能跑;脱离 Creator 环境后,exe 就找不到依赖库了。这是 Qt 开发新手最容易懵的环节之一,好在 Qt 提供了官方部署工具windeployqt。

解决:

cd build-Maze-Desktop_Qt_5_15_2_MSVC2019_64-Release windeployqt Maze.exe

在 Release 构建目录下执行这条命令,它会把 Maze.exe 依赖的所有 Qt DLL、platforms 插件、styles 插件一并拷贝到 exe 同目录下。之后把这个整个文件夹压缩发送给任何 Windows 机器都能直接运行。注意一定要用 Release 版本部署,Debug 版本的 exe 依赖调试版 DLL,体积大且目标机器上没有对应的 VC 运行库,很容易在别人电脑上继续翻车。

5.3 现场三:qDebug 中文乱码与控制台编码不一致

现象:代码里写qDebug() << "迷宫生成完毕";,应用输出面板显示一堆乱码,比如迷宫生成完毕变成杩疯糠敓鎴愪簡。Windows 默认编码是 GBK,而 Qt Creator 源文件默认 UTF-8,MSVC 编译器如果没指定/utf-8,字符串字面量会被当成当前代码页解释,于是乱码。

原因:MSVC 对源码编码的默认处理与 GCC 不一样,Qt 5 时代这个问题尤其典型。把整个文件另存为 UTF-8 带 BOM 也能解决,但治标不治本,多人协作时别人重新保存一次就复发。

解决:在 .pro 文件里给 msvc 单独加编译选项,这是最干净的做法:

msvc { QMAKE_CXXFLAGS += /utf-8 }

加完记得 构建 → 清理并重新构建,因为编译器选项的变更不会触发自动重新编译旧文件。另外,qDebug()输出的中文本质上只是调试信息,如果要在界面标签上显示中文,用QStringLiteral("迷宫")明确指定其为 UTF-8 编码的宽字符串,绕开源码编码问题。

5.4 现场四:高 DPI 模糊与 QPainter 线条偏移 1px

现象:在 125%、150% 缩放的 Windows 屏幕上,迷宫窗口整体发糊;开着抗锯齿画线,墙体边缘出现半透明灰边,相邻两个格子的墙看起来粗细不一。

原因:Qt 5 默认没有启用高 DPI 缩放,系统按 100% 缩放把窗口画出来再拉伸,自然糊。QPainter 开了Antialiasing后,画线时会尝试把线条边界做半像素平滑,但迷宫墙体本身是严格对齐像素网格的,抗锯齿反而制造出脏边缘。

解决:第一步,在main函数创建QApplication之前强制启用高 DPI 属性:

int main(int argc, char* argv[]) { QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); QApplication app(argc, argv); MazeWidget w; w.show(); return app.exec(); }

第二步,迷宫这种像素对齐的场景,画墙时不要开抗锯齿:

painter.setRenderHint(QPainter::Antialiasing, false);

同时把线宽设为 2 后,注意右墙和下墙的坐标要内缩 1 像素,否则 2 像素宽的线会盖到相邻格子的区域:

if (cell.walls[1]) // 右墙 painter.drawLine(x + cellSize - 1, y, x + cellSize - 1, y + cellSize - 1); if (cell.walls[2]) // 下墙 painter.drawLine(x, y + cellSize - 1, x + cellSize - 1, y + cellSize - 1);

这是个看起来玄学但实际规律很强的问题:凡是做网格类游戏界面,线条都按“宽度的一半”内缩,就不会出现一边粗一边细。

5.5 现场五:100×100 迷宫生成栈溢出崩溃

现象:把迷宫尺寸调到 100×100,运行到生成器直接崩溃,调用栈显示在dfs函数里不断重复嵌套;调回 21×15 又一切正常。这就是典型的递归深度过大导致的栈溢出——递归回溯算法最深的递归层数等于迷宫格子数,10000 层深度足以压爆默认的栈空间。

解决:把递归实现改成显式栈迭代。C++ 的std::vector作为栈,既不依赖调用栈深度,又便于调试查看路径:

void generate() { std::vector<std::pair<int, int>> stack; stack.push_back({ 0, 0 }); grid[0][0].visited = true; while (!stack.empty()) { auto [r, c] = stack.back(); std::vector<int> dirs = { 0, 1, 2, 3 }; std::shuffle(dirs.begin(), dirs.end(), rng); bool moved = false; for (int d : dirs) { int nr = r + dr[d], nc = c + dc[d]; if (nr < 0 || nr >= rows || nc < 0 || nc >= cols) continue; if (grid[nr][nc].visited) continue; grid[r][c].walls[d] = false; grid[nr][nc].walls[(d + 2) % 4] = false; grid[nr][nc].visited = true; stack.push_back({ nr, nc }); moved = true; break; } if (!moved) stack.pop_back(); // 没有可走邻居,回溯 } }

逻辑说明:这段代码与递归版完全等价,区别在于把“当前路径”保存在堆内存的 vector 中,而不是依赖系统栈。moved标志决定是继续深入还是回退。将此版本替换dfs后,即使 1000×1000 的迷宫也能顺利生成,性能瓶颈会转移到渲染层,而不是生成层。这套迭代模板建议背下来,它会出现在你未来写路径规划、拓扑排序等各种场景里。

6. 给迷宫加上 BFS 自动寻路动画:从用到玩到玩得爽

迷宫能玩了,接下来最有成就感的一步是加“提示”功能——按一下 H,自动高亮最短路径。这里用广度优先搜索(BFS)最合适,因为 BFS 天然给出无权图中的最短路径。

在MazeGame里加一个方法:

std::vector<QPoint> findShortestPath() { std::vector<std::vector<bool>> visited(rows, std::vector<bool>(cols, false)); std::vector<std::vector<QPoint>> parent(rows, std::vector<QPoint>(cols, {-1, -1})); std::queue<QPoint> q; q.push({ playerRow, playerCol }); visited[playerRow][playerCol] = true; while (!q.empty()) { auto p = q.front(); q.pop(); if (p.x() == exitRow && p.y() == exitCol) { // 从出口回溯到起点 std::vector<QPoint> path; while (p.x() != playerRow || p.y() != playerCol) { path.push_back(p); p = parent[p.x()][p.y()]; } std::reverse(path.begin(), path.end()); return path; } for (int d = 0; d < 4; ++d) { int nr = p.x() + dr[d], nc = p.y() + dc[d]; if (nr < 0 || nr >= rows || nc < 0 || nc >= cols) continue; if (grid[p.x()][p.y()].walls[d]) continue; if (visited[nr][nc]) continue; visited[nr][nc] = true; parent[nr][nc] = p; q.push({ nr, nc }); } } return {}; }

拿到路径后,在paintEvent里遍历路径格子,用半透明蓝色填充即可。这里给路径上色时只填充格子内部,不覆盖墙体线条,视觉上才能看出“路线”而非“拆墙”。再把路径渲染逻辑用showPath布尔变量控制,按 P 切换提示的显示与隐藏,实现非常简单。

如果想让玩家移动更顺滑而不是一格一格跳,可以用QVariantAnimation插值玩家坐标,但切记:插值只影响渲染坐标,逻辑坐标每次只允许移动一格,否则碰撞检测会被绕过去。我个人的习惯是不做平滑移动,迷宫游戏的核心手感就是“一步一响应”,平滑反而让操作反馈变钝。

这个项目做完,建议你自己留着当工具箱:以后无论是做寻路算法演示、状态机练习,还是 Qt 的界面布局练习,迷宫代码都能直接改造成可视化容器。这个从 0 到通关的过程中,我最大的心得是——涉及 Qt 的每个环节,先做最小可运行版本,再谈封装和美观;哪怕临时把所有变量塞进一个 Widget,至少先跑起来再拆。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询