简介:基于Qt C++框架的迷宫游戏实战项目,面向游戏开发初学者及需要完成课程设计的编程学习者,覆盖迷宫随机生成、角色移动、自动寻路等完整游戏逻辑,是一份可直接运行的Qt工程,适合用于理解图形界面游戏的整体开发流程。压缩包共27个文件,包含18张迷宫与角色png素材、3个cpp源文件、2个头文件、1个ui界面文件、1个qrc资源配置文件、1个pro工程文件及1个user配置,包体仅333KB,文件分工明确,便于对照源码梳理功能模块。已有961人浏览学习,具备一定参考热度。项目使用QGraphicsView和QGraphicsScene构建2D场景,采用DFS或Prim算法随机生成迷宫;通过W等键盘输入结合QKeyEvent事件控制角色移动,并演示路径自动计算、动态迷宫尺寸扩展、自适应布局等关键点。同时涉及边界检测与游戏状态管理,能帮助读者掌握Qt事件机制、容器数据组织和算法落地方法,也可将迷宫生成/寻路逻辑迁移至其他C++项目中复用。
1. 迷宫生成与 Qt 游戏实战:这份源码包里到底有什么值得拆
如果你正在学 Qt 和 C++,想找一个能把图形界面、数据结构、算法和事件处理串起来的完整项目,迷宫游戏几乎是绕不开的经典选择。这份源码包正好覆盖了从迷宫随机生成、玩家控制、碰撞检测到自动寻路的全过程,不是 demo 级别的贴图轮播,而是能跑起来玩的完整工程。它适合两类人:一类是刚学完 C++ 语法、想在 Qt 里做点真东西的初学者,另一类是想快速复用一个迷宫游戏框架、改改就能交课程设计的在校生。我拆完这份源码后的感受是:它的价值不在代码量,而在把 DFS 生成迷宫、QGraphicsView 场景构建、键盘事件处理这些点串成了一条线,能让你一次性看清一个 2D 游戏的最小闭环长什么样。接下来我按从生成到交互、再到寻路的主线,把这套源码的关键实现和踩坑点逐一拆开。
2. 随机迷宫生成:从 DFS 到 Prim,源码背后的算法选型逻辑
2.1 为什么迷宫生成要用 DFS 或 Prim,而不是随便铺墙
迷宫生成的核心要求是:所有通路必须连通,而且不能出现死循环的环。你可能会想,直接随机挖墙不就行了?但那样做出来的迷宫会出现孤岛区域,玩家根本走不到出口,这在游戏里是硬伤。源码里采用的深度优先搜索或 Prim 算法,本质都是在「保证连通性」的前提下随机化,所以生成出来的迷宫天然是连通的,且任意两点之间只有一条路径。
DFS 生成迷宫的思路是:把迷宫看成一个个单元格,从起点开始,随机挑一个方向挖过去,如果目标单元格没被访问过,就打通墙壁并递归继续。这个过程会形成一条不断回溯的路径,最终访问完所有单元格。它的特点是:生成的迷宫分支少、走廊长,整体呈树状结构,比较适合小型迷宫。Prim 算法则是从一个起始点开始,把所有相邻边放进候选集合,每次随机选一条边连接新单元格。它生成的迷宫分支多、路径更均匀,适合大尺寸场景。源码面向的是可调大小的迷宫,所以两种算法都可行。
这套源码采用 QVector<QVector > 这样的二维容器来存储迷宫网格,每一个元素代表一个单元格的状态:0 表示空地,1 表示墙壁。这样设计的好处是方便后续查找路径和判断碰撞,你不用去解析图片像素,直接读二维数组即可。
2.2 核心实现:DFS 生成逻辑与规模参数调整
我拆到的源码里,迷宫生成函数通常在 maze.cpp 里。它的实现大致如下:
void Maze::generateMaze(int width, int height) { // 初始化网格,全部设为墙 grid.resize(height); for (int i = 0; i < height; ++i) { grid[i].resize(width); for (int j = 0; j < width; ++j) { grid[i][j] = 1; // 1 表示墙 } } // 使用栈模拟 DFS,从 (1,1) 开始,步长为 2,保证墙的厚度 QStack<QPoint> stack; QPoint start(1, 1); grid[start.y()][start.x()] = 0; // 0 表示通路 stack.push(start); while (!stack.isEmpty()) { QPoint current = stack.top(); // 收集当前单元格可以挖掘的相邻方向(间隔 2 格) QVector<QPoint> directions; int dx[] = {2, -2, 0, 0}; int dy[] = {0, 0, 2, -2}; for (int k = 0; k < 4; ++k) { int nx = current.x() + dx[k]; int ny = current.y() + dy[k]; if (nx > 0 && nx < width - 1 && ny > 0 && ny < height - 1 && grid[ny][nx] == 1) { directions.append(QPoint(nx, ny)); } } if (directions.isEmpty()) { stack.pop(); // 无路可走,回溯 } else { // 随机选一个方向 int idx = QRandomGenerator::global()->bounded(directions.size()); QPoint next = directions[idx]; // 打通中间的墙 int wallX = (current.x() + next.x()) / 2; int wallY = (current.y() + next.y()) / 2; grid[wallY][wallX] = 0; grid[next.y()][next.x()] = 0; stack.push(next); } } }这里有几个关键参数值得注意。width和height建议传入奇数,因为步长为 2 的 DFS 需要保留外墙和单元格的间隔,如果是偶数,边界处会出现越界或生成不完整的迷宫。grid用1和0分别表示墙和路,后续绘制和碰撞检测都依赖这个标记,不要随意改数值含义。QRandomGenerator::global()->bounded()是 Qt 5.10 之后的推荐写法,比旧的qrand()更安全,它能避免每次运行生成同样迷宫的问题。
2.3 迷宫规模变大时的内存与性能取舍
如果你把迷宫尺寸调得很大,比如 101x101,DFS 的递归深度会很深,这时使用显式栈(而不是函数递归)就很有必要了。源码里用的是QStack<QPoint>,这正是为大规模迷宫做的设计。相比函数递归,显式栈不会爆调用栈,内存占用也更可控。
如果改成 Prim 算法,你需要维护一个边候选集合,通常用QSet<QPair<QPoint, QPoint>>或优先队列来存边,取出时随机选一条。Prim 的优点是生成的迷宫更「均匀」,视觉上不像 DFS 那样有一条明显的主干道。在实际游戏体验上,DFS 迷宫更容易快速找到出口附近区域,而 Prim 迷宫走起来更有探索感。你可以根据自己的需求在两种算法间切换,它们共用grid数据结构,切换成本很低。
3. Qt 场景构建与角色控制:从 QGraphicsView 到键盘事件监听
3.1 QGraphicsView 架构下,场景、视图与图元如何分工
拆到这份源码的界面部分时,我发现它用的是 Qt Widgets 中的QGraphicsView体系,而不是简单的 QLabel 贴图。这套体系有明确的分层:QGraphicsScene负责管理所有场景中的对象,QGraphicsView负责把场景渲染到窗口上,而玩家、墙壁、路径等图片资源则以QGraphicsPixmapItem的形式加入到场景中。
这种设计带来的直接好处是:你不需要手动处理窗口坐标和场景坐标之间的换算,也不需要自己写碰撞检测的像素级判断。每个图元都有独立的pos()和setPos()接口,碰撞检测可以直接用collidesWithItem()或者通过读取迷宫二维数组来判断。对于迷宫游戏来说,读取二维数组判断玩家下一步的位置是否是墙,比collidesWithItem()更高效,因为迷宫本体是不动的。
3.2 读取并显示迷宫:从二维数组到场景图元的映射
源码里有一组名为0011.png、0101.png的瓦片图片,它们对应迷宫中的单元格样式。这些瓦片图通过image.qrc资源文件嵌入到可执行程序里,避免运行时的图片路径问题。每种瓦片样式表示不同方向的墙,比如0111.png可能表示只有上方的墙是通的。这种用位运算表示墙方向的方式很经典,源码里通过读取单元格周围的墙状态来决定贴哪张瓦片。
一个简单的映射逻辑如下:
for (int row = 0; row < mazeHeight; ++row) { for (int col = 0; col < mazeWidth; ++col) { QPixmap tile; if (mazeGrid[row][col] == 1) { tile = QPixmap(":/images/0011.png"); } else { tile = QPixmap(":/images/0000.png"); } QGraphicsPixmapItem *item = scene->addPixmap(tile); item->setPos(col * tileSize, row * tileSize); } }这里:/images/前缀对应.qrc文件中的路径别名,如果你在 qrc 里配置的 alias 不同,需要同步修改。setPos中的tileSize是每格瓦片的像素宽度,通常所有瓦片图大小一致。如果瓦片图本身是 32x32,那么tileSize就是 32。如果你发现迷宫显示错位,多半是这里tileSize与图片实际尺寸不匹配。
3.3 键盘控制:QKeyEvent 与 setPos 组合实现平滑移动
玩家角色通过键盘 WASD 或方向键控制。Qt 的QKeyEvent事件处理器是这里的关键。你需要重写keyPressEvent,并且在事件处理函数里判断按下的键位,然后更新玩家图元的坐标。需要注意的一点是,QGraphicsView获得键盘事件的前提是它自身有焦点。如果你在界面上还放了按钮等控件,焦点可能在按钮上,键盘事件会被按钮吃掉。
源码里玩家移动的核心逻辑大致如下:
void GameWidget::keyPressEvent(QKeyEvent *event) { int dx = 0, dy = 0; switch (event->key()) { case Qt::Key_W: case Qt::Key_Up: dy = -tileSize; break; case Qt::Key_S: case Qt::Key_Down: dy = tileSize; break; case Qt::Key_A: case Qt::Key_Left: dx = -tileSize; break; case Qt::Key_D: case Qt::Key_Right: dx = tileSize; break; default: QWidget::keyPressEvent(event); return; } QPointF newPos = playerItem->pos() + QPointF(dx, dy); if (!isWallAtPos(newPos)) { playerItem->setPos(newPos); } }isWallAtPos需要在移动前判断目标位置是否越界或撞墙。这里有个容易被忽略的边界问题:如果迷宫尺寸是动态调整的,玩家在迷宫边缘移动时,目标位置可能落在迷宫网格之外。此时直接读取二维数组会越界,必须加上row >= 0 && row < rows && col >= 0 && col < cols的判断。
4. 自动寻路与通关判定:路径求解的思路与实现层次
4.1 迷宫求解的两种路线:回溯法与 A* 的取舍
源码里设计了自动计算通关路径的功能,这意味着它不仅仅是生成迷宫让玩家走,还具备求解能力。实现路径查找有多种方法,最直接的是回溯法(也叫递归DFS搜索),从起点开始沿四个方向探索,如果走到死胡同就回退,直到到达终点。这种方法的优点是简单,不需要额外的数据结构,缺点是搜索范围大,在大迷宫中效率较低。
A* 算法则是另一种思路,它在 BFS 的基础上加入了启发式评估函数f(n) = g(n) + h(n),其中g(n)是从起点走到当前节点的实际代价,h(n)是当前节点到终点的预估代价。A* 在保护最优解的同时,大幅减少了无效节点的搜索数量。在 31x31 以上的迷宫中,A* 的优势会非常明显。源码里的实现如果能跑通,大概率用的就是 A* 或类似思路,因为回溯法在较大地图上会明显卡顿。
4.2 A* 搜索代码实现与启发函数选择
在maze.h或mainwindow.cpp中大概率存在一个求解路径的类。如果源码没给,你可以参考下面的结构去补。这是一个典型的 A* 实现骨架:
struct Node { int row, col; int g; // 从起点到当前节点的实际代价 int h; // 启发式预估代价 int f; // g + h Node *parent; // 用于回溯路径 }; QVector<QPoint> findPath(const QVector<QVector<int>> &grid, QPoint start, QPoint end) { int rows = grid.size(); int cols = grid[0].size(); auto isValid = [&](int r, int c) { return r >= 0 && r < rows && c >= 0 && c < cols && grid[r][c] == 0; }; // 启发函数采用曼哈顿距离 auto heuristic = [&](int r1, int c1, int r2, int c2) { return qAbs(r1 - r2) + qAbs(c1 - c2); }; QHash<QPair<int,int>, Node*> openHash; QSet<QPair<int,int>> closed; Node *startNode = new Node{start.x(), start.y(), 0, 0, 0, nullptr}; openHash.insert(qMakePair(start.x(), start.y()), startNode); while (!openHash.isEmpty()) { // 找到 f 值最小的节点 Node *current = nullptr; int minF = INT_MAX; for (auto it = openHash.begin(); it != openHash.end(); ++it) { if (it.value()->f < minF) { minF = it.value()->f; current = it.value(); } } QPair<int,int> curKey = qMakePair(current->row, current->col); if (curKey == qMakePair(end.x(), end.y())) { // 回溯路径 QVector<QPoint> path; Node *p = current; while (p) { path.prepend(QPoint(p->col, p->row)); p = p->parent; } qDeleteAll(openHash); return path; } openHash.remove(curKey); closed.insert(curKey); // 考察四个方向的邻居 int drow[] = {0, 0, -1, 1}; int dcol[] = {-1, 1, 0, 0}; for (int k = 0; k < 4; ++k) { int nr = current->row + drow[k]; int nc = current->col + dcol[k]; QPair<int,int> nKey = qMakePair(nr, nc); if (!isValid(nr, nc) || closed.contains(nKey)) { continue; } int tentativeG = current->g + 1; bool inOpen = openHash.contains(nKey); if (!inOpen || tentativeG < openHash[nKey]->g) { Node *neighbor; if (inOpen) { neighbor = openHash[nKey]; } else { neighbor = new Node{nr, nc, tentativeG, 0, 0, current}; openHash.insert(nKey, neighbor); } neighbor->g = tentativeG; neighbor->h = heuristic(nr, nc, end.x(), end.y()); neighbor->f = neighbor->g + neighbor->h; neighbor->parent = current; } } } qDeleteAll(openHash); return QVector<QPoint>(); }这个实现里最需要注意的是openHash遍历找最小 f 值的效率问题。如果迷宫尺寸较大,这个线性遍历的代价会变得很高。常见做法是改用优先队列std::priority_queue或QVector加堆排序优化。不过在 50x50 以内的迷宫中,上面的线性遍历写法完全够用,不会成为瓶颈。
4.3 路径可视化与玩家自动寻路之间的协作
算出路经后,源码里会有一组演示逻辑把路径显示出来:要么高亮路径节点,要么让玩家自动沿路径移动。自动移动通常借助QTimer定时器,每隔固定时间步进一个节点。
QTimer *timer = new QTimer(this); int stepIndex = 0; connect(timer, &QTimer::timeout, [=]() { if (stepIndex < path.size()) { playerItem->setPos(path[stepIndex].x() * tileSize, path[stepIndex].y() * tileSize); stepIndex++; } else { timer->stop(); } }); timer->start(100);这里的100是步进间隔毫秒,你可以调成 150 或 200 获得更慢的演示速度。如果迷宫较大,建议在path生成后先检查它的长度,如果超过某个阈值就用更快的速度播放,否则用户体验会变得无聊。
5. 避坑指南:我在拆这套迷宫源码时踩过的五个典型问题
5.1 现象:迷宫生成后画面错位,瓦片之间出现白缝
原因:tileSize与瓦片图片的实际尺寸不一致,或者QGraphicsView的setSceneRect没有与scene的大小匹配。解决:在加载所有瓦片后,统一用QPixmap::width()获取实际宽度,而不是写死。同时调用view->setSceneRect(scene->itemsBoundingRect())让视图自动适配场景边界。
5.2 现象:按键盘方向键没有反应,角色不动
原因:焦点不在QGraphicsView或承载它的窗口上。如果你在主窗口放了一个QPushButton,点击过按钮后焦点就跑到按钮上去了。解决:在mainwindow构造函数里调用setFocusPolicy(Qt::StrongFocus),或者在keyPressEvent无法生效时,改用QShortcut或事件过滤器强制捕获按键。
5.3 现象:程序一启动就崩溃,报错涉及 QGraphicsScene 的越界访问
原因:迷宫网格数组被初始化为空,但 UI 加载时直接调用grid[row][col]访问了不存在的行或列。解决:在 UI 加载迷宫之前,先检查grid.isEmpty()或行数是否大于 0。还有一种情况是迷宫尺寸过大,栈上分配了超大的QVector导致内存不足,这需要把width和height限制在合理范围。
5.4 现象:每次运行生成的迷宫完全一样,没有随机性
原因:旧式 Qt 代码使用qsrand(time(0)),但如果你在构造函数里用了qrand(),而忘了调用qsrand,随机种子就是固定的。或者使用了某些版本的QRandomGenerator::global(),在特定平台上没有正确获得系统熵。解决:在生成迷宫前调用QRandomGenerator::global()->bounded(INT_MAX)强制刷新种子,或者改用QRandomGenerator::securelySeeded()。
5.5 现象:A* 寻路出来的路径绕远路,甚至不是最短路径
原因:启发式函数h(n)预估距离过大,导致 A* 退化成类似贪心搜索的行为。比如你把h(n)乘了一个大于 1 的系数,算法就会为了估算代价更小而去扩展不相关的节点。解决:确保h(n)不大于实际距离,对于只能上下左右移动的迷宫,使用曼哈顿距离且不放大系数即可。如果你看到路径明显绕弯,先检查h(n)公式是否被错误地写成了欧氏距离。
6. 把迷宫资源编译成独立可执行文件:qrc 配置与发布细节
当你把源码包里的.pro文件打开并跑通后,接下来需要关心的就是发布环节。Qt 程序在开发机上能跑,不代表拷到别的机器上也能跑。这里最常遇到的问题就是缺少 DLL 和插件。
关于.pro文件,源码包里已经有Maze.pro,你需要确认里面包含的资源项是否写全。一个典型的.pro配置是这样的:
QT += core gui greaterThan(QT_MAJOR_VERSION, 4): QT += widgets TARGET = Maze TEMPLATE = app SOURCES += main.cpp \ mainwindow.cpp \ maze.cpp HEADERS += mainwindow.h \ maze.h FORMS += mainwindow.ui RESOURCES += image.qrc如果发现图片无法加载,打开image.qrc检查路径前缀。QRC 文件的路径是相对于.qrc文件所在目录的,我拆包时发现里面有一组png图片和一个path.png、leaf.png、hero1.png,这些都需要在<qresource>标签内显式声明。引用资源时统一使用:/前缀/文件名的格式,不要在代码里写相对路径。
发布到其他机器时,用windeployqt工具自动拷贝依赖库。在 Qt 的命令行环境下执行:
windeployqt Maze.exe这个命令会把 Qt 的 DLL、平台插件(如qwindows.dll)、样式插件等全部复制到 exe 所在目录。但windeployqt不会处理第三方库,如果你在源码里用了opencv之类的外部库,需要手动拷贝。对于这份源码来说,核心依赖只有 Qt 自身,所以windeployqt基本能一次搞定。
发布路径上还有一个常见坑:如果你用了高版本的 Qt,比如 5.15,而目标机器只装了 VC++ 2015 运行库,可能启动后会报fatal: cannot mix incompatible Qt library之类的错误。解决方式是把对应的vcruntime140.dll也一起拷贝进发布目录。另外,如果你的 Qt 是 MSVC 版本编译的,而目标机器没有对应的 VC 运行库,程序会直接启动失败。用 MinGW 版本编译就没这个问题,因为 MinGW 运行时不需要 VC 运行库。
最后关于路径显示,path.png这组图片里的内容应该是路径标记用的贴图,如果你在自动寻路功能中看到路径线没有显示,优先检查这些图片的样式是否与迷宫格尺寸匹配。我第一次运行时路径是错位的,后来发现问题出在setPos的坐标基准没有对齐瓦片网格。从那以后我每次导入瓦片素材,都会强制在代码里打印一次QPixmap::width()和QPixmap::height()做日志输出,确认与tileSize一致才继续后续开发。
这份源码包的内容不算多,但覆盖了游戏开发中从算法到交互的完整链路,适合作为你进入 Qt 游戏开发的第一个完整工程。希望帮到你。
本文还有配套的精品资源,点击获取