ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Qt/C++迷宫游戏开发:从生成算法到QPainter绘制实战

Qt/C++迷宫游戏开发:从生成算法到QPainter绘制实战 简介基于跨平台图形界面框架Qt与面向对象语言C实现的迷宫游戏完整工程源码面向广大游戏开发初学者及需要课程设计项目的计算机专业学生能够解决随机迷宫生成、角色移动与自动寻路等经典问题。压缩包内共二十七个文件其中包含三份C源码文件、两份头文件、一份用户界面设计文件以及十八张PNG迷宫素材整体大小约三百三十三KB代码结构清晰便于快速编译与学习。工程实现了深度优先搜索与普里姆算法的迷宫生成并通过回溯法或A星寻路自动计算通关路径结合QGraphicsView场景机制支持键盘控制与规模调节。资源附带界面布局文件与资源配置文件方便二次开发与界面定制目前已有九百六十一人学习适合作为Qt入门学习或课程设计的实战参考。1. 用 Qt 和 C 写一个迷宫游戏为什么说选对生成算法比画图更重要拿“Maze_qt迷宫_迷宫qt代码_qt生成迷宫_QT游戏”这个选题练手的人多半是冲着两件事来的一是想验证 C 的数据结构有没有学扎实二是想把 Qt 的界面、绘图、事件响应整个串起来。老实说迷宫游戏做出来不难但网上能直接抄的“完整方案”很少多数代码不是只有生成没有交互就是给出的 Qt 版本和本地环境对不上编译那一步就卡住劝退了。这篇笔记不给你抄整个工程而是把迷宫生成、Qt 绘制、玩家碰撞、自动寻路四条线逐步拆开每段代码都可以直接落进你自己的项目里。适合正在做课程设计、或者想在 Qt/C 里找一个小而完整的练手项目的人。你会发现最花时间的不是画界面而是生成算法的选择和寻路数据结构的设计。2. 迷宫生成算法怎么选从 DFS 递归回溯到随机 Prim2.1 先把迷宫变成一组二维数组墙与路的比特位建模写生成算法之前得先确定迷宫在内存里的数据结构。常见做法是开一个二维数组每个元素表示一个格子其中奇数下标表示“路”偶数下标表示“墙”。比如一个 21×21 的数组实际迷宫是 10×10 的路外面包一圈墙。这样建模的好处是墙和路都是格子绘制时可以直接按坐标画矩形不用额外维护墙体列表。另一种数据结构是“格子 墙集合”即把迷宫看成 N 个连通节点候选墙是节点之间的边。这种结构更贴近 Kruskal 和 Prim 的算法描述但写起来比数组麻烦而且 Qt 绘制时还是要转成像素坐标。所以我建议除了一开始定长宽后面所有算法都直接在这个二维数组上做。// maze_data.h #include vector #include cstdint // 用 0 表示墙1 表示路 using MazeGrid std::vectorstd::vectorint; // 迷宫尺寸cols 和 rows 必须是奇数保证路格子存在 struct MazeSize { int cols; // 列数含墙 int rows; // 行数含墙 int roadCount() const { return (cols / 2) * (rows / 2); } }; // 方向数组上、右、下、左 // 用于从当前路格子向相邻路格子打通墙 const int DIRS[4][2] { {-1, 0}, {0, 1}, {1, 0}, {0, -1} }; // 初始化所有格子先设为墙再把奇数坐标的格子设为路 MazeGrid createMazeBase(const MazeSize size) { MazeGrid maze(size.rows, std::vectorint(size.cols, 0)); for (int y 1; y size.rows; y 2) { for (int x 1; x size.cols; x 2) { maze[y][x] 1; // 路 } } return maze; }这段代码做了三件基础的事定义二维数组的类型别名、固定方向顺序、初始化全墙再填充路。方向数组是后面所有算法共用的顺序不固定没关系但“上右下左”这个顺序便于和人脑的直觉对齐。注意cols和rows必须取奇数否则路格子数量不对称生成结果会偏掉。2.2 DFS 递归回溯生成C 代码与随机数洗牌在“迷宫生成”这个领域深度优先搜索递归回溯DFS Recursive Backtracker是目前最直观、也是迷宫游戏里出效果最快的方案。它的核心思路从起点出发随机选一个相邻且没访问过的路格子打通两者之间的墙走过去继续递归如果四周都访问过就回溯到上一个格子。因为 C 的递归栈天然承担了回溯逻辑实现成本极低。下面这段代码直接生成一个完整迷宫返回值里1是路0是墙#include algorithm #include random #include stack // 在二维数组上执行 DFS 递归回溯 // visited 记录路格子是否已访问maze 引用传出结果 void generateMazeDFS(MazeGrid maze, int startX, int startY) { const int rows static_castint(maze.size()); const int cols static_castint(maze[0].size()); // 访问标记只关心奇数坐标的路格子 std::vectorstd::vectorbool visited(rows, std::vectorbool(cols, false)); // 用迭代栈模拟递归避免深迷宫导致系统栈溢出 std::stackstd::pairint, int stk; stk.push({startX, startY}); visited[startY][startX] true; // 随机数引擎用 me19937 而不是 rand()保证每次运行结果分布更均匀 std::mt19937 rng{std::random_device{}()}; while (!stk.empty()) { int x stk.top().first; int y stk.top().second; // 收集当前格子可前进的邻居间隔 2 个坐标中间隔一堵墙 std::vectorstd::pairint, int neighbors; for (int d 0; d 4; d) { int nx x DIRS[d][1] * 2; int ny y DIRS[d][0] * 2; if (nx 0 nx cols - 1 ny 0 ny rows - 1) { if (!visited[ny][nx]) { neighbors.push_back({nx, ny}); } } } if (!neighbors.empty()) { // 随机打乱邻居顺序模拟“随机选择下一步” std::shuffle(neighbors.begin(), neighbors.end(), rng); int nx neighbors.front().first; int ny neighbors.front().second; // 打通当前格与邻居格之间的墙 int wallX x (nx - x) / 2; int wallY y (ny - y) / 2; maze[wallY][wallX] 1; stk.push({nx, ny}); visited[ny][nx] true; } else { // 没有可走的邻居回溯 stk.pop(); } } }逻辑说明visited只标记奇数坐标的格子所以循环里跳跃步长是 2墙的位置用两个格子的中点算出。std::mt19937 std::shuffle是 C11 起的标准做法比rand() % n更均匀也更容易通过种子复现同一个迷宫。如果你只是做课程展示种子固定会更方便——把std::random_device{}()换成固定数字即可比如std::mt19937 rng{42}。2.3 随机 Prim、Kruskal 和 DFS 的取舍什么时候该换算法除了 DFS另两个常见算法是随机 Kruskal用并查集合并集合和随机 Prim维护候选墙集合。随机 Prim 生成出来的迷宫“主干道”更多分支更均匀整体显得更开阔DFS 的迷宫则支路深、死胡同多更有“探洞”的感觉。对迷宫游戏来说我一般建议用 DFS原因是它的死路多玩家在探索时会频繁面临“回头路”的抉择游戏性反而更好。如果你做的是一个迷宫寻路算法的演示程序想比较不同算法的输出形态那随机 Prim 和 Kruskal 是更好的参照组因为它们生成的迷宫通常没有明显的主路径偏见。// 如果用随机 Prim核心结构是一个候选墙队列 // 每轮从候选墙里随机取一个如果墙两侧的格子不同时被访问就打通它 // 这个代码片段只展示核心循环便于和上面的 DFS 对比 void generateMazePrim(MazeGrid maze, int startX, int startY) { const int rows static_castint(maze.size()); const int cols static_castint(maze[0].size()); std::vectorstd::vectorbool visited(rows, std::vectorbool(cols, false)); std::vectorstd::tupleint, int, int, int walls; // x,y,nx,ny visited[startY][startX] true; // 把起点四周的墙加入候选 for (int d 0; d 4; d) { int wx startX DIRS[d][1]; int wy startY DIRS[d][0]; int nx startX DIRS[d][1] * 2; int ny startY DIRS[d][0] * 2; if (nx 0 nx cols - 1 ny 0 ny rows - 1) { walls.push_back({wx, wy, nx, ny}); } } std::mt19937 rng{std::random_device{}()}; while (!walls.empty()) { // 随机取一个候选墙 int idx std::uniform_int_distributionint(0, walls.size() - 1)(rng); auto [wx, wy, nx, ny] walls[idx]; walls.erase(walls.begin() idx); if (!visited[ny][nx]) { maze[wy][wx] 1; visited[ny][nx] true; // 新格子的墙也加入候选 for (int d 0; d 4; d) { int newNX nx DIRS[d][1] * 2; int newNY ny DIRS[d][0] * 2; if (newNX 0 newNX cols - 1 newNY 0 newNY rows - 1) { if (!visited[newNY][newNX]) { walls.push_back({nx DIRS[d][1], ny DIRS[d][0], newNX, newNY}); } } } } } }逻辑说明Prim 用walls集合代替了 DFS 的递归栈所以它生成的迷宫形态不会受“上一次探索深度”影响而是每个路格子大致均匀扩张。代价是erase在std::vector上是 O(n)迷宫大时效率稍差工程里可以换成std::unordered_set或std::vector加“交换删除”。多数 Qt 迷宫游戏尺寸在 51×51 以下这个开销可以忽略。3. 把迷宫画到 Qt 界面上QGraphicsView 与 QPainter 自绘的差异3.1 先搭工程框架Qt Widgets 还是 QML到了 Qt 这一步先说框架选择。热搜词里反复出现 qt designer 界面设计、qt creator、qt 下载说明很多人正卡在“用什么版本、怎么搭界面”上。我的建议很直接这个项目用 Qt Widgets Application 模板不要碰 QML。理由有三个一是 C 代码直接写在 QWidget 子类里更容易看懂数据流二是迷宫游戏没有复杂的动态界面QPainter 自绘完全够三是 Qt Designer 拖出来的.ui文件虽然界面排布方便但迷宫生成和键盘事件都得手动写代码不如纯代码来得直接。工程里至少需要三个类MazeWidget负责绘制和玩家交互、MazeGenerator封装第 2 章的算法、MainWindow放一个MazeWidget实例并处理窗口缩放。如果你用 qmake.pro文件相应如下# maze_qt.pro QT core gui widgets TARGET maze_qt TEMPLATE app SOURCES main.cpp MazeWidget.cpp MazeGenerator.cpp HEADERS MazeWidget.h MazeGenerator.h这里QT widgets是 Qt5 之后必须的core和gui默认带。TARGET maze_qt对应生成的可执行文件名也是标题里“Maze_qt”的由来。3.2 用 QPainter 重写 paintEvent绘制迷宫的最小代码绘制迷宫有两种主流做法一种是把每一面墙都放到QGraphicsScene里独立成QGraphicsRectItem另一种是直接在QWidget::paintEvent里用QPainter画矩形。第一种在人机交互、碰撞检测上确实方便但迷宫一旦到 31×31Item 数量上千拖动窗口或频繁重绘会出现明显的卡顿感第二种只画路和墙两种矩形每帧最多画几百个矩形性能稳定代码量也少。我一般选QPainter自绘。下面这个paintEvent是完整可用的// MazeWidget::paintEvent 的核心绘制代码 void MazeWidget::paintEvent(QPaintEvent* event) { QPainter painter(this); painter.fillRect(rect(), QColor(245, 242, 238)); // 背景米白色 QColor wallColor(52, 73, 94); // 墙体深蓝灰 QColor roadColor(255, 255, 255); // 路纯白 // 每个格子的像素尺寸由传入参数决定 int cellSize m_cellSize; // 通常 16~24 像素 int offsetX (width() - m_mazeWidth * cellSize) / 2; // 水平居中 int offsetY (height() - m_mazeHeight * cellSize) / 2; // 垂直居中 // 先画路再画墙 painter.fillRect(offsetX, offsetY, m_mazeWidth * cellSize, m_mazeHeight * cellSize, roadColor); painter.setBrush(wallColor); for (int row 0; row m_mazeHeight; row) { for (int col 0; col m_mazeWidth; col) { if (m_maze[row][col] 0) { painter.drawRect(offsetX col * cellSize, offsetY row * cellSize, cellSize, cellSize); } } } // 玩家一个圆点画在格子中央 painter.setBrush(QColor(231, 76, 60)); int playerX offsetX m_playerCol * cellSize cellSize / 2; int playerY offsetY m_playerRow * cellSize cellSize / 2; painter.drawEllipse(QPoint(playerX, playerY), cellSize / 3, cellSize / 3); }逻辑说明先整体填充路色再逐个格子画墙避免墙和路之间有一像素缝隙。offsetX/offsetY用于迷宫在窗口内居中否则当窗口大小不整除迷宫总像素时迷宫会贴在左上角。玩家坐标m_playerCol/m_playerRow也是格子坐标绘制时换算成像素中心点。如果想让墙有立体感可以用painter.fillRect配合不同灰度做两次偏移但课程设计里没必要。3.3 绘制参数怎么调格子大小、窗口尺寸与清晰度这里给出实际可用的参数范围都是我调过的经验值参数建议值说明m_cellSize1624 px低于 16 迷宫太小高于 24 大尺寸迷宫会超出屏幕迷宫尺寸21×21 到 51×5131×31 是最佳演示尺寸生成快、玩家走起来不累窗口尺寸与迷宫总像素一致或略大用resize(mazeWidth * cellSize 40, mazeHeight * cellSize 80)墙色深色系如 RGB(52,73,94)与路色对比度要高避免视觉疲劳绘制间隔不需要定时器只在update()里重绘事件驱动即可一个值得注意的细节是 Qt 的坐标原点在窗口左上角但迷宫数组的[0][0]对应的是画布的(offsetX, offsetY)。后续键盘移动和碰撞检测都要沿用这套坐标换算数组坐标 → 像素坐标 数组坐标 × 格子像素 偏移量。别偷懒直接把数组下标当像素用否则迷宫越大偏移越明显。4. 玩家移动与碰撞判定键盘事件、格子级碰撞与平滑移动4.1 监听键盘事件keyPressEvent 的方向识别与状态保存玩家控制是迷宫游戏交互的核心。在 QWidget 里覆写keyPressEvent根据event-key()判断方向。这里有个常见坑用户快速连按两次方向键第一次移动还没完成第二次移动就已经触发导致角色在格子之间“飘移”。所以不要直接在键盘事件里改玩家坐标而是保存一个“待处理方向”的状态。// MazeWidget.h 中与移动相关的成员变量 int m_playerRow; // 当前所在行格子坐标 int m_playerCol; // 当前所在列格子坐标 int m_targetRow; // 目标行平滑移动的终点 int m_targetCol; // 目标列 bool m_moving; // 是否正在移动 int m_pendingDir; // 最近一次有效方向-1 表示无 // MazeWidget.cpp 中按键事件的处理 void MazeWidget::keyPressEvent(QKeyEvent* event) { int dir -1; switch (event-key()) { case Qt::Key_Up: dir 0; break; case Qt::Key_Right: dir 1; break; case Qt::Key_Down: dir 2; break; case Qt::Key_Left: dir 3; break; default: QWidget::keyPressEvent(event); return; } if (!m_moving) { tryMove(dir); } else { m_pendingDir dir; // 当前移动结束后再尝试 } } // 尝试向指定方向移动一格 void MazeWidget::tryMove(int dir) { int dr (dir 0) ? -1 : (dir 2) ? 1 : 0; int dc (dir 1) ? 1 : (dir 3) ? -1 : 0; int nr m_playerRow dr; int nc m_playerCol dc; // 碰撞判定只检查目标格子是否是路 if (isWalkable(nr, nc)) { m_targetRow nr; m_targetCol nc; m_moving true; startMovingTimer(); // 启动 10ms 步进定时器 } // 如果撞墙不加任何惩罚只是不移动 }逻辑说明tryMove只负责发起移动真正的坐标更新交给定时器逐帧做。m_moving为真时新按键只记录在m_pendingDir里等当前格子走完再调用tryMove(m_pendingDir)。这个做法模拟了经典方块游戏的“按键排队”玩家快速按两下方向键角色能连贯地走两步不会出现穿墙。4.2 碰撞检测别用物理引擎直接用格子状态判断很多刚接触游戏开发的人会在 Qt 里引入QRect的交叠判断甚至试图用刚体模拟。对一个网格游戏来说这是本末倒置。碰撞检测只需两步把目标的像素坐标换算成数组行列号再查maze[row][col]是否为 1。刚才代码里的isWalkable(nr, nc)展开如下bool MazeWidget::isWalkable(int row, int col) { if (row 0 || row m_mazeHeight || col 0 || col m_mazeWidth) { return false; // 出界视为不可走 } return m_maze[row][col] 1; // 1 是路 }这里的边界判断很容易漏。如果迷宫周围有墙玩家视觉上出不去但数组索引会越界直接访问m_maze[row][col]会触发段错误。先判断范围再访问是排查“走到一半程序崩了”类问题的第一检查点。4.3 平滑移动的实现QTimer 步进与网格对齐纯格点跳变会显得很生硬加上一个小小的动画会让程序质感提升一个档次。常见做法是在移动开始时启动一个 10ms 的QTimer每次到时把玩家像素坐标向目标格子推进若干像素走到目标格子后停止定时器。// 平滑移动每 tick 向目标位置推进 m_moveSpeed 像素 void MazeWidget::onMoveTick() { // 当前像素坐标参考点在格子左上角 int curX m_playerCol * m_cellSize; int curY m_playerRow * m_cellSize; int targetX m_targetCol * m_cellSize; int targetY m_targetRow * m_cellSize; int dx targetX - curX; int dy targetY - curY; int step m_moveSpeed; // 通常为 4~6 像素10ms 一帧每秒移动约 10 格 // 当前帧只移动 step 距离不直接赋目标值 if (abs(dx) 0) m_playerCol (dx 0 ? 1 : -1) * step / m_cellSize; if (abs(dy) 0) m_playerRow (dy 0 ? 1 : -1) * step / m_cellSize; // 到达目标格后收尾 if (m_playerCol m_targetCol m_playerRow m_targetRow) { m_timer-stop(); m_moving false; // 处理排队的方向键 if (m_pendingDir ! -1) { int nextDir m_pendingDir; m_pendingDir -1; tryMove(nextDir); } } update(); }注意这段代码我用的是“格子坐标增量”而不是像素坐标累加因为格子坐标做碰撞检测更干净。step / m_cellSize每帧推进的格数在像素尺寸较大时会丢精度所以实际工程建议把玩家位置用float存像素值绘制时再除以cellSize得到格子坐标。这里为了可读性用了简化写法。如果你发现按键后角色反应慢半拍通常不是 Qt 的事件慢而是QTimer间隔设太大或者tryMove里做了多余的碰撞全表扫描。10ms 的间隔和 4 像素的步进实测手感最接近键盘响应直觉。步进取 2 像素会更细腻但角色移动速度过慢大迷宫里走起来着急。5. Qt 迷宫游戏的避坑指南版本冲突、绘图卡顿与坐标错位5.1 编译报错fatal: cannot mix incompatible Qt library (version ex50601) with this library现象编译能通过链接也没问题一运行就弹出这个致命错误后面通常还带一段version XXXX的文字。这在 Qt 新手阶段出现率极高尤其是下载了旧版库、又新建了新版项目时。原因你的程序链接到的 Qt 库和运行时加载的 Qt 动态库不是同一个版本常见于电脑上有多个 Qt 安装目录或者环境变量PATH里旧版本的bin排在前面。解决检查.pro里QT 依赖的模块版本确认 Qt Creator 里选择的 Kit 和你实际使用的 Qt 版本一致。Windows 下把正确的 Qtbin目录挪到PATH前面或者干脆把目标动态库复制到可执行文件目录再运行。5.2 运行时崩溃qt.qpa.plugin: could not find the Qt platform plugin linuxfb现象程序在无桌面环境或远程终端里运行报找不到linuxfb平台插件。很多人的第一反应是重新安装 Qt但重装并不能解决。原因Qt 需要通过 platform plugin 与窗口系统对接。在 Linux 桌面环境通常是xcb在嵌入式设备才是linuxfb。报错往往是因为缺libQt5XcbQpa.so或platforms目录没有随程序一起打包。解决在程序入口main.cpp里显式指定插件路径#include QApplication #include QDir int main(int argc, char *argv[]) { // 如果从打包目录运行优先加载自带的平台插件 QApplication::addLibraryPath( QDir(QCoreApplication::applicationDirPath()) .filePath(platforms)); QApplication app(argc, argv); // ... 创建主窗口 return app.exec(); }如果你是交叉编译到树莓派等开发板要用-platform linuxfb参数启动或者设置环境变量QT_QPA_PLATFORMlinuxfb。5.3 绘制错位迷宫右边和下边的墙跑出窗口外现象迷宫在窗口里看起来“缺了一行”尤其窗口被拉伸后更加明显缩小窗口时迷宫被裁剪放大时右边和下边留大片空白。原因绘制时直接在paintEvent里按m_cellSize * 迷宫行列数计算总尺寸但没有考虑窗口大小变化后的重排也没有把迷宫整体居中。数组坐标到窗口坐标换算里的偏移量一旦写死窗口一缩放就错位。解决每次绘制前动态计算offsetX/offsetY永远不要缓存上一次的偏移值// 在 paintEvent 里先算偏移再画迷宫 int mazePixelW m_mazeWidth * m_cellSize; int mazePixelH m_mazeHeight * m_cellSize; int offsetX qMax(0, (width() - mazePixelW) / 2); int offsetY qMax(0, (height() - mazePixelH) / 2);同时把m_mazeWidth和m_mazeHeight设为只读成员生成迷宫后不再改动。如果你需要“缩放迷宫以适应窗口”不要改这两个值改m_cellSize即可。5.4 快速连按方向键导致角色穿墙或者卡在墙里现象连续快速按“上→右”或“左→上”时角色偶尔会瞬移穿过墙或者停在墙格子里无法继续移动。原因keyPressEvent里直接改m_playerRow/m_playerCol第一次移动还没到达目标格第二次按键又改了一次坐标于是角色跳过了isWalkable的检查。另一个常见原因是 tryMove 里检查的是“目标格子”但在onMoveTick的步进过程中没有再次校验路径上的格子导致像素坐标越过墙体。解决用前面第 4 章的方案——移动过程中不响应新的方向只记录到m_pendingDir在每次 tick 里都检查当前格子是不是路不是就立即回退到上一个合法格子并停表。可以加一层保险// 每帧开始前校验玩家是否仍在合法格子 if (!isWalkable(m_playerRow, m_playerCol)) { // 回退到上一次记录的合法坐标 m_playerRow m_lastValidRow; m_playerCol m_lastValidCol; m_timer-stop(); m_moving false; }这里m_lastValidRow/m_lastValidCol是关键每次成功走到目标格后更新这两个变量。这样即使出现极端情况角色也会回退到最近一次合法位置而不是永久卡死。5.5 绘制性能跳到 20 FPS把“每帧创建 QPainter 对象”的坑记牢现象迷宫尺寸调大后拖动窗口或连续按键移动能明显看到刷屏闪烁和延迟。热度词里“游戏延迟高”“unity游戏优化”和这个现象是同源问题。原因paintEvent里每帧都在堆上创建大量临时对象比如QColor、QRect甚至有人直接在循环里new QRect。QWidget 本身没有强制垂直同步重绘频繁时性能瓶颈自然暴露。解决把绘制常用的颜色和画刷做成成员变量初始化一次后复用绘制循环里尽量用painter.fillRect(QRect, QColor)而不是drawRect加setBrush前者开销更小不要主动调用update()太频繁QTimer 间隔保持 10ms 即可不要为追求“高帧率”把它改成 1ms。6. 给迷宫加上自动寻路演示BFS 与 DFS 求解的可视化对比迷宫能“走”了接下来最有价值的进阶功能是自动求解动画。把寻路结果用动画逐帧画出来既能展示栈和队列这两种数据结构的差异也让课程设计多一个亮点。常见的实现是玩家走到终点前按一个键启动自动寻路。求最短路径用 BFS因为 BFS 按层扩展第一次到达终点的路径就是最短路径求一条可行路径用 DFS它用栈沿一条路走到底找到目标就返回但路径不一定最短。下面这个代码段是 BFS 求解并把路径回溯到数组#include queue #include vector // BFS 寻路返回从 (startRow, startCol) 到 (endRow, endCol) 的路径点集合 std::vectorstd::pairint, int solveMazeBFS(const MazeGrid maze, int startRow, int startCol, int endRow, int endCol) { int rows maze.size(); int cols maze[0].size(); // prev 用于回溯路径prev[y][x] 存的是上一个格子的坐标 std::vectorstd::vectorstd::pairint, int prev( rows, std::vectorstd::pairint, int(cols, {-1, -1})); std::queuestd::pairint, int q; q.push({startRow, startCol}); prev[startRow][startCol] {startRow, startCol}; while (!q.empty()) { auto [r, c] q.front(); q.pop(); // 到达终点回溯路径 if (r endRow c endCol) { std::vectorstd::pairint, int path; for (auto cur std::make_pair(endRow, endCol); cur ! std::make_pair(startRow, startCol); cur prev[cur.first][cur.second]) { path.push_back(cur); } path.push_back({startRow, startCol}); std::reverse(path.begin(), path.end()); return path; } // 四方向扩展只走路 for (int d 0; d 4; d) { int nr r DIRS[d][0]; int nc c DIRS[d][1]; if (nr 0 nr rows nc 0 nc cols maze[nr][nc] 1 prev[nr][nc] std::make_pair(-1, -1)) { prev[nr][nc] {r, c}; q.push({nr, nc}); } } } return {}; // 无路径 }这个算法最核心的部分是prev数组它记录了 BFS 树里每个节点的父节点。终点找到后从终点一路回溯到起点把走过的节点倒序输出就是完整路径。注意队列用的是std::queue这正是热词里“ds堆栈-迷宫求解”中的队列应用如果把队列换成栈就变成了 DFS路径往往更长。你在演示时可以把这段 BFS 封装成一个槽函数用上一章的QTimer把路径逐帧画出来。我自己的习惯是在自动求解前先把当前玩家位置记录为起点求解结束后显示路径长度并高亮路径上的格子。这个功能做完整个项目就同时覆盖了“生成算法 UI 绘制 事件交互 数据结构应用”拿去答辩或面试时随便追问一个环节都能展开讲。另外别忘了Qt 的按钮事件、键盘事件和QTimer都运行在同一个主线程里不要在paintEvent里跑 BFS否则界面会直接卡死。常见做法是点击按钮时求出路径缓存下来再由定时器逐步update()。迷宫生成同理生成算法放到构造函数或按钮槽里一次性执行而不是在绘制函数里生成。希望这套从生成到寻路的思路能帮到你少踩几个我当年踩过的坑。本文还有配套的精品资源点击获取
返回列表