ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

VC++ MFC迷宫游戏工程拆解:随机生成、键盘控制与BFS自动寻路

VC++ MFC迷宫游戏工程拆解:随机生成、键盘控制与BFS自动寻路 简介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 主流算法选型为什么我推荐递归回溯而不是随机 Primcreatemaze.h这个文件名暗示了它大概率用的是「创建迷宫」的老式思路。常见的迷宫生成算法有两类递归回溯Recursive Backtracker和随机 PrimRandomized Prims 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 还是 PreTranslateMessageMFC 迷宫游戏里红块移动不是用定时器自动走而是靠用户按键盘方向键。你会发现在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 MFCx86 和 x64」。这是老 VC 工程迁移到新 VS 的第一道坎也是最容易劝退的坑。5.2 字符集问题printf 调试信息乱码MessageBox 中文变问号现象在Maze32.cpp里写MessageBox(恭喜)编译通过运行时弹窗显示乱码或问号。或者用TRACE打印调试信息输出窗口一片乱。原因VC6 时代默认使用 ANSI 编码字符串字面量是 char*。新版 VS 的工程默认字符集是 UnicodeMessageBox被映射成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.hmVC6 的辅助文件删掉或改名的场景。解决先确认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 打开后另存为「UnicodeUTF-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::vectorPoint 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::queuePoint 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::vectorPoint 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是终点。返回的vectorPoint如果为空说明当前迷宫没有通路——但理论上完美的随机迷宫一定有通路所以空路径通常意味着你的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 寻路解决自动求解——这三块吃透了换任何图形库都能重写一遍。从那以后我每次拿到老工程都强制先读数据头和算法文件再碰界面代码这个习惯就是拆这类项目练出来的。希望这套拆解能帮你少走几段弯路。本文还有配套的精品资源点击获取
返回列表