简介:这是一份面向MFC与Windows程序设计初学者的迷宫游戏源码资源,基于微软MFC类库与C++开发,适合想通过小项目理解GUI编程、事件驱动模型与路径搜索算法的读者练手。压缩包共45个文件,约2.13MB,包含7个h头文件、6个cpp源文件及obj、pdb等编译中间文件,另有png、bmp、ico等图片与图标资源,以及rc、dsp、dsw等工程配置和ReadMe说明,结构完整可直接用VC6打开编译运行。资源中附带游戏思路说明与多张步骤截图,涵盖迷宫创建、自定义地图等环节,便于对照理解文档/视图架构、菜单控件、GDI绘图与DFS/BFS寻路逻辑。目前已有378人学习下载,适合作为MFC入门与游戏编程的参考案例。
1. 用 MFC 手搓一个迷宫游戏:从对话框到键盘响应的最小闭环
很多人第一次接触 Windows 桌面开发,都是从 MFC 开始的。但真到动手写点东西,往往卡在“对话框怎么画”“键盘消息怎么收”这些最基础的问题上。这个项目就是一个用 MFC 做的简单迷宫游戏,没有花哨的引擎,没有第三方依赖,打开 Visual Studio 新建一个 MFC 对话框工程就能跑。它适合两类人:一是刚学完 C++ 语法、想找个能看见界面的练手项目;二是需要快速验证某个交互逻辑、不想引入 Qt 或 Unity 的开发者。整个游戏的核心就三件事:用对话框当画布、用键盘消息驱动角色移动、用二维数组存迷宫数据。把这三点串起来,一个能跑能改的 MFC 小游戏就成型了。
2. 工程结构与消息映射:MFC 对话框程序的骨架
2.1 从解决方案到对话框资源
用 Visual Studio 新建“MFC 应用程序”,选择“基于对话框”的模板,工程名随意。生成后你会看到几个关键文件:MazeGameDlg.h/MazeGameDlg.cpp是主对话框类,MazeGame.rc是资源文件,resource.h里是控件 ID 的宏定义。主对话框默认带一个“确定”和一个“取消”按钮,这两个按钮在游戏里用不上,直接删掉,把对话框的标题改成“迷宫游戏”,客户区大小调到 600×600 左右,方便后面画格子。
对话框本身没有绘图能力,所有绘制都要在OnPaint里做。常见做法是给对话框类加一个OnPaint响应,然后在里面获取CPaintDC,用Rectangle和FillSolidRect画墙和路。迷宫数据用一个二维int数组存,0 表示通路,1 表示墙,2 表示终点。数组大小根据你想做的关卡来定,比如 15×15,每个格子 40 像素,正好 600×600。
// MazeGameDlg.h 中声明 private: int m_maze[15][15]; // 迷宫数据:0 通路,1 墙,2 终点 int m_playerX; // 玩家当前列 int m_playerY; // 玩家当前行 int m_cellSize; // 每个格子的像素大小 void InitMaze(); // 初始化迷宫数据 void DrawMaze(CDC* pDC); // 绘制迷宫这里m_cellSize单独抽出来,是为了后面改格子大小时不用满代码找数字。InitMaze里把数组填好,可以硬编码一个固定关卡,也可以从文件读。硬编码最简单,适合第一次跑通。
2.2 消息映射:让键盘控制角色
MFC 的键盘消息默认发给拥有焦点的控件。对话框上如果放了按钮,焦点可能在按钮上,方向键会被按钮吃掉。解决办法是在对话框的PreTranslateMessage里拦截WM_KEYDOWN,或者给对话框加OnKeyDown响应并确保对话框本身能获得焦点。更稳妥的做法是重写PreTranslateMessage,因为它在消息分发到控件之前就处理了。
BOOL CMazeGameDlg::PreTranslateMessage(MSG* pMsg) { if (pMsg->message == WM_KEYDOWN) { switch (pMsg->wParam) { case VK_UP: TryMove(0, -1); break; case VK_DOWN: TryMove(0, 1); break; case VK_LEFT: TryMove(-1, 0); break; case VK_RIGHT: TryMove(1, 0); break; } return TRUE; // 消息已处理,不再往下传 } return CDialogEx::PreTranslateMessage(pMsg); }TryMove是自定义函数,参数是行列的增量。它先算出目标位置,检查是否越界、是否是墙,如果都能通过就更新m_playerX/m_playerY,然后调用Invalidate触发重绘。这里有个细节:Invalidate默认重绘整个客户区,对于 15×15 的小迷宫完全够用,不用做局部刷新优化。
2.3 绘制逻辑:把二维数组变成画面
OnPaint里拿到CPaintDC后,先填充背景,再遍历二维数组。每个格子根据值选颜色:墙用深灰,通路用白色,终点用绿色,玩家位置用红色圆或方块覆盖。画玩家时要注意,玩家坐标和迷宫坐标是同一套,直接按m_playerX * m_cellSize算像素位置。
void CMazeGameDlg::DrawMaze(CDC* pDC) { for (int row = 0; row < 15; row++) { for (int col = 0; col < 15; col++) { CRect rect(col * m_cellSize, row * m_cellSize, (col + 1) * m_cellSize, (row + 1) * m_cellSize); if (m_maze[row][col] == 1) pDC->FillSolidRect(rect, RGB(80, 80, 80)); // 墙 else if (m_maze[row][col] == 2) pDC->FillSolidRect(rect, RGB(0, 180, 0)); // 终点 else pDC->FillSolidRect(rect, RGB(255, 255, 255)); // 通路 } } // 画玩家 CRect playerRect(m_playerX * m_cellSize + 5, m_playerY * m_cellSize + 5, (m_playerX + 1) * m_cellSize - 5, (m_playerY + 1) * m_cellSize - 5); pDC->FillSolidRect(playerRect, RGB(200, 0, 0)); }这段代码没有用双缓冲,快速移动时可能会闪。如果觉得闪,可以在OnPaint里先画到内存 DC,再BitBlt到屏幕。不过对于这个体量的游戏,闪一下也不影响玩,先跑通再优化。
3. 核心玩法实现:移动、碰撞与胜利判定
3.1 移动与碰撞检测
TryMove是整个游戏里唯一修改玩家位置的函数,所有边界检查和碰撞都集中在这里。参数dx/dy表示方向,比如按上键传(0, -1)。先算目标行列,再判断是否在 0~14 范围内,然后看m_maze对应位置是不是 1。只有两个条件都满足才更新坐标。
void CMazeGameDlg::TryMove(int dx, int dy) { int newX = m_playerX + dx; int newY = m_playerY + dy; // 边界检查 if (newX < 0 || newX >= 15 || newY < 0 || newY >= 15) return; // 撞墙检查 if (m_maze[newY][newX] == 1) return; // 更新位置 m_playerX = newX; m_playerY = newY; // 胜利判定 if (m_maze[newY][newX] == 2) { MessageBox(_T("你到达终点了!"), _T("恭喜"), MB_OK | MB_ICONINFORMATION); // 可以在这里重置关卡或进入下一关 } Invalidate(); // 触发重绘 }这里把胜利判定直接写在移动逻辑里,简单直接。如果要做多关卡,可以把m_maze换成三维数组或者从文件加载,胜利后切换关卡索引再Invalidate。
3.2 迷宫数据的初始化
InitMaze负责把二维数组填成设计好的关卡。最土的办法是手动写 15 行,每行 15 个数字。虽然看起来笨,但胜在直观,改起来也方便。比如下面这个简单的迷宫,外圈全是墙,内部留一条通路到终点。
void CMazeGameDlg::InitMaze() { int map[15][15] = { {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}, {1,0,0,0,1,0,0,0,0,0,0,0,0,0,1}, {1,0,1,0,1,0,1,1,1,1,1,1,1,0,1}, {1,0,1,0,0,0,1,0,0,0,0,0,1,0,1}, {1,0,1,1,1,1,1,0,1,1,1,0,1,0,1}, {1,0,0,0,0,0,0,0,1,0,0,0,1,0,1}, {1,1,1,1,1,1,1,0,1,0,1,1,1,0,1}, {1,0,0,0,0,0,1,0,1,0,0,0,0,0,1}, {1,0,1,1,1,0,1,0,1,1,1,1,1,0,1}, {1,0,1,0,0,0,1,0,0,0,0,0,1,0,1}, {1,0,1,0,1,1,1,1,1,1,1,0,1,0,1}, {1,0,0,0,1,0,0,0,0,0,1,0,0,0,1}, {1,1,1,0,1,0,1,1,1,0,1,1,1,0,1}, {1,0,0,0,0,0,1,0,0,0,0,0,0,2,1}, {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1} }; memcpy(m_maze, map, sizeof(map)); m_playerX = 1; m_playerY = 1; }memcpy把局部数组拷到成员变量里,注意sizeof(map)是 15×15×4 字节,和m_maze大小一致。玩家起点设在(1,1),终点在(13,13),中间用墙围出一条弯弯绕绕的路。这个关卡不算难,但足够验证移动、碰撞和胜利判定是否正常。
3.3 重绘与刷新时机
MFC 里触发重绘的方式有好几种:Invalidate标记整个客户区为无效,UpdateWindow立即重绘,RedrawWindow可以指定区域。这个项目里每次移动后调Invalidate就够了,系统会在消息队列空闲时发WM_PAINT。如果发现移动后画面没更新,检查是不是在OnPaint里没调用基类的OnPaint,或者Invalidate被其他逻辑挡住了。
注意:
Invalidate只是标记,不是立即重绘。如果在一个循环里连续调Invalidate,只有最后一次生效。需要立即看到效果时,后面跟一句UpdateWindow()。
4. 避坑与常见问题:MFC 对话框游戏的几个翻车点
4.1 方向键没反应,焦点被按钮抢走
现象:按方向键角色不动,但按回车会触发“确定”按钮。原因:对话框上的按钮默认拥有焦点,方向键被按钮用于切换焦点。解决:删掉不需要的按钮,或者在PreTranslateMessage里拦截WM_KEYDOWN并返回TRUE。如果必须保留按钮,可以在OnInitDialog里把焦点设到对话框本身。
4.2 画面闪烁严重
现象:快速按方向键时,整个客户区白一下再重绘。原因:OnPaint里直接画到屏幕 DC,背景擦除和前景绘制不同步。解决:用内存 DC 做双缓冲。在OnPaint里创建兼容 DC 和位图,先画到内存,再一次BitBlt到屏幕。代码量不大,但效果立竿见影。
4.3 迷宫数组越界导致崩溃
现象:角色走到边缘时程序直接退出。原因:TryMove里先更新了坐标再检查边界,或者检查条件写反了。解决:严格按“先算新坐标 → 检查边界 → 检查墙 → 再更新”的顺序写。边界检查用newX < 0 || newX >= 15,不要用<=。
4.4 修改格子大小后画面错位
现象:把m_cellSize从 40 改成 30,玩家方块和墙对不齐。原因:玩家绘制时用了固定的 5 像素内边距,格子变小后内边距比例失调。解决:内边距也按m_cellSize的比例算,比如m_cellSize / 8。或者干脆把玩家画成和格子等大的圆,用Ellipse画。
4.5 胜利后 MessageBox 弹窗导致重绘异常
现象:到达终点弹出提示框,关掉后画面花屏。原因:MessageBox会阻塞消息循环,期间WM_PAINT可能被丢弃。解决:在弹窗之前先Invalidate+UpdateWindow确保画面更新,或者把胜利提示改成在对话框上画一行文字,不用模态弹窗。
5. 二次开发:从固定关卡到可配置迷宫
这个项目最值得改的地方,是把硬编码的迷宫数组换成从文本文件加载。这样不用重新编译就能换关卡,也方便做关卡编辑器。具体做法:在InitMaze里用CStdioFile或ifstream读一个.txt文件,第一行是行列数,后面每行是迷宫数据。文件放在工程目录下,用相对路径读。
void CMazeGameDlg::LoadMazeFromFile(LPCTSTR path) { CStdioFile file; if (!file.Open(path, CFile::modeRead | CFile::typeText)) { MessageBox(_T("迷宫文件打开失败"), _T("错误"), MB_ICONERROR); return; } CString line; int row = 0; while (file.ReadString(line) && row < 15) { for (int col = 0; col < 15 && col < line.GetLength(); col++) { m_maze[row][col] = line[col] - _T('0'); // 字符转数字 } row++; } file.Close(); }文件格式可以长这样,每行 15 个字符,1是墙,0是路,2是终点:
111111111111111 100010000000001 101010111111101 101000100000101 101111101110101 100000101000101 111111101011101 100000101000001 101110101111101 101000100000101 101011111110101 100010000010001 111010111010101 100000100000201 111111111111111读进来之后,玩家起点可以固定设在(1,1),也可以约定文件里用3表示起点。这样改的好处是,你可以在记事本里画迷宫,存成 txt,程序一跑就是新关卡。再进一步,可以加一个“下一关”按钮,读不同文件;或者用CFileDialog让用户自己选文件。
另一个改造方向是加计时和步数统计。在对话框上放两个静态文本控件,OnInitDialog里初始化,每次TryMove成功后更新步数,用SetTimer每秒更新计时。这些都不需要改核心逻辑,只是在现有框架上挂功能。
我自己的习惯是,每次改完迷宫文件,先手动走一遍最短路,确认终点可达,再跑程序。因为 MFC 的调试不像控制台那么直观,一旦迷宫数据有误,角色可能被完全封死,而画面上看不出来。从那以后我每次换关卡都强制走一遍验证路径,省得在界面上瞎点。希望这个思路帮到你。
本文还有配套的精品资源,点击获取