简介:这份压缩包提供了一套用 MFC 编写的简易迷宫游戏项目,面向刚接触 C++ Windows 编程的开发者,帮助理解窗口程序设计与基础算法实现。项目采用 MFC 文档/视图架构,涉及二维数组迷宫建模、深度优先或广度优先路径搜索、键盘鼠标交互、GDI 绘图,以及菜单和按钮消息处理等知识点,完整展示了一个小游戏从界面搭建到交互逻辑落地的过程。
资源共 45 个文件,压缩包体积为 2.13MB。除 C++ 源代码(.cpp/.h)和 VC6.0 工程文件(.dsp/.dsw)外,还包含可执行的 .exe、界面与迷宫图片(.ico/.bmp)、分步骤演示截图(.png)、思路说明文档,以及编译生成的中间文件;各类文件层次清晰,适合对照源码逐一查看。
尤其适合学习 MFC 编程的初学者通过阅读源码掌握窗口消息映射、控件使用与简单算法实现,也可参考其中的迷宫设计思路,自行修改地图数据或扩展为更复杂的寻路演示。压缩包中保留了完整的工程结构和可运行程序,便于在 Visual C++ 6.0 环境中直接打开调试。
目前已有 378 人学习使用,可作为 MFC 自学、课程设计或毕业设计的基础素材。
1. MFC迷宫游戏拆解:拿课程设计练手C++与GUI消息循环
室友答辩前夜递过来一个MFC工程,说迷宫游戏跑不起来,让我帮忙看看。打开一看,一个基于对话框的Windows程序,点击“新迷宫”按钮生成随机地图,用方向键控制红色方块从左上角走到右下角,步数在界面上实时刷新,到达出口弹窗提示胜利。整个项目只有一个迷宫生成算法、一个绘制函数、一个键盘消息处理,却把C++类封装、Windows消息循环、GDI绘图、碰撞检测串成了一条完整链路。
这套工程很适合三类人:刚学完C++想接触Windows GUI开发的,准备交MFC课程设计但不知道从哪下手的,以及已经写过MFC却想看看别人怎么组织迷宫数据结构的老哥。说白了,它不是一个炫技项目,而是一个“麻雀虽小五脏俱全”的MFC入门范本,从新建对话框到二次开发扩展,每一个环节都能拆出来讲清楚。
2. 架构与选型:对话框程序、位标记地图与消息分发
2.1 为什么是MFC对话框而不是单文档视图
拿到这套工程时我第一个关注点是它为什么选对话框程序。MFC应用向导里通常有单文档、多文档、对话框三种形态,单文档视图结构适合文本编辑器、画图板这类“一个文档对应一个视图”的场景,而迷宫游戏只有一个画布、一组按钮、几个文本控件的固定布局,用对话框承载游戏逻辑是更轻的做法。
对话框程序的主窗口从CDialogEx派生,资源文件里拖几个控件就能搭出界面骨架,不需要处理框架窗口和视图的分离关系。我之前的习惯也是这样,凡是玩法单一的桌面小游戏,优先用对话框;真要做关卡编辑器、多视口联动的工具,再考虑单文档架构。
这套工程的主对话框承载了几乎所有游戏逻辑,唯一的例外是迷宫生成算法被独立成了一个CMaze类。这个拆分看起来不起眼,但对后续改造很关键:算法不依赖具体的控件和绘图,意味着我可以单独对CMaze写测试,也可以随时把迷宫生成换成别的算法而不动界面代码。
2.2 工程文件结构与核心类职责
工程文件的组织方式很典型,拆开来看一共四组关键文件:
| 文件 | 职责 |
|---|---|
| LabyrinthApp.h / LabyrinthApp.cpp | CWinApp派生类,初始化程序实例并创建主对话框 |
| LabyrinthDlg.h / LabyrinthDlg.cpp | CDialogEx派生类,主对话框,负责控件管理、键盘消息、绘制调度 |
| Maze.h / Maze.cpp | 迷宫算法封装,随机生成地图、查询墙状态 |
| resource.h / Labyrinth.rc | 资源脚本,定义对话框布局、按钮ID、图标与版本信息 |
主流程比较直接:WinMain入口由MFC框架提供,App类在InitInstance里new出主对话框并DoModal,用户点击按钮后走BN_CLICKED消息映射,键盘事件在PreTranslateMessage或OnKeyDown里拦截,每次状态变化调用Invalidate触发WM_PAINT。一条用户在键盘上的操作到屏幕画面变化,中间经过的环节在这套工程里都能找到落点。
我比较欣赏的是CMaze这种分离设计。早期写MFC游戏我习惯把所有东西塞进Dlg类,迷宫数组、玩家坐标、步数、绘制代码全堆在一起,后期加功能时一个文件几千行,改起来想骂人。这套工程把“地图数据”和“界面表现”分开,让核心逻辑不依赖窗口句柄,这是值得保持的架构习惯。
2.3 地图数据结构:四面墙的位标记设计
迷宫地图的存储方式是这套工程最值得看的数据结构设计。常见做法是给每个格子开一个结构体,记录上下左右四堵墙是否存在,但这里的做法更省内存:用一个unsigned char存储一个格子的四堵墙,每个bit位对应一个方向。
我一般会用这样的约定:bit0表示上墙,bit1表示右墙,bit2表示下墙,bit3表示左墙,值为1表示这堵墙存在,值为0表示这面是通的。这样一个15×15的地图只需要225个字节,比bool四维数组省不少空间,而且查询和拆除墙壁只需要位运算,效率很高。
玩家坐标用(m_nPlayerRow, m_nPlayerCol)表示,对应地图数组的行列下标,出口固定在右下角(m_nMazeRow - 1, m_nMazeCol - 1)。界面绘制时,把行列坐标乘以格子像素边长就得到屏幕坐标,这个换算关系贯穿了后面的OnPaint和移动检测逻辑。
位标记设计的好处在判定碰撞时体现得最直接。判断“玩家能不能向上走”,只需要检查当前格子m_walls[row][col]的bit0是否为1,一行代码就能完成,比比较两个相邻格子的墙数组状态要清晰得多。
3. 核心代码拆解:迷宫生成、绘制与移动的完整链路
3.1 递归回溯生成迷宫:方向打乱与拆墙
迷宫生成算法采用的是递归回溯,也叫深度优先搜索生成法。它的思路不复杂:从一个格子出发,随机挑一个未访问过的相邻格子,拆掉两面之间的墙,然后递归进入新格子;如果当前格子四周都已访问过,就回退到上一个格子继续尝试其他方向。
// Maze.h 核心接口 #pragma once #include <vector> class CMaze { public: void Generate(int row, int col); bool IsWall(int r, int c, int dir) const; // dir: 0上 1右 2下 3左 int GetRow() const { return m_nRow; } int GetCol() const { return m_nCol; } private: void Dig(int r, int c); std::vector<unsigned char> m_walls; // 每个格子的四面墙位标记 std::vector<bool> m_visited; int m_nRow = 15; int m_nCol = 15; };// Maze.cpp 生成算法主体 void CMaze::Dig(int r, int c) { m_visited[r * m_nCol + c] = true; // 四个方向的候选顺序,每次洗牌保证随机性 int dirs[4] = { 0, 1, 2, 3 }; for (int i = 3; i > 0; --i) { int j = rand() % (i + 1); int temp = dirs[i]; dirs[i] = dirs[j]; dirs[j] = temp; } for (int i = 0; i < 4; ++i) { int dir = dirs[i]; int nr = r + dr[dir]; // dr/dc 是方向偏移数组 int nc = c + dc[dir]; // 越界检查 if (nr < 0 || nr >= m_nRow || nc < 0 || nc >= m_nCol) continue; // 已访问则跳过 if (m_visited[nr * m_nCol + nc]) continue; // 拆除当前格子与相邻格子之间的墙 m_walls[r * m_nCol + c] &= ~(1 << dir); // 清除当前格子的墙 m_walls[nr * m_nCol + nc] &= ~(1 << (dir ^ 2)); // 清除相邻格子的反向墙 Dig(nr, nc); // 递归进入新格子 } }这段代码有两个细节值得注意。第一是方向数组的打乱方式,每次从当前位置随机抽取一个方向跟末尾元素交换,相当于原地洗牌,rand() % (i + 1)保证每个方向被选中的概率均匀。第二是dir ^ 2这个位运算技巧,方向上右下的序号分别是0、1、2、3,与2按位异或刚好得到相反方向:0变2,2变0,1变3,3变1。拆墙时同时清除当前格子的墙和相邻格子的反向墙,保证通道是双向可见的。
参数说明:r和c是当前递归格子的行列坐标,m_nRow和m_nCol控制地图规模,乘积r * m_nCol + c是把二维下标压成一维vector下标。递归出口藏在“四方向全部已访问”这个条件里,此时函数自然返回,回溯到上一级继续找可走的路径。
3.2 OnPaint双缓冲绘制:从二维数组到屏幕像素
绘制部分的代码设计直接影响视觉体验,未使用双缓冲时窗口拖动会出现白块和闪烁。常见做法是先在内存位图里画好整幅画面,再一次BitBlt到窗口,这套工程采用了同样的策略。
void CLabyrinthDlg::OnPaint() { CPaintDC dc(this); // 1. 创建内存画布,避免闪烁 CDC memDC; memDC.CreateCompatibleDC(&dc); CBitmap bmp; bmp.CreateCompatibleBitmap(&dc, m_nMazeCol * m_nCellSize, m_nMazeRow * m_nCellSize); CBitmap* pOldBmp = memDC.SelectObject(&bmp); // 2. 填充背景色 CBrush bgBrush(RGB(240, 240, 240)); memDC.FillRect(CRect(0, 0, m_nMazeCol * m_nCellSize, m_nMazeRow * m_nCellSize), &bgBrush); m_nCellSize 是一个可调整的全局值,工程里默认是30像素。 // 3. 遍历每个格子画墙 for (int r = 0; r < m_nMazeRow; ++r) { for (int c = 0; c < m_nMazeCol; ++c) { int x0 = c * m_nCellSize; int y0 = r * m_nCellSize; if (maze.IsWall(r, c, 0)) // 上墙 dc.FillRect(CRect(x0, y0, x0 + m_nCellSize, y0 + 2), &wallBrush); if (maze.IsWall(r, c, 1)) // 右墙 dc.FillRect(CRect(x0 + m_nCellSize - 2, y0, x0 + m_nCellSize, y0 + m_nCellSize), &wallBrush); if (maze.IsWall(r, c, 2)) // 下墙 dc.FillRect(CRect(x0, y0 + m_nCellSize - 2, x0 + m_nCellSize, y0 + m_nCellSize), &wallBrush); if (maze.IsWall(r, c, 3)) // 左墙 dc.FillRect(CRect(x0, y0, x0 + 2, y0 + m_nCellSize), &wallBrush); } } // 4. 画玩家方块 CBrush playerBrush(RGB(200, 50, 50)); int px = m_nPlayerCol * m_nCellSize + m_nCellSize / 4; int py = m_nPlayerRow * m_nCellSize + m_nCellSize / 4; memDC.FillRect(CRect(px, py, px + m_nCellSize / 2, py + m_nCellSize / 2), &playerBrush); // 5. 整体拷贝到窗口 dc.BitBlt(0, 0, m_nMazeCol * m_nCellSize, m_nMazeRow * m_nCellSize, &memDC, 0, 0, SRCCOPY); memDC.SelectObject(pOldBmp); }绘制顺序比较讲究:先铺背景色,再画墙,最后画玩家,这样玩家永远显示在墙壁之上。画墙的方式不是画整条长线,而是每个格子只画自己的上墙和左墙以及必要时补充右墙和下墙,这样相邻格子之间才不会出现重叠的粗线条。
坐标换算参数:m_nCellSize是单个格子的像素尺寸,工程默认30,玩家方块大小固定为格子的一半,起始位置在格子的四分之一偏移处,视觉上居中。需要提速时可以只对发生变化的格子局部重绘,但迷宫整图重绘在30×30以内基本无感,不必过度优化。
3.3 玩家移动与碰撞检测:键盘消息到坐标更新
移动逻辑的核心是“先检查墙再更新坐标”,墙检查失败则直接忽略本次按键。实现上要处理两个层面:消息从哪来,以及消息如何处理。对话框程序里方向键的接收是个易踩坑点,后面会详细说,这里先看处理函数。
BOOL CLabyrinthDlg::PreTranslateMessage(MSG* pMsg) { // 在消息分发前截获键盘事件 if (pMsg->message == WM_KEYDOWN && pMsg->hwnd == GetSafeHwnd()) { switch (pMsg->wParam) { case VK_UP: MovePlayer(0); return TRUE; case VK_DOWN: MovePlayer(2); return TRUE; case VK_LEFT: MovePlayer(3); return TRUE; case VK_RIGHT: MovePlayer(1); return TRUE; default: break; } } return CDialogEx::PreTranslateMessage(pMsg); } void CLabyrinthDlg::MovePlayer(int dir) { int nr = m_nPlayerRow + dr[dir]; int nc = m_nPlayerCol + dc[dir]; // 边界检查 if (nr < 0 || nr >= m_nMazeRow || nc < 0 || nc >= m_nMazeCol) return; // 碰撞检测:当前格子在该方向是否有墙 if (maze.IsWall(m_nPlayerRow, m_nPlayerCol, dir)) return; // 更新坐标 m_nPlayerRow = nr; m_nPlayerCol = nc; m_nStepCount++; // 更新界面步数显示 SetDlgItemInt(IDC_STEP_TEXT, m_nStepCount); // 触发重绘,参数FALSE表示背景不变 Invalidate(FALSE); // 胜利判定 if (nr == m_nMazeRow - 1 && nc == m_nMazeCol - 1) { MessageBox(_T("恭喜,到达出口!"), _T("提示"), MB_OK); } }移动流程里最容易被忽略的是碰撞检测与边界检查的顺序。先做边界检查是因为数组越界比撞墙更危险,一旦下标越界程序可能直接崩溃。墙体检测只是判断当前格子目标方向的bit位是否为1,为0则说明通道畅通,可以放心走。
参数说明:dir复用0上1右2下3左的编码,与迷宫生成时的方向一致,所以dr/dc偏移数组在生成、绘制、移动三处共用,统一了方向语义。m_nStepCount在生成新迷宫时清零,SetDlgItemInt只做刷新不做校验,因为已经保证整数非负。胜利判定放在坐标更新之后,保证到达出口那一帧画面已经重绘。
这套流程设计上有个实用性考量:把移动逻辑独立成MovePlayer函数而非塞进switch里,后续要支持点击按钮移动或者手柄输入时,只需增加入口调用同一函数,核心判定不会走散。
4. 编译运行与动手点:从新建工程到自定义地图规模
4.1 VS创建MFC对话框工程的完整步骤
拿到这套工程源码后第一件是把它跑起来。如果你从零开始建一个同款工程,建议直接在Visual Studio里选“MFC应用”,应用程序类型选“基于对话框”,语言保持C++。别选“单文档”,那会生成一堆用不上的框架类,对话框形态最贴合这套工程的结构。
创建完成后会生成一个带有“确定”“取消”按钮的空对话框,需要把手动添加一个“新迷宫”按钮,把默认的静态文本改成步数显示区。比较快的做法是把自带按钮删掉,从工具箱拖一个新Button和一个Static Text,ID分别设为IDC_BTN_NEW与IDC_STEP_TEXT。
把CMaze类的Maze.cpp和Maze.h加入工程后,记得在主对话框头文件里包含Maze.h并声明一个成员变量CMaze maze。工程默认生成的CLabyrinthDlg构造函数里,需要初始化玩家坐标、步数计数器、迷宫行列数以及格子像素尺寸。棋盘大小我一般用15×15起步,这个规模递归回溯生成够快,视觉上也不会太小。
编译时最容易遇到的坑是MFC头文件路径和字符集设置。新建工程时如果勾选了“使用Unicode库”,那么所有字符串字面量都要用_T()宏包裹,代码里直接写"提示"这种窄字符串编译会报类型不匹配。这套工程源码里用的都是_T()宏,你完全不用改。如果非要改成多字节字符集,属性管理器里“常规→字符集”改一下就行。
4.2 按钮消息映射与键盘焦点处理
“新迷宫”按钮的事件处理要用类向导添加,右键按钮选“添加事件处理程序”,消息类型选BN_CLICKED,函数名默认OnBnClickedBtnNew。这个函数的核心工作是重置一局游戏状态:
void CLabyrinthDlg::OnBnClickedBtnNew() { // 重新生成迷宫地图 maze.Generate(m_nMazeRow, m_nMazeCol); // 重置玩家状态 m_nPlayerRow = 0; m_nPlayerCol = 0; m_nStepCount = 0; SetDlgItemInt(IDC_STEP_TEXT, m_nStepCount); // 强制全量重绘 Invalidate(TRUE); }这段代码本身没有复杂逻辑,但注意三个顺序:先生成迷宫,再重置玩家,最后重绘。如果先重置玩家再生成迷宫,界面可能在极短时间内显示旧地图新坐标,虽然肉眼几乎察觉不到,但设计上应该让数据状态一次性更新完毕。
键盘焦点问题是对话框程序的通病,新手常常在这里翻车:界面跑起来后方向键要么没反应,要么按方向键时焦点在“新迷宫”按钮上移动而不是移动玩家。原因在于对话框控件默认会吃掉方向键用于焦点切换,所以不能指望自带的WM_KEYDOWN消息处理。
解决方法是重写PreTranslateMessage,让键盘消息在到达控件之前先被主对话框截获。前面3.3小节的实现里就是通过这个虚函数拦下四个方向键并直接返回TRUE吞掉消息,防止焦点发生转移。唯一要留意的是消息的hwnd过滤条件,防止误拦截其它控件输入,比如未来如果加一个编辑框输入迷宫尺寸,方向键还要留给编辑框用。
4.3 地图尺寸可设置的改造入口
想调整迷宫难度,最简单是改m_nMazeRow和m_nMazeCol两个成员变量的初始值。但要注意递归回溯生成算法的时间复杂度跟格子数量的关系,15×15几乎瞬间完成,32×32约有上千次递归调用,依旧流畅;设到60×60以后递归深度可能逼近栈上限,程序可能在生成阶段就崩掉,这就是第5章要讲的坑之一。
我一般会顺手加一个“难度”下拉框或者编辑框,把行列数作为参数传给Generate函数。改造时保持CMaze类接口不变,只改调用处:
void CLabyrinthDlg::OnEnChangeEditSize() { // 从编辑框读取输入值并校验 int newSize = GetDlgItemInt(IDC_EDIT_SIZE); if (newSize < 5 || newSize > 50) { MessageBox(_T("请输入5~50之间的数字"), _T("提示"), MB_OK); return; } m_nMazeRow = newSize; m_nMazeCol = newSize; }把行列合并成一个值,既简化输入逻辑,也让迷宫保持方形。格子像素尺寸可以动态计算:m_nCellSize = max(10, min(60, 600 / newSize)),确保大迷宫不至于把窗口撑爆。这套思路适合继续往后扩展成三个编辑框分别控制行列数和格子大小。
5. 避坑记录:字符集、重绘与消息映射的五个大坑
5.1 从现象到原因到解决
坑1:方向键按了没反应,焦点却在按钮上跳来跳去
现象:程序跑起来后,点击窗口空白处再按方向键,玩家纹丝不动,反而“新迷宫”按钮和高亮边框在之间切换。
原因:对话框程序默认会拦截方向键进行控件导航,键盘焦点仍在某个控件上,WM_KEYDOWN消息被控件处理掉了,根本没走到玩家移动逻辑。
解决:重写PreTranslateMessage,在消息分发前捕获WM_KEYDOWN,处理四个方向键后直接返回TRUE。加上pMsg->hwnd == GetSafeHwnd()这个过滤条件确保只拦截发给主窗口的按键,后续添加可输入控件时两边的消息不会打架。
这套工程就是因为预见到了这个坑,才特意把键盘截获统一放在PreTranslateMessage而非OnKeyDown里。当你的程序症状对得上、按键无效时,优先查这个函数是否存在、方向键的case是否完整,而不是怀疑碰撞检测写错了。
坑2:窗口拖动后迷宫区域花屏、闪烁
现象:迷宫生成正常,但拖动窗口边框或调整大小时,画布出现一片一片的白色残影,严重时整个迷宫闪得看不清。
原因:窗口背景默认用白色擦除,OnPaint执行重绘前,OnEraseBkgnd先把背景刷成白色,两个操作的时序不同步,人眼就看到了闪白。
解决:启用双缓冲。把绘制内容画到内存位图,再一次性BitBlt到屏幕,同时在OnEraseBkgnd里直接返回TRUE屏蔽默认擦除。回到这套工程的绘制代码,逻辑已经如此实现,真正需要注意的点是Invalidate(FALSE)和Invalidate(TRUE)的区别,前者不触发背景擦除,对画布稳定性更友好。
坑3:文本显示乱码,按钮标题变成问号
现象:MessageBox里的中文提示或按钮文字在界面上显示为乱码,或者干脆编译器就报了“无法将参数从const char*转换为LPCTSTR”的错误。
原因:工程的字符集设置和源文件里字符串字面量的编码不一致。MFC应用向导默认Unicode,资源文件保存为UTF-8时中文能在界面正常显示,但代码文件里的中文字符串如果直接写窄字符串,编译器会按当前代码页解释导致乱码。
解决:所有字符串字面量一律用_T()包裹,例如_T("提示")。源文件统一保存为UTF-8带签名,这样不管是Unicode工程还是多字节工程都能正确解析。另一个补救是在项目属性里把字符集改成“使用多字节字符集”,让窄字符串直接对应GBK编码,但这是偷懒不是正道。
坑4:迷宫尺寸调大后,程序在点“新迷宫”时崩溃
现象:把迷宫行列从15改成50,再点“新迷宫”按钮,程序直接弹出“已停止工作”的异常框。
原因:递归回溯是深度优先遍历,递归深度和格子数量成正比。50×50意味着最坏情况下有2500层递归,栈溢出风险极高。另外,visited数组和walls数组的resize如果填错了尺寸,越界写入也会静默触发崩溃。
解决:迷宫规模限制在5×5到40×40之间,超出建议改成迭代式回溯,用栈数据结构代替递归调用。我这里看到工程代码用递归是因为它按默认15×15设计,并没有刻意做成大图。二次开发时如果再往大图方向扩展,务必把Generate改成栈模拟。
坑5:点击按钮后界面卡死,CPU占用飙满
现象:点“新迷宫”后程序没有崩溃,但窗口完全无响应,任务管理器里CPU占用接近100%。
原因:死循环。常见来源是Generate函数里方向洗牌逻辑出错,比如随机数没有重置种子导致每次方向完全一致,递归永远走不完;或者访问格子时行列下标算错,visited数组越界访问但该内存恰好为false,于是不断尝试重访。
解决:在Generate入口加一行条件断点或TRACE日志,输出当前行列,观察是否有重复访问同一个格子没退出的情况。另外rand()要用srand((unsigned)time(nullptr))初始化种子,否则每次生成的迷宫都是同一张图,看不出随机性,也容易掩盖逻辑问题。
5.2 断点调试与日志定位技巧
MFC程序出错时,对话框界面很容易直接崩溃弹窗,单步调试是最直接的排障手段。我习惯在递归回溯的入口和拆墙操作处打断点,按F5运行后打开“调用堆栈”窗口,可以直观看到递归深度。如果调用堆栈里有成百上千层Generate,那就是栈深度问题,基本可以认定是迷宫尺寸太大。
对于不崩溃但行为不对的bug,OutputDebugString比MessageBox更合适,不会弹出模态框打断游戏流程。比如在MovePlayer里输出移动方向和当前位置,就能判断是消息没传进来还是碰撞检测误判。手动验证时我一般开着“输出”窗口,把每一帧的玩家坐标打出来,比盲改代码效率高得多。
这套工程规模小,不建议一上来上重型调试工具,先看三个位置:PreTranslateMessage是否拦截到按键、MovePlayer的边界检查是否拦住坐标、IsWall查询的方向编号与墙壁绘制是否一致。排查顺序由外到内,先确认消息流路通,再查逻辑正确性。
6. 二次开发思路:计时、存档与自动寻路
6.1 加入计时与胜利结算
给游戏加上限时挑战是成本最低的增强。在“新迷宫”按钮里用SetTimer(1, 1000, nullptr)启动每秒一次的定时器,保存一个m_nElapsedSeconds计数,在OnTimer里更新时间显示:
void CLabyrinthDlg::OnTimer(UINT_PTR nIDEvent) { if (nIDEvent == 1) { m_nElapsedSeconds++; SetDlgItemInt(IDC_TIME_TEXT, m_nElapsedSeconds); } CDialogEx::OnTimer(nIDEvent); }胜利时KillTimer(1),把步数和用时写入某个成绩结构,结合CFile写文件就能做一个简单的排行榜。需要注意销毁窗口前在OnDestroy里也调用一次KillTimer,否则定时器会一直存在直到系统回收,属于典型的资源泄漏。
6.2 自动寻路:把迷宫当成图做BFS
进阶玩法是加一个“自动演示”按钮,用BFS计算最短路径,在OnPaint里用另一支笔把通路画出来。BFS实现时把每个格子当节点,四面墙状态决定哪些邻接节点可达,用队列逐层扩展直到到达出口,记录每个格子的前驱节点。回溯前驱就能得到路径坐标序列,绘制时逐点填充即可。
路径表建议生成后存成成员变量,而不是每次重绘都重新计算。这样窗口覆盖再恢复也不会卡顿,配合双缓冲不会有任何性能压力。如果你还想更进一步,把BFS换成A*,启发式用曼哈顿距离,在迷宫规模变大后能明显减少扩展节点数。
6.3 我的一点改造心得
整套工程看完,你会发现它最值钱的地方不是迷宫算法本身,而是“界面层与算法层分离”的架构习惯。想扩展贴图时,只需要改3.2节的绘制函数,换成真彩位图背景;想扩展新玩法时,只需在MovePlayer入口加游戏规则逻辑。算法层稳定不动,界面层自由变化。
我拿到别人的MFC工程时,第一件事永远是先检查字符集配置、消息映射声明和OnPaint绘制结构这三处,确认它们不出问题,再谈别的功能。这套迷宫游戏我断断续续改过五六次,结构一直没崩,靠的也是坚持这个审视顺序。希望帮到你。
本文还有配套的精品资源,点击获取