新闻详情

新闻详情

首页 / 资讯中心 / 详情

MFC迷宫游戏开发实战:从GDI双缓冲到DFS算法,吃透C++桌面编程

发布时间:2026/9/29 8:25:18来源:尧图网络
MFC迷宫游戏开发实战:从GDI双缓冲到DFS算法,吃透C++桌面编程
简介一套基于MFC框架的简单迷宫游戏源码包面向正在学习C与Windows程序设计的初学者演示如何利用微软基础类库搭建图形界面并实现核心游戏逻辑。包内完整工程基于Visual C 6.0构建包含7个头文件与6个C源文件将窗口管理、文档/视图架构、菜单与控件消息映射、画笔与位图绘制等功能拆分为清晰模块另有迷宫地图素材png/bmp、可直接运行的exe以及dsp/dsw等工程配置压缩包体积约2.13MB。资源共45个文件附有ReadMe和“游戏的思路”说明文本适合对照工程理解二维数组迷宫建模、深度优先搜索与广度优先搜索的差别以及玩家移动、碰撞检测、重置关卡等交互逻辑。已有378人学习下载可作为课程设计或MFC入门练习的参考范例帮助读者掌握从界面搭建到算法落地的一整套编码方法。1. 一个 MFC 迷宫游戏其实是一张完美的 C 桌面开发试卷如果你跟我一样被C 只会写控制台黑框这个评价卡过那用 MFC 做一个迷宫游戏是性价比最高的翻盘手段。它看似是个小玩意实际上把 Windows 桌面开发的四根大梁一次压齐窗口消息循环怎么转、GDI 绘图什么时候会闪、对话框资源怎么挂、算法怎么跟界面解耦。我见过太多人学了半年 MFC 还在拖按钮而一个迷宫游戏能把消息映射、定时器、键盘响应、双缓冲绘图全串起来——这正是工业级 Windows 工具软件每天都在用的那套东西。这个项目适合两类人一是刚啃完《C Primer》想看看窗口程序长什么样的新手二是被扔去维护老旧 MFC 代码、急需补上 GDI 和消息机制短板的在职开发者。往下我会按框架 → 迷宫生成 → 绘制与交互 → 踩坑的顺序把这份代码能复现的东西一桩桩讲透。2. 项目骨架用对话框程序还是单文档程序决定了你要写多少代码MFC 里做游戏第一条岔路就是程序框架选型。2.1 为什么我选对话框程序而不是单文档视图架构单文档SDI架构自带文档/视图分离听起来很正规但对迷宫游戏是杀鸡用牛刀而且坑不少视图重绘走OnDraw但对话框程序的消息路由更直接按键响应、定时器刷新全在一个类里搞定心智负担小很多。实际做的时候我发现SDI 的CView重绘还要处理滚动条坐标换算、文档模板路径这些跟游戏毫无关系的复杂度而对话框程序只要往OnPaint里写绘制代码把OnEraseBkgnd返回 TRUE 防闪烁就行。新建项目时按 MFC 对话框应用向导走注意这几项设置设置项我的选择理由应用程序类型基于对话框单窗口、全屏绘制最简单使用 Unicode 库勾选现在还用 MBCS 是给自己找麻烦静态链接 MFC不勾调试期用共享 DLL发布再换静态生成的注释代码保留方便看InitInstance的初始化入口生成之后打开资源视图把对话框上的确定取消按钮删掉只留一个空白的IDD_MFC_GAME_DIALOG对话框。这个对话框就是游戏窗口本体接下来一切绘制都发生在它的客户区里。2.2. 迷宫地图的数据结构一维数组还是二维数组地图是整个游戏的数据库选型直接影响生成算法和碰撞检测的复杂度。我用的是vectorvectorint外层索引是行y内层是列x值有四种语义0 表示空地1 表示墙2 表示起点3 表示终点。// MazeMap.h #pragma once #include vector class CMazeMap { public: enum { WALL 1, ROAD 0, START 2, END 3 }; CMazeMap(int rows, int cols); bool Generate(); // 用 DFS 回溯生成迷宫 bool Solve(); // 用 BFS 求解最短路径 int GetCell(int row, int col) const; void SetCell(int row, int col, int val); int GetRows() const { return m_rows; } int GetCols() const { return m_cols; } private: std::vectorstd::vectorint m_data; int m_rows; int m_cols; };逻辑说明我用vectorvectorint而不用int[][]是因为行数和列数在游戏里是可配置的——用户可以在 UI 上选10×10还是20×20运行时动态分配用 STL 容器最顺手也避免了new int[m_rows][m_cols]这种内存连续性折磨。enum给魔法数字起了名字Generate和Solve把算法跟界面彻底隔开这意味着我可以先把迷宫逻辑写成一个控制台程序测试通过再塞进 MFC 里调试效率高得多。参数说明rows和cols我建议是奇数因为 DFS 生成迷宫的经典做法是把墙和路都按格子铺开窄巷宽度为 1行列必须是奇数才能保证边界是墙、内部道路连通。2.3 把地图画到窗口上的最小路径有了地图类下一步是让窗口显示它。MFC 对话框的重绘入口是OnPaint这是 GDI 绘制的唯一正确入口——任何想用GetDC在别处乱画的代码都是闪烁和残影的源头。我一般这样组织// CMazeGameDlg.cpp 的关键片段 void CMazeGameDlg::OnPaint() { CPaintDC dc(this); // 设备上下文构造时自动 BeginPaint CRect rcClient; GetClientRect(rcClient); // 计算单元格边长取宽高较小值除以列数/行数保证不越界 int cellW rcClient.Width() / m_map.GetCols(); int cellH rcClient.Height() / m_map.GetRows(); int cellSize min(cellW, cellH); // 地图整体居中偏移避免贴在窗口左上角不好看 int offsetX (rcClient.Width() - cellSize * m_map.GetCols()) / 2; int offsetY (rcClient.Height() - cellSize * m_map.GetRows()) / 2; for (int r 0; r m_map.GetRows(); r) { for (int c 0; c m_map.GetCols(); c) { CRect cellRect(offsetX c * cellSize, offsetY r * cellSize, offsetX (c 1) * cellSize, offsetY (r 1) * cellSize); COLORREF color GetCellColor(m_map.GetCell(r, c)); CBrush brush(color); dc.FillRect(cellRect, brush); brush.DeleteObject(); } } }逻辑说明这段代码先取窗口客户区尺寸再看地图行列数算出每格像素大小然后用min取较小值保证迷宫整体不超出窗口。之后按行按列填充色块墙用深灰色、路用白色、起点用绿色、终点用红色。FillRect是 GDI 画实心矩形最省事的函数没有边框颜色由CBrush决定。这里有三个新手最容易卡住的点。第一CPaintDC只能在OnPaint里用它的析构函数会调用EndPaint你在别处手动创建CClientDC画东西窗口一被遮挡就全花掉。第二brush.DeleteObject()必须写CBrush虽然封装了HBRUSH但 GDI 对象是系统资源不删会泄漏——MFC 的调试版会弹Dump提示。第三min的用法如果rcClient宽比高多很多直接拿Width() / cols算格子迷宫可能被竖直拉长变形取较小值后居中偏移视觉上最稳。3. 迷宫生成DFS 回溯算法与二维数组地图模型地图类只是壳子真正让迷宫可信的是生成算法。我选了 DFS 回溯递归回溯法它生成的迷宫特点是路径唯一、分支多、死胡同分布自然没有那种横平竖直的格子感。3.1 为什么不用随机拆墙而用 DFS常见的迷宫生成算法有四种Prim、Kruskal、递归回溯DFS、递归分割。我做过对比实验结论如下算法生成效果实现难度适合场景Prim分支多、纹理均匀中需要大量短分支的策略游戏Kruskal随机性强、无偏向中高需要完全不可预测的地图DFS 回溯路径长、死胡同深低迷宫游戏首选递归分割结构方正低看惯了斜路、想要房间感时DFS 的核心思路是从起点出发四个方向随机打乱每走一步就把当前位置标记为路然后在邻墙之间打通。回溯发生在四个方向都走不通时栈退回上一个分叉点。这样生成的迷宫必然连通不会出现孤岛。Prim 和 Kruskal 在网格图上会产生更多岔路对迷宫游戏来说往往显得过于细碎而 DFS 单路径特性让玩家始终有这条路能通到底的探索感跟游戏场景最贴合。3.2 DFS 的递归实现与栈溢出边界递归实现直观但 20×20 以上的地图就可能触碰调用栈深度上限所以我在工程里用了显式栈版本// MazeMap.cpp —— 显式栈 DFS 迷宫生成 bool CMazeMap::Generate() { // 初始化全部填墙 for (int r 0; r m_rows; r) for (int c 0; c m_cols; c) m_data[r][c] WALL; struct Step { int row, col; }; std::vectorStep stack; stack.push_back({ 1, 1 }); m_data[1][1] ROAD; const int dr[] { -2, 2, 0, 0 }; const int dc[] { 0, 0, -2, 2 }; while (!stack.empty()) { Step cur stack.back(); int r cur.row, c cur.col; // 收集可走的邻格隔一堵墙的位置 std::vectorint dirs; for (int i 0; i 4; i) { int nr r dr[i]; int nc c dc[i]; if (nr 0 nr m_rows - 1 nc 0 nc m_cols - 1 m_data[nr][nc] WALL) { dirs.push_back(i); } } if (!dirs.empty()) { int d dirs[rand() % dirs.size()]; int nr r dr[d]; int nc c dc[d]; // 打通当前格与目标格之间的墙 m_data[(r nr) / 2][(c nc) / 2] ROAD; m_data[nr][nc] ROAD; stack.push_back({ nr, nc }); } else { stack.pop_back(); // 四个方向都堵死了回溯 } } return true; }逻辑说明dr和dc数组里存放的是两步跳跃的位移——从 (1,1) 出发左右前后各跳两格中间夹的那格就是被打通的墙。dirs收集所有仍为墙的可跳邻格随机挑一个走走完压栈进入下一轮。当某个格子四周再无可跳墙时pop_back回溯到上一个分叉点。这个流程跑完迷宫一定是完美迷宫任意两格之间恰有一条路径。参数说明{1, 1}作为起点是因为边界必须留一圈墙nr 0 nr m_rows - 1确保不跳出墙边界。rand() % dirs.size()用的是朴素的 C 随机数够用如果追求每次跑图不一样记得在InitInstance里srand((unsigned)time(nullptr))不然每次都生成同一个迷宫玩家两分钟就腻了。3.3 迷宫求解BFS 最短路径与路径标记还原生成完迷宫下一步是可玩性核心——求解。我选了 BFS因为它天然给出最短路径而且实现简单。跟 DFS 不同BFS 一层层向外扩散第一次到达终点时记录的就是最短路。// MazeMap.cpp —— BFS 求解并把最短路径标成灰色 bool CMazeMap::Solve() { if (m_solved) return true; // 找到起终点 int sr 1, sc 1, er m_rows - 2, ec m_cols - 2; std::vectorstd::vectorint dist(m_rows, std::vectorint(m_cols, -1)); std::vectorstd::vectorstd::pairint,int prev(m_rows, std::vectorstd::pairint,int(m_cols, { -1, -1 })); std::queuestd::pairint,int q; q.push({ sr, sc }); dist[sr][sc] 0; const int dr[] { -1, 1, 0, 0 }; const int dc[] { 0, 0, -1, 1 }; while (!q.empty()) { auto cur q.front(); q.pop(); if (cur.first er cur.second ec) break; for (int i 0; i 4; i) { int nr cur.first dr[i]; int nc cur.second dc[i]; if (nr 0 nr m_rows nc 0 nc m_cols m_data[nr][nc] ROAD dist[nr][nc] -1) { dist[nr][nc] dist[cur.first][cur.second] 1; prev[nr][nc] cur; q.push({ nr, nc }); } } } // 回溯路径并标记 std::pairint,int p { er, ec }; while (p.first ! sr || p.second ! sc) { m_data[p.first][p.second] 4; // 自定义标记路径 p prev[p.first][p.second]; if (p.first -1) return false; // 无路可走理论上不会发生 } m_solved true; return true; }逻辑说明dist二维数组存每个格子到起点的步数-1表示未访问prev存每个格子的前驱坐标这是还原路径的关键——很多人会只存距离到最后发现路径回溯不出来。BFS 遍历时每访问一个格子就把它的前驱记下来找到终点后沿prev一路回退把路径上的格子标记为 4绘图时GetCellColor把 4 渲染成灰色玩家立刻能看到一条完整路线。参数说明dr/dc用的是单位位移而非两步跳跃因为这是在路组成的网格上走不需要隔墙打洞。dist[nr][nc] -1既判未访问也顺带做了去重——避免走回头路死循环。{ -1, -1 }这个哨兵值非常重要它既是初始化的默认前驱也是回溯时的终止条件如果起点或终点被墙包围prev链会断这时候必须拦一下不然m_data[p.first][p.second]会越界崩溃。4. 绘制与交互双缓冲、键盘移动与定时器计时地图类就位以后MFC 界面层的重头戏才开始。迷宫游戏最大的坑不在算法而在画面怎么不闪、按键怎么灵敏、计时怎么准。4.1 双缓冲原理与三种实现方案对比GDI 绘图闪屏的根源是OnEraseBkgnd和OnPaint交替执行——先擦白再画图两帧之间露出窗口底色人眼就看到了闪烁。方案原理闪烁程度代码量我的评价直接FillRect每帧清屏重画严重少不推荐眼睛受不了OnEraseBkgnd返回 TRUE跳过擦除步骤可能改善最少配合全屏重绘偶尔能蒙混过关内存 DC 双缓冲先在内存画好一次性BitBlt上屏基本消除中等正式项目必选我用的当然是第三种。核心是三步创建一个兼容内存 DC、把对话框客户区的CBitmap选入 DC、在内存 DC 上画完再BitBlt一次性拷贝到屏幕 DC。代码结构如下// CMazeGameDlg::OnPaint 的完整双缓冲版本 void CMazeGameDlg::OnPaint() { CPaintDC dc(this); CRect rcClient; GetClientRect(rcClient); // 1. 创建内存 DC 和位图 CDC memDC; memDC.CreateCompatibleDC(dc); CBitmap bmp; bmp.CreateCompatibleBitmap(dc, rcClient.Width(), rcClient.Height()); CBitmap* pOldBmp memDC.SelectObject(bmp); // 2. 灰色背景铺底防止位图透明区域露出杂色 memDC.FillSolidRect(rcClient, RGB(60, 60, 60)); // 3. 在内存 DC 上画迷宫画格子逻辑同前DC 对象换成 memDC DrawMap(memDC, rcClient); // 4. 一次性整体上屏 dc.BitBlt(0, 0, rcClient.Width(), rcClient.Height(), memDC, 0, 0, SRCCOPY); // 5. 清理 GDI 对象顺序不能乱 memDC.SelectObject(pOldBmp); bmp.DeleteObject(); }逻辑说明CreateCompatibleDC创建的 DC 默认是 1×1 的黑色位图必须用CreateCompatibleBitmap给它分配一块跟窗口客户区相同尺寸的画布。SelectObject把位图选入内存 DC此后所有FillRect/MoveTo/LineTo都画在这块内存画布上。最后BitBlt是纯内存到显存的一次位块传输速度极快屏幕上不会出现先擦后画的中间态。参数说明SRCCOPY是光栅操作码表示直接覆盖目标区域如果你用 XOR 之类的操作码颜色会叠加出怪效果这里不要乱换。pOldBmp保存了 DC 原来的位图对象清理时把它选回去然后DeleteObject——顺序反了会报对象被选中无法删除的调试断言。我这套写完之后实测 20×20 的迷宫窗口拉伸时不再闪烁CPU 占用也几乎为零。4.2 键盘移动方向键消息处理与碰撞检测MFC 对话框程序默认不会接收方向键消息焦点都跑到按钮控件上去了。处理方式是重写PreTranslateMessage在消息进TranslateMessage之前截获WM_KEYDOWN// CMazeGameDlg::PreTranslateMessage BOOL CMazeGameDlg::PreTranslateMessage(MSG* pMsg) { if (pMsg-message WM_KEYDOWN pMsg-hwnd m_hWnd) { CDialog::PreTranslateMessage(pMsg); // 先走基类避免消息丢失 int key (int)pMsg-wParam; int nr m_playerRow, nc m_playerCol; switch (key) { case VK_UP: nr--; break; case VK_DOWN: nr; break; case VK_LEFT: nc--; break; case VK_RIGHT: nc; break; case VK_SPACE: m_map.Solve(); Invalidate(FALSE); return TRUE; default: return CDialog::PreTranslateMessage(pMsg); } if (m_map.GetCell(nr, nc) ROAD || m_map.GetCell(nr, nc) END) { m_playerRow nr; m_playerCol nc; if (m_map.GetCell(nr, nc) END) { // 胜利把计时器停掉弹提示 KillTimer(TIMER_GAME); MessageBox(_T(通关用时 m_timeStr), _T(恭喜), MB_OK); } Invalidate(FALSE); // 重绘用双缓冲防止闪烁 } return TRUE; // 消息已处理不再往下传 } return CDialog::PreTranslateMessage(pMsg); }逻辑说明我在CMazeGameDlg里保存了玩家当前位置m_playerRow/m_playerCol按下方向键先生成目标坐标nr/nc查地图里那格是不是ROAD或END。墙不可走玩家的移动自然被网格卡住不需要额外的物理碰撞逻辑。走到END时停表弹窗表示通关。参数说明Invalidate(FALSE)的FALSE表示不擦除背景——擦除背景会触发OnEraseBkgnd从而闪屏。配合双缓冲这里不擦比擦效果好得多。return TRUE是这条消息被消费了不会继续传给默认窗口过程如果不吞掉方向键还可能触发对话框的默认按钮点击逻辑。4.3 计时器SetTimer 与时间字符串格式化迷宫加计时器用SetTimer是 MFC 里最直接的方案。我在OnInitDialog里启动每秒钟触发一次OnTimer把秒数格式化成分:秒显示在窗口标题栏玩家不看屏幕都能知道时间在走// CMazeGameDlg.cpp #define TIMER_GAME 1 void CMazeGameDlg::OnInitDialog() { CDialog::OnInitDialog(); m_elapsedSeconds 0; SetTimer(TIMER_GAME, 1000, nullptr); } void CMazeGameDlg::OnTimer(UINT_PTR nIDEvent) { if (nIDEvent TIMER_GAME) { m_elapsedSeconds; int minutes m_elapsedSeconds / 60; int seconds m_elapsedSeconds % 60; m_timeStr.Format(_T(%02d:%02d), minutes, seconds); SetWindowText(m_timeStr); } CDialog::OnTimer(nIDEvent); }逻辑说明SetTimer的第一个参数是定时器 ID第二个是间隔毫秒数——1000 就是一秒。OnTimer里用整除和取模算分秒Format是CString的格式化方法%02d保证输出两位不足补零。SetWindowText直接把时间写到窗口标题省掉一个静态文本控件。参数说明定时器精度在 Windows 上不是精确的——如果你要测速间隔 1000 毫秒可能实际跑出 997 或 1003 毫秒累积误差会在 10 分钟左右漂移 1 秒。真正的计时精度需求应该用GetTickCount64或QueryPerformanceCounter但这里只做竞速参考SetTimer完全够用。退出时记得在OnDestroy里KillTimer(TIMER_GAME)进程退出虽然会自动清理但养成习惯对管理多个定时器有好处。5. 避坑记录从内存泄漏到界面闪烁五条血泪经验这个项目最容易翻车的不是算法而是 MFC 的 GDI 对象管理和消息处理细节。我把实际调试中踩过的坑整理出来每条都标了现象、原因和解决照单排查能省一整晚。5.1 现象迷宫越玩越卡内存稳步上涨最后程序无响应原因分析OnPaint里我最初直接用CBrush画格子但刷子对象用完没DeleteObject。MFC 的CBrush析构时确实会释放 GDI 对象但如果你把CBrush创建在if块之外、手动管理生命周期或者频繁CreateSolidBrush而不删除GDI 对象表就会耗尽。这个不会报异常只会让系统绘图越来越慢最后CreateCompatibleBitmap失败返回 NULL。解决办法CBrush局部变量用栈对象作用域结束自动析构非要跨作用域就加DeleteObject。我还在OnPaint开头加了断言检查ASSERT(dc.GetSafeHdc() ! nullptr)一旦 GDI 泄漏这里会先崩问题定位快得多。5.2 现象窗口一拉伸迷宫就碎成一地残影原因分析这是我在第 2 章埋的伏笔——没有做双缓冲。OnEraseBkgnd默认用白色擦背景OnPaint再画迷宫两个操作交替执行人眼看到的不是一帧完整画面而是白底 格子的拼接拉伸时尤其明显。有段时间我怀疑是FillRect颜色不对查了半天才发现是擦除逻辑在捣乱。解决办法双缓冲 OnEraseBkgnd返回 TRUE 双管齐下。OnEraseBkgnd直接返回 TRUE 跳过擦除OnPaint里先铺深灰色背景再画所有格子屏幕只有一帧完整内容滚动条拖动时也纹丝不动。5.3 现象方向键没反应焦点不知跑到哪个控件上去了原因分析对话框里残留的确定取消按钮会抢焦点WM_KEYDOWN被按钮收到后直接丢弃。就算你删了按钮OnOK和OnCancel的默认处理也会让回车和 ESC 关掉对话框。解决办法PreTranslateMessage截获按键 重写OnCancel为空函数。方向键在对话框级别处理不依赖控件焦点OnCancel空实现让 ESC 退出失效防止玩家误按关窗口。这个坑特别隐蔽因为你 MessageBox 调试时焦点又变了一定要用pMsg-hwnd m_hWnd判断来源窗口。5.4 现象迷宫每次启动生成一模一样毫无随机性原因分析rand()没有播种默认种子是 1每次生成的随机序列完全相同。这是 C 标准库的经典陷阱——你写rand() % 4拿到的是伪随机序列而不是真随机。解决办法在CMazeGameDlg::OnInitDialog里加一行srand((unsigned)time(nullptr))用当前时间做种子。但这还不够——time的精度只有一秒玩家在 1 秒内重启游戏仍会得到相同地图。更稳的是用GetTickCount64()或随机设备std::random_device我后来改成srand((unsigned)GetTickCount64())才彻底解决。5.5 现象游戏通关时 MessageBox 一闪而过计时器还在走原因分析KillTimer是在MessageBox之后调用的但MessageBox内部会进入模态消息循环期间定时器消息继续派发OnTimer里还在m_elapsedSeconds。等你点掉弹窗时间已经多跑了好几秒排名的公平性直接毁了。解决办法先KillTimer再弹窗。更彻底的做法是加一个m_bGameOver标志OnTimer第一行就判断if (m_bGameOver) return;双保险防住时序问题。6. 把这套代码用活从固定地图到存档与难度曲线走到这一步迷宫游戏已经能玩能赢了。真正的进阶方向是让代码有产品感——能存档、能调难度、能复现问题。6.1 存档把地图序列化成文本文件方便调试与复现我最常干的一件事是把生成的地图存到文本文件里。迷宫算法的随机性让你没法预测哪一局会卡住地图存档是排查问题的后悔药void CMazeMap::SaveToFile(const char* filename) { FILE* fp nullptr; fopen_s(fp, filename, w); if (!fp) return; for (int r 0; r m_rows; r) { for (int c 0; c m_cols; c) { fprintf_s(fp, %d , m_data[r][c]); } fprintf_s(fp, \n); } fclose(fp); }逻辑说明用 C 标准库的fopen_s/fprintf_s输出纯文本每行一行地图空格分隔。这个格式可以直接被文本工具打开也能作为测试用例喂回程序。遇到哪把钥匙开了哪个门这种复杂逻辑出 bug存地图再手动构造输入比一遍遍跑随机快得多。参数说明fopen_s是fopen的安全版本要求传入指针地址并返回错误码VS 默认会警告你用不安全函数。写文件时注意编码纯 ASCII 没问题如果存了中文字符串就要考虑setlocale或_wfopen_s等宽字符版本了。6.2 难度曲线把行列数和定时器间隔做成可配置参数游戏不能万年 10×10。我给CMazeMap的构造函数加了行列参数Generate前先擦掉旧数据然后按新尺寸重新生成。这背后的一个设计原则是地图类不持有的状态一个都不要放——玩家坐标、计时都是对话框的事CMazeMap只管生成、读取、求解。void CMazeGameDlg::StartNewGame(int rows, int cols) { m_map CMazeMap(rows, cols); // 注意需要拷贝构造函数 m_map.Generate(); m_playerRow 1; m_playerCol 1; m_elapsedSeconds 0; m_timeStr _T(00:00); m_map.SetCell(1, 1, START); m_map.SetCell(rows - 2, cols - 2, END); SetTimer(TIMER_GAME, 1000, nullptr); Invalidate(FALSE); }我用一个CComboBox下拉框让玩家选难度简单是 11×11中等 15×15困难 21×21。SetTimer间隔也分三档困难模式每 0.8 秒跳一秒模拟压迫感。注意m_map CMazeMap(rows, cols)这行依赖CMazeMap有正确的拷贝赋值运算符——std::vector会自己深拷贝数据所以默认的拷贝构造就够了但如果加了CString、指针这类成员就必须自己实现深拷贝。6.3 验证方法与最后一条习惯写完这一切我建议你做一个我每次都会做的快速验证三步走步骤操作预期结果1打开程序选 11×11方向键走到终点标题栏计时停住弹窗弹出无闪烁2按空格触发求解灰色路径显示出来与手动走的路重合3连开 5 局每次按空格显示解法5 局地图各不相同路径解法都通过这三步能同时验证地图生成、绘制、计时、消息处理四个模块。我后来维护老项目时总会把同样的快速验证清单贴在代码注释里——从那以后我每次改完OnPaint或消息处理都强制自己走一遍这三步而不是凭感觉点两下就收工。这个习惯救过我至少三次一次是改了难度参数后地图没居中一次是OnEraseBkgnd返回值的顺序写反导致白屏还有一次是存档格式改动后旧档读不进来。自动化测试不一定覆盖 GDI 绘图和消息时序人工验证清单反而是桌面开发里最实用的兜底办法。希望这篇拆解能让你少走几段弯路。如果你也打算把这个迷宫游戏扩展出存档、换肤或者关卡编辑器从上面的代码往上搭就行——底层的地图类和双缓冲渲染已经是能用的状态剩下的就是往框架里填功能了。本文还有配套的精品资源点击获取
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

