
简介一份基于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::vectorstd::vectorCell 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::vectorint 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::vectorint 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::vectorstd::pairint, 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_castint(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 里是高精度计时器的原始 tickQt 提供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-8MSVC 编译器如果没指定/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::vectorstd::pairint, int stack; stack.push_back({ 0, 0 }); grid[0][0].visited true; while (!stack.empty()) { auto [r, c] stack.back(); std::vectorint 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::vectorQPoint findShortestPath() { std::vectorstd::vectorbool visited(rows, std::vectorbool(cols, false)); std::vectorstd::vectorQPoint parent(rows, std::vectorQPoint(cols, {-1, -1})); std::queueQPoint 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::vectorQPoint 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至少先跑起来再拆。希望帮到你。本文还有配套的精品资源点击获取