简介:VC++迷宫游戏完整源码,基于MFC或Win32开发,运行时随机生成迷宫地图,玩家通过键盘方向键控制红色方块从起点走向终点,每次路径各不相同。资源面向C++初学者、高校计算机相关专业学生,以及需要课程设计或图形编程练手的开发者,侧重展示迷宫生成算法、键盘事件响应和基础绘图实现,无论是课堂作业还是自学提升都比较直观。压缩包共11个文件,主体包含C++主程序与多个功能头文件,分别负责随机地图生成、路径查找与迷宫渲染,辅以rc/aps资源文件和dsp/dsw工程配置,整体仅14KB,结构紧凑、模块划分清晰,使用VC++可直接打开编译。已有487人学习,代码按功能拆分,注释简洁,读者可据此调整迷宫尺寸、更换角色外观或增加计时计步功能,进一步改造成完整游戏,是一份轻量实用的VC++练习与参考资源。
1. 这东西到底值不值得下:一个能跑通的 VC++ 迷宫游戏项目
先给结论:这是一份用 VC++ 6.0 / MFC 写的迷宫游戏完整工程,核心亮点是「每局迷宫地图都随机生成」,不是那种把地图写死在数组里的教学 Demo。你用方向键控制一个红块从起点走到出口,走出迷宫就算赢。工程里包含createmaze.h(随机迷宫生成)、findway.h(路径计算)、drawmaze.h(绘图)这几个关键模块,编译后就是一个可玩的 Win32 窗口程序。适合三拨人:刚学完 C++ 想碰 MFC 的学生、要做课程设计但不想从零画界面的开发者、以及想研究「随机迷宫算法怎么落地成代码」的读者。源码是典型的老式 VC 工程结构,.dsp/.dsw 打开即可,没有花哨依赖,这恰恰是它最有价值的地方——你能在一两天内把它彻底拆明白。
2. 随机迷宫生成:从 createmaze.h 看深度优先回溯与墙的编码
2.1 迷宫的数据模型:二维数组里存的是什么
老式 MFC 迷宫最常用的地图模型不是「格子是否可走」,而是「墙的编码」。打开createmaze.h能看到类似这样的数据结构声明(不同版本命名略有差异,但思路一致):
#define WALL 1 #define PATH 0 // 迷宫尺寸,比如 31x31 的奇数宽高 #define MAZE_WIDTH 31 #define MAZE_HEIGHT 31 int maze[MAZE_HEIGHT][MAZE_WIDTH];这里的核心设计是:迷宫地图的宽高必须是奇数,并且起点和终点分别放在地图的左上角和右下角。为什么?因为随机迷宫生成算法要么用深度优先回溯,要么用随机 Prim,这两套算法都是基于「在墙之间挖路」的思想。如果地图宽高是偶数,挖路的坐标系会错位,最终会生成残缺地图,甚至出现起点或终点被墙堵死的情况。
实际存进maze[][]里的值只有 0 和 1。0 表示可以走的路,1 表示墙。drawmaze.h绘图时只需要遍历这个二维数组,遇见 1 就画一个实心矩形,遇见 0 就留空或者画浅色背景。所以整个游戏最核心的逻辑其实不复杂:随机生成算法负责把maze[][]填好,绘图模块负责把数组画出来,键盘处理模块负责修改「红块当前坐标」,碰撞检测模块判断坐标对应的maze[y][x]是否为 0。
2.2 主流算法选型:为什么我推荐递归回溯而不是随机 Prim
createmaze.h这个文件名暗示了它大概率用的是「创建迷宫」的老式思路。常见的迷宫生成算法有两类:递归回溯(Recursive Backtracker)和随机 Prim(Randomized Prim's Algorithm)。两者都能生成完美迷宫(任意两点之间有且仅有一条路径),但代码写法和迷宫风格完全不同。
递归回溯的核心是「一条路走到黑,走不通再退回来」。它的迷宫会有很多长长的走廊,求解时路径比较明确。随机 Prim 则更像是「从一堵墙集合里随机挑墙打穿」,生成的迷宫分支更多、更均匀,路径更绕,视觉上更“密”。
对于 VC++ 课程设计这个场景,我建议用递归回溯,因为它的递归栈实现非常直观,代码量也短。下面是能在createmaze.h里落地的最小实现思路:
// 用递归回溯生成迷宫的核心函数 void generateMaze(int startX, int startY) { // 四个方向:右、下、左、上 int dir[4][2] = { {2, 0}, {0, 2}, {-2, 0}, {0, -2} }; // 随机打乱方向顺序 for (int i = 3; i > 0; i--) { int j = rand() % (i + 1); int tmp[2]; tmp[0] = dir[i][0]; tmp[1] = dir[i][1]; dir[i][0] = dir[j][0]; dir[i][1] = dir[j][1]; dir[j][0] = tmp[0]; dir[j][1] = tmp[1]; } for (int k = 0; k < 4; k++) { int nx = startX + dir[k][0]; int ny = startY + dir[k][1]; // 边界检查:nx/ny 必须在 0 ~ MAZE_WIDTH-1 范围内 if (nx > 0 && ny > 0 && nx < MAZE_WIDTH - 1 && ny < MAZE_HEIGHT - 1 && maze[ny][nx] == WALL) { // 打通当前点到新点的墙 maze[startY + dir[k][1] / 2][startX + dir[k][0] / 2] = PATH; maze[ny][nx] = PATH; // 递归进入新格子 generateMaze(nx, ny); } } }这段代码的逻辑要拆开看。第一,方向数组里用的是步长 2 而不是 1,这是刻意的:因为迷宫以墙为格,格子之间隔着一个墙位。比如从 (1,1) 向右走两步到 (3,1),中间隔着 (2,1),那 (2,1) 就是需要打穿的墙。第二,maze[ny][nx] == WALL这个判断保证了每个格子只被访问一次,一旦被标记为 PATH,后续递归就不会再进入它,所以不会产生循环路径。第三,随机打乱方向数组这一步决定了迷宫每次生成都不一样,这正是「随机生成迷宫地图」的关键所在。
如果你手里这份createmaze.h用的是迭代版本,那也没关系。递归版本在 31x31 的地图下递归深度最多几百层,不会爆栈。但如果有人把迷宫扩到 101x101,递归版本就会在 Debug 模式下出现栈溢出,此时可以改成显式栈迭代写法。我在拆解这份工程时发现,老代码里常有一个.dsw工程默认的栈大小设置,是 1MB 还是#pragma comment(linker, "/STACK:...")都值得注意,后面避坑章再展开。
2.3 初始化时机与随机种子:为什么每次重开地图都一样
很多初学者把rand()直接放进generateMaze(),结果发现每次运行程序迷宫都一样。这是因为rand()的默认种子是 1,只有调用srand(time(NULL))才能让随机序列不同。在createmaze.h或Maze32.cpp的初始化位置,应该有这样一行:
srand((unsigned)time(NULL));这行代码必须在调用生成迷宫函数之前执行,并且只执行一次。如果你把它放在按钮点击事件里每按一下就调用一次,那也无妨,但要防止用户在同一个毫秒内连点两次按钮导致 seed 相同。更好的做法是用GetTickCount()做种子混合:
srand((unsigned)time(NULL) ^ GetTickCount());这是实际工程里常见的写法,因为time(NULL)的精度是秒,多线程或快速重开会遇到种子重复。迷宫这种场景虽然影响不大,但改成混合种子后,你再也不会遇到「连续三次随机出同一张图」的玄学问题。
参数方面,MAZE_WIDTH和MAZE_HEIGHT建议保持为奇数。常见的选择是(奇数)^2,比如 21、31、41。地图越大生成越慢,但在 31x31 下递归回溯几乎是瞬间完成,不需要担心性能。drawmaze.h在绘制时按这个尺寸等分窗口客户区即可,格子像素数 = 客户区宽度 / MAZE_WIDTH。
3. 键盘控制与碰撞检测:方向键响应与红块移动逻辑
3.1 MFC 捕获方向键:OnKeyDown 还是 PreTranslateMessage
MFC 迷宫游戏里,红块移动不是用定时器自动走,而是靠用户按键盘方向键。你会发现在Maze32.cpp中有一个消息映射宏:
BEGIN_MESSAGE_MAP(CMaze32View, CView) ON_WM_KEYDOWN() ON_WM_PAINT() END_MESSAGE_MAP()ON_WM_KEYDOWN()把键盘按下事件绑定到OnKeyDown函数。这里有一个常见的坑:如果这个类是从CView派生的,那么键盘焦点必须落在视图窗口上。如果你的程序是从CDialog派生的(有些迷宫 Demo 直接用对话框),那么对话框里的控件会抢焦点,导致方向键按下去没反应。解决办法有两种。
第一种是把视图类的样式设置为可以接收焦点,在OnInitialUpdate或OnUpdate里调用:
SetFocus();第二种是重写PreTranslateMessage,在消息分发前拦截方向键。这个方法更通用,因为它不关心焦点在哪个子控件上:
BOOL CMaze32View::PreTranslateMessage(MSG* pMsg) { if (pMsg->message == WM_KEYDOWN) { switch (pMsg->wParam) { case VK_UP: case VK_DOWN: case VK_LEFT: case VK_RIGHT: OnKeyDown((UINT)pMsg->wParam, LOWORD(pMsg->lParam), HIWORD(pMsg->lParam)); return TRUE; } } return CView::PreTranslateMessage(pMsg); }一般来说,课程设计里的迷宫游戏用OnKeyDown就够,但如果你后面加了按钮「重新开始」,点完按钮后方向键失效,那就是焦点被按钮抢走了,此时用PreTranslateMessage是后悔药级别的解决方案。
3.2 移动与碰撞:先判断后移动,别先把坐标改了再回头检测
红块移动的逻辑看起来很简单:按右方向键,红块 x 坐标加 1。但如果加完才发现撞墙又减回去,就会产生“红块闪一下”的体验问题,更严重的是会出现越界访问。正确顺序是:先计算目标位置,再判断目标位置的数组值,最后才赋值。
void CMaze32View::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags) { int newX = m_playerX; int newY = m_playerY; switch (nChar) { case VK_UP: newY--; break; case VK_DOWN: newY++; break; case VK_LEFT: newX--; break; case VK_RIGHT: newX++; break; default: CView::OnKeyDown(nChar, nRepCnt, nFlags); return; } // 边界检查:防止数组越界,越界当成墙处理 if (newX < 0 || newX >= MAZE_WIDTH || newY < 0 || newY >= MAZE_HEIGHT) { return; } // 碰撞检测:只有目标格为 PATH 才允许移动 if (maze[newY][newX] == PATH) { m_playerX = newX; m_playerY = newY; Invalidate(FALSE); // 触发重绘 } // 胜利判断:到达右下角出口 if (m_playerX == MAZE_WIDTH - 2 && m_playerY == MAZE_HEIGHT - 2) { MessageBox(_T("恭喜!你走出来了!"), _T("迷宫游戏"), MB_OK); // 可以在这里重新生成地图 } }参数说明:m_playerX和m_playerY是玩家当前所在格子的地图坐标,初始值一般是(1,1),因为迷宫边界是墙,起点放在(1,1)是内层第一格。MAZE_WIDTH - 2和MAZE_HEIGHT - 2是对应右下角的最后一个可达格。Invalidate(FALSE)的FALSE表示不擦除背景,直接重绘,这样能减少闪烁。
这里有个细节:Invalidate只是向消息队列投递一个WM_PAINT消息,并不是立刻重绘。如果你在循环里连续按键,队列里会积压多个重绘请求。高帧率下可以改用RedrawWindow(NULL, NULL, RDW_INVALIDATE | RDW_UPDATENOW)强制立即重绘,但这个游戏场景用Invalidate足够了。
再聊聊碰撞检测的边界条件。老代码里有人喜欢写成if (maze[newY][newX] != WALL),这样写和== PATH等价,因为数组里只有 0 和 1。但如果你的createmaze.h里后来又加了一种「已访问标记」或者「终点标记」,比如终点格子值为 2,那么!= WALL就会把终点当成可走区域放行——这本身没问题,但== PATH更严谨。我建议统一用== PATH,以后加特性不迷路。
4. 绘制与双缓冲:Maze32 的绘图层细节
4.1 OnDraw 函数里画迷宫:CDC 与矩形遍历
MFC 视图类的绘图入口是OnDraw。迷宫地图的绘制本质就是一个双重 for 循环:把maze[y][x]的每个格子转换成屏幕上的一个矩形。
void CMaze32View::OnDraw(CDC* pDC) { // cellWidth/cellHeight 是每个格子的像素尺寸 int cellWidth = GetClientWidth() / MAZE_WIDTH; int cellHeight = GetClientHeight() / MAZE_HEIGHT; // 先画背景 pDC->FillSolidRect(0, 0, GetClientWidth(), GetClientHeight(), RGB(255, 255, 255)); // 再画墙 CBrush wallBrush(RGB(0, 0, 0)); for (int y = 0; y < MAZE_HEIGHT; y++) { for (int x = 0; x < MAZE_WIDTH; x++) { if (maze[y][x] == WALL) { CRect cell(x * cellWidth, y * cellHeight, (x + 1) * cellWidth, (y + 1) * cellHeight); pDC->FillRect(cell, &wallBrush); } } } // 最后画玩家红块 CBrush playerBrush(RGB(255, 0, 0)); CRect playerRect(m_playerX * cellWidth + 2, m_playerY * cellHeight + 2, (m_playerX + 1) * cellWidth - 2, (m_playerY + 1) * cellHeight - 2); pDC->FillRect(playerRect, &playerBrush); }这个实现有几点值得说明。第一,GetClientWidth()和GetClientHeight()不是 MFC 自带方法,需要配合GetClientRect自取,比如:
CRect rc; GetClientRect(&rc); int width = rc.Width(); int height = rc.Height();第二,格子像素尺寸用整除计算,意味着如果窗口大小不是MAZE_WIDTH的整数倍,右边和下边会留出空白。这不算 bug,但如果你追求地图铺满窗口,可以在OnSize响应里根据窗口尺寸动态调整格子大小。第三,老代码里drawmaze.h可能用的是MoveTo/LineTo画线而不是FillRect,那种画法会留下细线缝隙,且窗口拉伸时线宽比例失调。FillRect是更稳妥的选择。
4.2 双缓冲:彻底干掉迷宫重绘闪烁
这是整个绘图模块里最容易翻车的部分。直接用OnDraw里的FillSolidRect画迷宫,窗口每次重绘都会先全部涂白,再逐格画黑墙,这个过程肉眼可见的闪烁。尤其是在窗口拖动、缩放或者连续按键时,闪烁会让人很难受。解决方案是双缓冲:先在内存里的CBitmap上画好整个迷宫,再一次BitBlt到屏幕。
void CMaze32View::OnDraw(CDC* pDC) { CRect rc; GetClientRect(&rc); // 创建内存 DC CDC memDC; memDC.CreateCompatibleDC(pDC); CBitmap memBitmap; memBitmap.CreateCompatibleBitmap(pDC, rc.Width(), rc.Height()); CBitmap* pOldBitmap = memDC.SelectObject(&memBitmap); // 在内存 DC 上绘制,这里复用 4.1 的绘制逻辑 DrawMaze(&memDC, rc); // 一次拷到屏幕 pDC->BitBlt(0, 0, rc.Width(), rc.Height(), &memDC, 0, 0, SRCCOPY); // 恢复选择 memDC.SelectObject(pOldBitmap); }逻辑说明:CreateCompatibleBitmap创建与屏幕兼容的位图,SelectObject把它选进内存 DC,之后所有FillRect、绘制玩家的操作都发生在内存里。最后BitBlt通过SRCCOPY把整块内存位图复制到窗口 DC。整个过程屏幕只刷新一次,闪烁自然消失。
参数说明:CreateCompatibleDC(pDC)的pDC是窗口 DC,创建的兼容 DC 必须配合兼容位图才能画图。每次OnDraw都重新创建位图虽然浪费一点时间,但在迷宫这种低刷新率场景没有性能问题。如果你以后把这个逻辑移植到WM_ERASEBKGND,记得返回TRUE阻止系统擦背景,否则BitBlt之后又擦一次,等于白做。
4.3 重新生成地图与重绘:别忘了一起更新玩家位置
迷宫游戏通常有一个「新迷宫」按钮或者按某个键重开。重开流程包含三件事:重新生成maze[][]、把玩家位置重置到起点、触发重绘。顺序不能错。
void CMaze32View::OnNewMaze() { // 1. 重置迷宫为全墙 memset(maze, WALL, sizeof(maze)); // 2. 从起点开始挖路 generateMaze(1, 1); // 3. 确保起点和终点打通 maze[1][1] = PATH; maze[MAZE_HEIGHT - 2][MAZE_WIDTH - 2] = PATH; // 4. 玩家回到起点 m_playerX = 1; m_playerY = 1; // 5. 强制重绘 Invalidate(FALSE); }memset把整个数组刷成墙,这一步必须有。因为如果新迷宫尺寸和旧迷宫相同,generateMaze只会修改它访问到的格子,没访问到的仍然保存上一局的数据,导致新旧地图叠加。generateMaze(1, 1)的递归会自己打通路径,但为了防止边界因素,把起点和终点手动赋值为PATH是防御性写法。终点在MAZE_HEIGHT - 2而不是MAZE_WIDTH - 2别忘了,这是二维数组的 y 坐标,很多人在这里把行和列写反,最终出现红块走到终点却提示未胜利的玄学问题。
5. 避坑与常见问题:VC++ 编译、字符集与资源文件的四道坎
5.1 用 VS2017/2019 打开 .dsw 工程,编译直接报错几十条
现象:拿到Maze32.dsw,用新版 Visual Studio 打开,提示旧工程转换,转换完一编译,报错一堆,最常见的是fatal error C1010: unexpected end of file while looking for precompiled header和Cannot open include file: 'afxtempl.h'。
原因:老 VC++ 6.0 工程默认启用预编译头文件(PCH),而源文件里如果没在开头包含stdafx.h,编译器就找不到预编译头。另外新版 VS 不再默认安装 VC6 的 MFC 库路径,某些老头文件路径失效。
解决:右键项目 → 属性 → C/C++ → 预编译头,把「创建/使用预编译头」改为「不使用预编译头」。同时把每个.cpp顶部的#include "stdafx.h"注释掉。如果还缺afxwin.h等,去安装 VS 的可选组件「适用于最新 v143 生成工具的 C++ MFC(x86 和 x64)」。这是老 VC 工程迁移到新 VS 的第一道坎,也是最容易劝退的坑。
5.2 字符集问题:printf 调试信息乱码,MessageBox 中文变问号
现象:在Maze32.cpp里写MessageBox("恭喜"),编译通过,运行时弹窗显示乱码或问号。或者用TRACE打印调试信息,输出窗口一片乱。
原因:VC6 时代默认使用 ANSI 编码,字符串字面量是 char*。新版 VS 的工程默认字符集是 Unicode,MessageBox被映射成MessageBoxW,要求宽字符串。你把窄字符串传进去,系统做了隐式转换但转换不了中文,就成了乱码。
解决:在项目属性 → 常规 → 字符集里,把「使用 Unicode 字符集」改成「使用多字节字符集」。这是最省事的方法,老代码基本都能恢复。如果你坚持用 Unicode,那就把所有字符串用_T()宏包起来,比如_T("恭喜")。_T宏在 ANSI 编译时展开成普通字符串,在 Unicode 编译时展开成L"..."宽字符串。我拆过的十个老 MFC 工程里有八个是直接切多字节解决的,剩下两个用了_T宏重写所有字符串,最后都稳定运行。
5.3 方向键没反应:焦点根本不在视图窗口上
现象:程序运行后,点击窗口任意位置,按上下左右方向键,红块纹丝不动。点击菜单或者按 Tab 之后,方向键完全失效。
原因:MFC 框架里,键盘消息是发给当前拥有焦点的窗口的。如果你的迷宫视图不是焦点窗口,OnKeyDown根本不会被调用。更隐蔽的是,如果这个项目是用CDialog做的,对话框里的默认按钮或编辑框会抢焦点。
解决:在OnActivateView或窗口初始化完成后调用SetFocus()。如果用了对话框,在对话框的OnInitDialog里给迷宫区域控件设焦点,或者干脆重写PreTranslateMessage拦截方向键。这里有个血泪经验:SetFocus()要在窗口显示出来之后再调用,放在构造函数里是无效的,因为窗口还没创建完成。最稳妥的位置是OnInitialUpdate或OnActivateView的第一个if分支里。
5.4 随机迷宫每次生成都一样:srand 没调用或调用太频繁
现象:点「新迷宫」按钮,连续生成五张地图,长得一模一样。关闭程序重新打开,第一张图还是那个样。
原因:这是 5.2 的老朋友——rand()没有施肥。没有调用srand,程序每次启动时种子固定为 1,随机序列完全相同。调用srand(time(NULL))但把它放在generateMaze函数内部,并且该函数被递归调用,相当于每次递归都重新设置种子,也会出现奇怪的重复模式。
解决:在InitInstance或主窗口构造函数里调用一次srand((unsigned)time(NULL))。注意不要在每次点击按钮时调用,除非你确保两次点击间隔超过 1 秒。我一般会写成srand((unsigned)time(NULL) ^ GetTickCount()),这样即使快速点击也不用担心同秒重复的问题。如果你发现改完还是重复,检查一下createmaze.h里是否有个static int maze[MAZE_SIZE]被定义成了常量数组,压根没走随机生成逻辑——有些精简版题目源码会预置一张地图当兜底,这个也遇到过。
5.5 资源文件 .rc 打不开或图标丢失:resource.h 与 .rc 不同步
现象:Maze32.rc双击打开提示「无法打开资源脚本」,或者编译时提示error RC2108: expected numerical dialog constant。程序能编译,但窗口图标是默认的,找不到IDR_MAINFRAME。
原因:老工程里resource.h和.rc文件必须配合使用。resource.h定义了 ID 宏,比如#define IDR_MAINFRAME 128。如果.rc里引用了某个 ID 而resource.h没有对应定义,RC 编译器就会报错。这种情况常发生在你把resource.hm(VC6 的辅助文件)删掉或改名的场景。
解决:先确认Maze32.rc开头#include "resource.h"还在,然后打开resource.h检查所有#define是否有重复或缺失。最粗暴但有效的方法:新建一个空的 MFC 工程,把它的resource.h复制过来,然后把自己Maze32.rc里用到的IDR_MAINFRAME、IDC_MAZE_BTN等 ID 改成新头文件里的对应值。这个操作不复杂,但容易被忽略。另外注意.rc文件编码,如果用 VS 保存成了 UTF-8 带 BOM,老编译器可能不认,建议用 VS 打开后另存为「Unicode(UTF-8 带签名)」,或者干脆保存为「Windows(默认代码页)」。
6. 进阶技巧:把 findway.h 用起来,加一个自动寻路演示
这份工程里最容易被忽略的文件是findway.h。很多初学者以为它没用,其实它才是加分项。findway.h大概率定义了一个寻路函数,比如bool findPath(int maze[][MAZE_WIDTH], int startX, int startY, int endX, int endY)。我的建议是,你把它从「编译不过的黑匣子」变成「能展示的亮点」:在界面加一个按钮「自动寻路」,点击后让红块自己按最短路径走向出口。
寻路算法最合适的是 BFS(广度优先搜索),因为迷宫地图小,BFS 能找到最短路径且代码简单,不需要引入 A* 的启发式函数。实现的思路是:用队列记录待访问节点,用visited[][]标记已经走过的格子,用parent[][]记录每个格子是从哪里来的,最后从出口回溯到起点,得到完整路径。
#include <queue> #include <vector> struct Point { int x, y; }; // BFS 寻路,返回从 (startX, startY) 到 (endX, endY) 的路径 std::vector<Point> findPathBFS(int maze[][MAZE_WIDTH], int startX, int startY, int endX, int endY) { // 访问标记和父节点记录 bool visited[MAZE_HEIGHT][MAZE_WIDTH] = { false }; Point parent[MAZE_HEIGHT][MAZE_WIDTH]; std::queue<Point> q; q.push({startX, startY}); visited[startY][startX] = true; parent[startY][startX] = { -1, -1 }; // 起点没有父节点 // 四个方向 int dir[4][2] = { {1,0}, {-1,0}, {0,1}, {0,-1} }; bool found = false; while (!q.empty() && !found) { Point cur = q.front(); q.pop(); for (int i = 0; i < 4; i++) { int nx = cur.x + dir[i][0]; int ny = cur.y + dir[i][1]; if (nx < 0 || ny < 0 || nx >= MAZE_WIDTH || ny >= MAZE_HEIGHT) continue; if (visited[ny][nx] || maze[ny][nx] == WALL) continue; visited[ny][nx] = true; parent[ny][nx] = { cur.x, cur.y }; if (nx == endX && ny == endY) { found = true; break; } q.push({nx, ny}); } } // 回溯路径 std::vector<Point> path; if (found) { Point cur = { endX, endY }; while (cur.x != -1) { path.push_back(cur); cur = parent[cur.y][cur.x]; } std::reverse(path.begin(), path.end()); } return path; }逻辑说明:BFS 从起点逐层向外扩展,第一次到达终点的路径一定是最短路径。parent数组记录每个格子的上一个格子,回溯时从终点一路往回找,最后reverse成从起点到终点的路径。visited防止重复入队,避免死循环。
参数说明:maze是全局或传入的迷宫数组,startX/startY是玩家当前位置,endX/endY是终点。返回的vector<Point>如果为空,说明当前迷宫没有通路——但理论上完美的随机迷宫一定有通路,所以空路径通常意味着你的generateMaze有 bug。调用后,你可以把这条路径画成蓝色小方块,或者做一个定时器让红块沿着路径自动走过去。
怎么接入现有工程?在按钮事件里调用findPathBFS,然后把路径存成成员变量m_path,再启动一个SetTimer(1, 100, NULL)。在OnTimer里每次取路径下一个点,更新m_playerX/m_playerY,调用Invalidate(FALSE)。路径走完后KillTimer(1)。注意定时器间隔不要小于 50ms,否则人会看不清移动过程,而且窗口重绘跟不上。这里有个我之前踩过的坑:定时器回调里如果迷宫正在重新生成,m_path里的坐标可能越界,所以每次触发「新迷宫」按钮时必须先KillTimer并清空m_path。
如果你把findway.h里的函数命名或参数和 BFS 版本不一致,不要硬套。先读它原有的函数签名,看看它是返回bool还是vector,然后在此基础上包装一层。老代码的findway大概率是 C 风格函数,只告诉你能不能走通,不返回路径。那就只用它做一个「检测按钮」:点击后弹窗提示「有通路」或「无通路」,这样也能展示你能看懂源码,而不是只把工程编译一遍就交差。
我个人用完这份工程的体会是:迷宫游戏的难点从来不在 MFC 框架,而在「数据怎样被生成、怎样被约束、怎样被移动」这三件事。随机生成解决地图多样性,碰撞检测解决合法性,BFS 寻路解决自动求解——这三块吃透了,换任何图形库都能重写一遍。从那以后我每次拿到老工程,都强制先读数据头和算法文件,再碰界面代码,这个习惯就是拆这类项目练出来的。希望这套拆解能帮你少走几段弯路。
本文还有配套的精品资源,点击获取