.NET 5.0 WinForms免注册调用大漠插件:SxS并行程序集实战 2026/9/29 9:18:22

.NET 5.0 WinForms免注册调用大漠插件:SxS并行程序集实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
DeepSeek-R1技术拆解:从API调用到本地部署的完整实践指南 2026/9/29 9:18:22

DeepSeek-R1技术拆解:从API调用到本地部署的完整实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
AI编程代理skills实战:从SKILL.md到Claude Code与Codex的安装管理 2026/9/29 9:18:22

AI编程代理skills实战:从SKILL.md到Claude Code与Codex的安装管理

说实话,我第一次认真研究 AI 编程代理里的skills,是因为一个特别没面子的场景:Claude Code 在同一个项目里连续三次把同样的 ESLint 配置改错,我气得差点把终端砸了。后来朋友甩了一个词过来:你没给它写 skill 吧&…

阅读更多 →
bup restore 完全指南:从备份集中精确提取文件与目录 2026/9/29 9:17:55

bup restore 完全指南:从备份集中精确提取文件与目录

灾备CLI存储 【免费下载链接】bup Very efficient backup system based on the git packfile format, providing fast incremental saves and global deduplication (among and within files, including virtual machine images). Please post problems or patches to the mail…

阅读更多 →
Apache Beam 测试基础设施:使用 Kustomize 在 Kubernetes 上安装 Strimzi Kafka Operator 2026/9/29 9:17:54

Apache Beam 测试基础设施:使用 Kustomize 在 Kubernetes 上安装 Strimzi Kafka Operator

【免费下载链接】beam Apache Beam is a unified programming model for Batch and Streaming data processing. 项目地址: https://gitcode.com/gh_mirrors/beam18/beam 点击查看 免费下载 导读 本文围绕 Apache Beam 仓库中 .test-infra/kafka/strimzi 目录下的…

阅读更多 →
Claude Code 配置管理模板:从零搭建高效开发环境 2026/9/29 9:17:40

Claude Code 配置管理模板:从零搭建高效开发环境

1. 为什么需要一套配置管理方案第一次接触 Claude Code 的人,大概率会经历这样一个过程:兴冲冲装好 CLI,敲了几个命令,发现确实能读代码、能改文件、能跑终端,然后开始琢磨怎么把它用得顺手一点。结果一搜资料&#xf…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