简介这是一份基于C与Qt框架实现的老鼠走迷宫游戏完整源码项目附带了可直接运行的EXE程序。项目面向C/Qt初学者、游戏开发入门者可帮助理解GUI程序搭建、迷宫随机生成、寻路算法与用户交互设计、自定义迷宫编辑等核心知识点。压缩包共53个文件其中dll动态库用于程序运行依赖cpp与h为源码工程ui和qrc负责界面与资源管理另附exe可执行文件整体约24.31MB工程文件与界面文件分离目录结构清晰便于按模块查阅目前已有291人学习下载。通过研读源码可以掌握Qt控件布局与事件驱动机制、深度优先/随机Prim等迷宫生成思路以及A*路径搜索的实现细节还能看到自定义迷宫编辑器中的墙壁与通道设置逻辑以及界面状态显示、控制选项、错误提示等交互处理方式适合作为C项目实战练习或毕业设计参考。1. 老鼠走迷宫游戏一套能跑能改的 QT6/C 迷宫源码先说结论这套基于 C 与 QT 框架实现的老鼠走迷宫游戏源代码不是那种只给几个 .cpp 文件就让人自求多福的半成品它自带编译好的 EXE双击就能进游戏还附了一整套 Qt6 运行库。你既能直接玩也能把源码丢进 Qt Creator 里改逻辑、换皮肤、调迷宫生成算法。资源里两个核心模块刚好覆盖了游戏开发里最常被问到的两件事随机迷宫怎么生成、自定义地图怎么存怎么走。想学 C 游戏开发但不想从零搭工程的人或者正在做课设、demo、面试作品的人这份代码是很好的落地参照。2. 工程结构与编译运行从 EXE 到源码的完整链路拿到压缩包第一件事不是急着点开 EXE而是先看清它的目录结构。这个资源里既有代码工程也有发布目录两者混在一起放恰恰适合用来理解「源码如何变成可分发程序」这条链路。下面按实际使用顺序拆开讲。2.1 先看文件清单每个模块是干什么的解压后能看到一批.cpp/.h/.ui文件外加MouseInMaze.pro工程文件和resource.qrc资源文件。把主要文件按职责归类大概是下面这张表文件/目录职责main.cpp程序入口创建 QApplication 并加载主窗口mainwindow.*主窗口框架承载模式选择入口selectgame.ui/.cpp/.h模式选择界面随机模式、自定义模式、路径展示randommode.*随机迷宫生成与老鼠寻路玩法diymode.ui/.cpp/.h自定义迷宫编辑器的外壳界面diygame.*自定义地图上的游戏运行逻辑allpath.*寻路模块用于展示从起点到终点的通路*.uiQt Designer 设计好的界面文件MouseInMaze.proqmake 工程配置声明模块、源文件、输出路径resource.qrc资源打包把图标、音效、图片编进二进制MouseInMaze.exe及一堆 Qt6 DLL已编译可执行程序与运行库platforms/、imageformats/、tls/等Qt 平台插件与运行时插件目录这里要特别提醒新手platforms目录不是摆设。Qt6 程序启动时会去加载platforms/qwindows.dll这个平台插件缺了它程序直接起不来报错类似could not find the qt platform plugin windows。这个坑在第五章专门讲。2.2 不编译也能玩EXE 与运行库的关系压缩包里带的MouseInMaze.exe是已经编译好的程序但它不能独立运行它依赖旁边那一堆 Qt6 DLL。你在文件列表里能看到Qt6Core.dll、Qt6Gui.dll、Qt6Widgets.dll、Qt6Network.dll、Qt6Svg.dll还有libgcc_s_seh-1.dll、libstdc-6.dll、libwinpthread-1.dll。这些名字暴露了构建环境MinGW 64 位工具链链接的是动态版 Qt6。我的习惯是拿到这种带运行库的程序先直接双击试跑确认它是好的再回头碰源码。如果双击报缺 DLL首先是检查这几个文件是否和 EXE 在同一级目录Qt 的程序不会去系统盘乱翻 DLL它先在自身目录找。其次是看platforms、imageformats、iconengines这些子目录有没有被误删一个都不能少。2.3 从源码编译qmake 与 Qt Creator 配置想改代码就得让工程在自己的环境里编译通过。这个项目是.pro工程常见做法是用 Qt Creator 直接打开MouseInMaze.pro它会自动生成构建目录。大体流程是# 命令行方式需提前把 Qt6 的 bin 目录加入 PATH qmake MouseInMaze.pro -spec win32-g CONFIGdebug mingw32-make -j8命令说明qmake读取.pro文件生成 Makefile-spec win32-g指定 MinGW 工具链CONFIGdebug表示编调试版方便打断点。我在 Windows 上编译 Qt6 程序时更推荐直接用 Qt Creator它会自动绑定编译器与 Qt 版本省掉环境变量那套事。打开工程后要注意.pro文件里模块声明这个项目用到了 Widgets 界面和网络库所以至少是这样的配置QT core gui widgets network greaterThan(QT_MAJOR_VERSION, 4): QT widgets CONFIG c11 TARGET MouseInMaze TEMPLATE app SOURCES main.cpp mainwindow.cpp randommode.cpp diymode.cpp diygame.cpp allpath.cpp selectGame.cpp HEADERS mainwindow.h randommode.h diymode.h diygame.h allpath.h selectGame.h FORMS mainwindow.ui randommode.ui diymode.ui diygame.ui allpath.ui selectGame.ui RESOURCES resource.qrc参数说明QT core gui widgets network决定链接哪些 Qt 模块如果代码里用了QNetworkAccessManager但没有network编译必报错greaterThan这行是为了兼容 Qt5 老代码Qt6 下其实可以删掉留着也不碍事RESOURCES把资源文件编进二进制图片和音效应在这里声明。编译完成后的产物在构建目录里比如build-MouseInMaze-Desktop_Qt_6_x_MinGW_64_bit-Debug。要把程序拷给别的机器或发给别人不能只带 EXE得把 DLL 和插件按 2.2 的方式组织好或者用 windeployqt 工具自动收集windeployqt MouseInMaze.exe这个工具是 Qt 官方提供的部署脚本它会扫出 EXE 依赖的所有 DLL 和插件并拷到同目录比我手动复制省心得多。3. 随机迷宫生成DFS 与 Prim 算法在 randommode 里的落地随机模式是这份代码里最有学习价值的部分。迷宫生成的算法不算多翻来覆去就是深度优先搜索DFS、Prim、Kruskal 这几个但每种算法的效果差异非常明显选错了生成的迷宫要么是一条长走廊要么是均匀但无聊的网格。下面结合 randommode 的实现思路拆透。3.1 先搞清楚迷宫的数据结构在看算法之前得先明白迷宫在内存里长什么样。这个项目用的是最经典的二维网格每个格子要么是墙要么是路。用 int 数组存约定 1 表示墙、0 表示路左上角是起点右下角是出口。这样一份网格在 C 里可以声明为const int ROWS 21; // 行数取奇数便于保证迷宫外圈是墙 const int COLS 21; // 列数 int maze[ROWS][COLS]; // 初始化全部为墙 for (int r 0; r ROWS; r) for (int c 0; c COLS; c) maze[r][c] 1;逻辑说明初始化把整个网格填成墙后续算法会在墙上「挖」出通路。行数和列数取奇数是个老经验因为迷宫外圈必须是完整墙体而内墙交错排布时奇数尺寸能保证每个通路格子都有规整的邻居关系避免出现 2 格宽的怪异通道。3.2 DFS 递归挖洞生成条理清晰但有主干道的迷宫randommode 里最基础的做法是 DFS 递归挖洞。思路是从起点格子出发随机选一个方向隔一格打墙打通后跳到下一格再递归。每次跳两格是关键中间打掉的那个格子就是通道这样墙的厚度始终是一格不会出现洞连洞的混乱局面。void generateMazeDFS(int r, int c) { // 标记当前格子为通路 maze[r][c] 0; // 随机打乱四个方向的顺序 std::vectorstd::pairint, int dirs {{-1,0},{1,0},{0,-1},{0,1}}; std::shuffle(dirs.begin(), dirs.end(), std::mt19937(std::random_device{}())); for (auto [dr, dc] : dirs) { int nr r dr * 2; // 隔一格跳过去 int nc c dc * 2; if (nr 0 nr ROWS nc 0 nc COLS maze[nr][nc] 1) { maze[r dr][c dc] 0; // 拆掉中间那面墙 generateMazeDFS(nr, nc); } } }逻辑说明maze[nr][nc] 1这个判断保证每个格子只被访问一次挖到边界或已访问区域就回头。maze[r dr][c dc] 0是在当前格与目标格之间拆一面墙这样路宽恒定一格。递归深度最多是 ROWS*COLS/4 量级21×21 的迷宫里大概一百多层栈完全扛得住如果加大到 101×101建议改成显式栈的迭代版否则递归有爆栈风险。效果上的特征是DFS 生成的迷宫「主干道」明显从起点往出口走路线长且绕分支也多是长条形的适合做冒险类游戏。如果你想要那种四通八达、分支均匀的迷宫就得换 Prim。3.3 Prim 算法均匀分布的迷宫是怎么生成的Prim 的思路和 DFS 完全不同它不是「一条路走到黑」而是维护一个「已挖掘格子的边界墙列表」每次从列表里随机挑一面墙墙那边如果连着未挖掘的格子就把墙拆掉、把那格挖开再把新边界加进列表。void generateMazePrim(int startR, int startC) { maze[startR][startC] 0; std::vectorstd::tupleint,int,int,int walls; // 先把起点的四条边墙加入候选 // 每个元素是 {墙行, 墙列, 墙对面格行, 墙对面格列} auto addWalls [](int r, int c) { for (auto [dr, dc] : std::vectorstd::pairint,int{{-1,0},{1,0},{0,-1},{0,1}}) { int wr r dr, wc c dc; int nr r dr * 2, nc c dc * 2; if (nr 0 nr ROWS nc 0 nc COLS) { walls.push_back({wr, wc, nr, nc}); } } }; addWalls(startR, startC); while (!walls.empty()) { // 随机取一面墙 int idx std::rand() % walls.size(); auto [wr, wc, nr, nc] walls[idx]; walls.erase(walls.begin() idx); if (maze[nr][nc] 1) { // 对面格子还没被挖过 maze[wr][wc] 0; // 拆墙 maze[nr][nc] 0; // 开挖新格 addWalls(nr, nc); } } }逻辑说明walls列表里存的是墙的位置和墙对面格子的位置随机抽墙保证了整体结构的随机性。if (maze[nr][nc] 1)防止把已连通的区域重复打通否则迷宫会出现多条捷径路径最优解就失去意义了。Prim 生成的迷宫没有明显的「主路」分支短而多几乎到处都有岔路绕路感比 DFS 弱但整体更均衡。我在自己做游戏时开放世界的迷宫地图偏好 Prim关底式迷宫偏好 DFS这个选择可以写进 randommode 的配置项里做成两个难度档位。3.4 随机种子为什么每次生成的迷宫不一样randommode 每次进入生成结果都不同原因是用了std::rand()或者QRandomGenerator::global()。前者不设种子的话默认种子是 1会导致每次启动程序后第一局迷宫完全一样这是新手常见的尴尬。#include QRandomGenerator // 用时间与设备噪声组合做种子 quint32 seed QRandomGenerator::global()-generate(); // 测试阶段则固定种子方便复现 bug // quint32 seed 20240617; qsrand(seed);参数说明QRandomGenerator::global()是 Qt 6 推荐的随机数来源它按系统熵池初始化不需要手动qsrand测试时把种子写死能让同一局迷宫反复复现调试寻路逻辑会轻松很多。这个「调试用固定种子发布用真随机」的习惯我后面还会再提一次。4. 自定义迷宫与寻路diy 模式加 allpath 的最短路径随机迷宫是电脑生成、人脑破解自定义迷宫则是人脑设计、程序来验证。这一层逻辑在diymode、diygame和allpath三个模块里协作完成。理解这段代码能顺带把「Qt 鼠标事件」「二维数组地图」「BFS 最短路径」三个知识点串起来。4.1 自定义地图编辑器鼠标画墙是怎么实现的diygame.ui里有一个可绘制区域常见做法是一个QGridLayout铺满按钮网格或者一个重写了mousePressEvent的QWidget。前者简单直观但控件多、内存大后者更符合游戏开发习惯。项目里大概率用的是第二种重写绘图区域的鼠标事件在点击处翻转格子状态。void DiyCanvas::mousePressEvent(QMouseEvent *event) { // 把鼠标坐标换算成格子坐标 int col event-position().x() / cellSize; int row event-position().y() / cellSize; if (row 0 || row ROWS || col 0 || col COLS) return; // 左键画墙右键擦墙 if (event-button() Qt::LeftButton) grid[row][col] 1; else if (event-button() Qt::RightButton) grid[row][col] 0; update(); // 触发重绘 }逻辑说明event-position()拿到的是鼠标在控件内的坐标用它除以单格像素cellSize就得到逻辑格子索引。左键设墙、右键拆除是实现「自定义设计迷宫」最直觉的交互方式。4.2 从 DiyMode 到游戏运行地图数据怎么传递自定义编辑器里画好的地图最终要交还给游戏逻辑去跑。Qt 里跨界面传数组数据常用的是在diygame的构造函数里接收一份二维数组副本或者通过 signal/slot 把网格发出去。实际项目里多半是前者DiyGame::DiyGame(const int grid[ROWS][COLS], int startRow, int startCol, int endRow, int endCol, QWidget *parent) : QWidget(parent) { memcpy(m_gameGrid, grid, sizeof(m_gameGrid)); m_start QPoint(startCol, startRow); m_end QPoint(endCol, endRow); }参数说明这里把grid完整复制进来避免原编辑器后续改动影响当前对局m_start和m_end用 QPoint 存储起点终点坐标老鼠的移动都围绕这两个锚点展开。4.3 allpath 寻路模块BFS 求最短路径并绘制allpath.*是整个代码库里我最想让读者反复看的文件。它的职责是给一张迷宫地图、给起点和终点求出一条最短路径并把它画出来。迷宫寻路的主流方案是 BFS因为它按层扩展第一次到达终点的路径必然最短。A* 当然也行但在小尺寸迷宫上 BFS 代码更短不用设计启发函数。std::vectorQPoint findShortestPath(const int grid[ROWS][COLS], QPoint start, QPoint end) { // 方向数组上、下、左、右 const int dr[] {-1, 1, 0, 0}; const int dc[] {0, 0, -1, 1}; // visited 记录是否访问过parent 记录前驱节点用于回溯路径 bool visited[ROWS][COLS] {false}; QPoint parent[ROWS][COLS]; std::queueQPoint q; q.push(start); visited[start.y()][start.x()] true; parent[start.y()][start.x()] QPoint(-1, -1); while (!q.empty()) { QPoint cur q.front(); q.pop(); if (cur end) break; // 到达终点停止搜索 for (int i 0; i 4; i) { int nr cur.y() dr[i]; int nc cur.x() dc[i]; if (nr 0 || nr ROWS || nc 0 || nc COLS) continue; // 越界跳过 if (grid[nr][nc] 1) continue; // 撞墙跳过 if (visited[nr][nc]) continue; // 已访问跳过 visited[nr][nc] true; parent[nr][nc] cur; q.push(QPoint(nc, nr)); } } // 回溯路径 std::vectorQPoint path; for (QPoint p end; p.x() ! -1; p parent[p.y()][p.x()]) { path.push_back(p); if (p start) break; } std::reverse(path.begin(), path.end()); return path; }逻辑说明visited二维数组用来防止回头路parent数组记录「我是从哪个格子走过来的」。搜索过程中一旦cur end就提前跳出因为 BFS 层序扩展保证了第一次碰到终点的路径长度就是最短的。最后从终点顺着parent回溯到起点再反序就得到从起点到终点的路径点集。参数说明grid是地图数据1 为墙、0 为路start、end是 QPoint注意代码里visited[start.y()][start.x()]的写法行是 y、列是 x这个坐标顺序错位是 Qt 新手高频 bug出现了「路径不对但报错不明显」的情况先检查是不是 x/y 写反了。绘图部分在allpath.ui里一般是 QPainter 重绘把 path 里的每个格子填充成高亮色顺便画一只小老鼠图标void AllPathWidget::paintEvent(QPaintEvent *) { QPainter painter(this); painter.setBrush(Qt::yellow); for (const QPoint p : m_path) { painter.drawRect(p.x() * cellSize, p.y() * cellSize, cellSize, cellSize); } }这里就体现出了「数据与表现分离」的思想寻路模块只返回坐标数组绘制模块只负责把数组渲染出来。你在 diy 模式里画完地图点「开始」游戏逻辑和数据流就是画地图 → 存二维数组 → 交给 DiyGame → 启动寻路 → 拿 path 绘制演示。整条链路清晰适合照着扩展。5. 避坑与排查QT6 运行环境常踩的五个坑这类解压即跑的 Qt 项目九成的求助帖都出在运行环境上而不是代码逻辑上。下面五条是我在实际分发、换机、重编译过程中反复踩过的按「现象 → 原因 → 解决」写成笔记建议对照着排查。5.1 EXE 双击没反应或提示缺少 DLL现象双击 MouseInMaze.exe系统弹窗「找不到 Qt6Widgets.dll」或直接闪退无提示。原因动态链接的 Qt 程序在启动时按固定顺序找依赖库先看 EXE 同级目录再看系统 PATH。压缩包里的 DLL 没和 EXE 放在同一目录或中途被安全软件隔离了。解决把全部 DLL 和 EXE 保持在同一目录。目录结构必须是MouseInMaze.exeQt6Core.dllQt6Gui.dllQt6Widgets.dll等平级然后让platforms/目录也保持在这个平级位置。重新用windeployqt部署一遍是最省心的方案。5.2 报错 could not find the qt platform plugin windows现象程序启动时终端输出类似qt.qpa.plugin: could not find the qt platform plugin windows in ...然后退出。原因Qt6 的窗口系统插件不是编进主程序的它单独存在于platforms/qwindows.dll。手工拷贝 DLL 时最常漏掉的就是这个目录。解决检查 EXE 同级目录下是否有platforms文件夹里面有qwindows.dll。如果是从别的机器打包来的请重新跑一次windeployqt MouseInMaze.exe它会自动生成全部插件目录。千万别试图把 qwindows.dll 放到platforms以外的地方Qt 只认固定路径。5.3 报错 cannot mix incompatible Qt library version现象自己打开.pro编译时报fatal: cannot mix incompatible Qt library (version 0x...) with this library。原因工程文件或构建环境里同时混入了不同版本的 Qt。比如机器上装了 Qt 5.15 又装了 Qt 6.xPATH里先找到的是某个旧版本的 qmake而代码里链接的又是另一个版本的库。MinGW 和 MSVC 的 ABI 也不兼容混用必出这错。解决检查 Qt Creator 的 Kit 设置确保编译器比如 MinGW 11.2、Qt 版本比如 Qt 6.5.3、qmake 三者来自同一个安装路径。命令行编译时先执行where qmake看实际调用的是哪个必要时把别的 Qt 从PATH里临时摘掉。经验是一个项目只用一套工具链别跨版本。5.4 程序启动后画面显示异常或报 OpenGL 相关错误现象程序能起来但窗口黑屏、花屏或者终端提示找不到opengl32sw.dll。原因Qt6 默认走 OpenGL 渲染管线目标机器的显卡驱动不提供完整 OpenGL 2.0 时会回退到软件渲染。opengl32sw.dll就是 Qt 自带的软件 OpenGL 实现它缺失或版本不匹配就会导致渲染异常。解决确认opengl32sw.dll与 EXE 同级更新显卡驱动仍然不行就强制 Qt 走软件渲染在程序入口加一行环境变量设置#include QApplication int main(int argc, char *argv[]) { qputenv(QT_OPENGL, software); // 强制软件渲染避免显卡兼容问题 QApplication app(argc, argv); return app.exec(); }参数说明QT_OPENGLsoftware告诉 Qt 不要尝试硬件加速直接把绘制指令交给 CPU 完成。对迷宫这种 2D 网格游戏来说软件渲染的性能完全够用代价只是内存占用稍高一点点。5.5 随机迷宫偶尔出现大面积空白或不通路现象生成几次后某次迷宫出现大片墙老鼠根本走不到出口或者有一条狭窄通路但一眼判定「不是迷宫」。原因生成算法的边界条件没处理好。常见原因有两个一是随机拆墙时拆到了外圈墙的边界打破了外圈墙体导致出口外露二是用了「随机打通所有相邻墙」的粗暴方案没有保证任意通路格子之间必有路径破坏了连通性。解决检查生成算法里是否限制nr、nc必须落在[1, ROWS-2]的范围内保证外圈墙体完整换用本章前面写的 DFS 或 Prim 标准实现它们挖洞时天然保证连通最后再加一道保险——鼠标寻路时起点和终点强制设在地图角落比如(1,1)和(ROWS-2, COLS-2)从物理上避免边缘漏洞。6. 进阶验证把随机迷宫固化成可复现关卡当你把 randommode 和 diymode 跑通接下来自然会产生一个新需求我想把某个漂亮的随机迷宫存下来当成关卡发给朋友玩。这个操作听起来是「保存图片」但更工程化的做法是保存地图数据本身。随机迷宫本质上是一张二维数组只要把数组序列化到文件再让 diymode 支持读入就完成了「随机生成 → 人工调整 → 固定关卡」的闭环。具体做法是在 randommode 里加一个「导出地图」按钮把当前maze[ROWS][COLS]按文本格式写出一行一个格子1 和 0 用空格分开对应的diygame 增加「导入地图」读取这个文本并填充回网格。文件头加上起点和终点坐标格式自定比如第一行21 21表示行列总数第二行1 1 19 19表示起点行、起点列、终点行、终点列剩下按行粘贴数组。这样导出的关卡体积只有几百字节比截图更精确还能直接进入寻路演示。验证迷宫是否合格我的习惯是写一段自动检查脚本对生成的迷宫跑一次 BFS统计从起点出发能到达多少格子。如果可达格子占全部通路格子的比例低于 95%说明生成逻辑有漏洞如果比例是 100%说明迷宫完全连通可以作为成品关卡。这个检查手段能压住随机算法「偶尔翻车」的脾气。我自己在这类课设项目上吃过一次亏当时为了演示效果直接改了 randommode 的代码却没动生成种子连续几次演示的迷宫一模一样被台下追问「是不是硬编码的」场面相当尴尬。从那以后我每改一次随机生成相关的代码都强制自己先跑一轮固定种子的回归测试确认改动生效再切回随机种子。随机是给玩家的可控是给开发者的两者之间得有个开关。这份资源把随机生成和自定义编辑两套流程都给你了建议你先把 EXE 玩明白再打开 Qt Creator 从 randommode 读起一步步把你的第一个 C 游戏从「能玩」改到「能秀」。希望帮到你。本文还有配套的精品资源点击获取