简介:这份资源是面向C++初学者与VC++6.0入门者的迷宫小游戏完整工程源码,围绕经典MFC框架讲解如何从零搭建一款可玩的迷宫游戏。压缩包共65个文件,约153KB,包含16个cpp源文件、17个h头文件、17个bmp位图素材,以及ico图标、rc资源脚本、dsp与sln工程文件等,覆盖迷宫生成、角色移动、界面绘制与工程配置各环节。项目采用二维数组表示迷宫,结合深度优先搜索等算法生成唯一出入口,界面左侧小地图提供全局视野,右侧大地图展示详细路径,并通过方向键控制兔子角色移动、检测碰撞防止穿墙。代码按迷宫类与游戏窗口类拆分,借助MFC消息映射处理键盘事件,结构清晰便于对照学习。目前已有213人学习下载,适合想通过小项目掌握C++基础、MFC应用与图形界面事件处理的读者参考实践。
1. 从一份 migong.rar 说起:VC++6.0 编小游戏到底还能不能跑
翻出一份名为migong.rar的压缩包,解压后是几个.dsw、.dsp和一堆.cpp、.h文件,双击.dsw用 VC++6.0 打开,按 F7 编译,运气好直接生成一个走迷宫的小窗口程序——这就是很多人在找的「vc++6.0编写游戏」资源最典型的形态。它不是什么引擎工程,也不是 Unity 那种拖拽式开发,而是用 Win32 API 加 GDI 画图,在一个WinMain里手写消息循环,把迷宫地图画成一个个矩形格子,用键盘方向键控制一个小方块移动。适合谁?适合想搞明白「游戏循环到底长什么样」的 C++ 初学者,适合被 vc6.0 编小作业卡住、需要一份能编译能跑的参考代码的人,也适合想回头看看二十年前 Windows 桌面小游戏怎么写的老手。它解决的不是「做个商业游戏」,而是「用最原始的工具把游戏跑起来」这件事。
2. 拆开 migong.rar:VC++6.0 工程结构与 GDI 绘图原理
2.1 工程文件到底哪个能双击
解压之后你会看到一堆后缀,很多人第一次就卡在这里,不知道点哪个。VC++6.0 的工程体系里,真正能双击打开的是.dsw(Developer Studio Workspace),它管着整个工作区;.dsp(Developer Studio Project)是具体项目文件,记录源文件列表和编译选项;.opt是本地配置,换台机器就失效,可以删。常见做法是直接双击.dsw,如果报「无法打开」,就打开 VC++6.0,File → Open Workspace,手动选.dsw。如果连.dsw都没有,只有.cpp,那就 File → New → Projects → Win32 Application,建一个空工程,再把.cpp和.h用 Add to Project → Files 加进去。
| 文件后缀 | 作用 | 能不能删 |
|---|---|---|
| .dsw | 工作区,双击入口 | 不能 |
| .dsp | 项目配置,源文件清单 | 不能 |
| .opt | 本地个性化设置 | 可以 |
| .cpp / .h | 源码与头文件 | 不能 |
| .rc | 资源脚本,图标菜单 | 视情况 |
| Debug / Release | 编译输出目录 | 可以清 |
2.2 迷宫地图怎么用二维数组存
这类小游戏的地图几乎都是硬编码的二维数组,1表示墙,0表示通路,再留一个起点和终点。下面这段是常见写法,我按能直接编译的方式整理过:
// 迷宫地图:1 为墙,0 为通路,2 为终点 #define MAP_W 15 #define MAP_H 15 int g_map[MAP_H][MAP_W] = { {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,0,1,0,1}, {1,0,1,0,0,0,1,0,0,0,1,0,1,0,1}, {1,0,1,1,1,1,1,0,1,0,1,0,1,0,1}, {1,0,0,0,0,0,0,0,1,0,0,0,1,0,1}, {1,1,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,0,1,1,1,0,1,1,1,0,1,1,1,0,1}, {1,0,0,0,0,0,1,0,0,0,0,0,1,0,1}, {1,1,1,1,1,0,1,0,1,1,1,0,1,0,1}, {1,0,0,0,1,0,0,0,1,0,0,0,1,0,1}, {1,0,1,0,1,1,1,1,1,0,1,1,1,0,1}, {1,0,1,0,0,0,0,0,0,0,1,0,0,0,2}, {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1} };MAP_W和MAP_H控制地图尺寸,改大改小都要同步改数组内容,否则越界。2是终点标记,判断胜利时遍历或直接记录终点坐标都行。参数上唯一要注意的是数组维度顺序,g_map[y][x]里第一个下标是行(y),第二个是列(x),画图时别写反,写反了地图会转 90 度,这是血泪经验。
2.3 GDI 画格子与双缓冲
VC++6.0 编小游戏绕不开 GDI。最朴素的画法是在WM_PAINT里对每个格子调Rectangle,但这样每按一次键就闪一次,体验很差。常见做法是加双缓冲:先在内存 DC 上画好整张图,再一次性BitBlt到窗口 DC。
// 双缓冲绘制,避免闪烁 void DrawGame(HWND hwnd) { HDC hdc = GetDC(hwnd); HDC memDC = CreateCompatibleDC(hdc); HBITMAP memBmp = CreateCompatibleBitmap(hdc, MAP_W * CELL, MAP_H * CELL); HBITMAP oldBmp = (HBITMAP)SelectObject(memDC, memBmp); for (int y = 0; y < MAP_H; y++) { for (int x = 0; x < MAP_W; x++) { HBRUSH brush; if (g_map[y][x] == 1) brush = CreateSolidBrush(RGB(60, 60, 60)); // 墙 else if (g_map[y][x] == 2) brush = CreateSolidBrush(RGB(255, 200, 0)); // 终点 else brush = CreateSolidBrush(RGB(240, 240, 240));// 通路 RECT r = { x * CELL, y * CELL, (x + 1) * CELL, (y + 1) * CELL }; FillRect(memDC, &r, brush); DeleteObject(brush); } } // 画玩家 HBRUSH pb = CreateSolidBrush(RGB(0, 120, 255)); RECT pr = { g_playerX * CELL, g_playerY * CELL, (g_playerX + 1) * CELL, (g_playerY + 1) * CELL }; FillRect(memDC, &pr, pb); DeleteObject(pb); BitBlt(hdc, 0, 0, MAP_W * CELL, MAP_H * CELL, memDC, 0, 0, SRCCOPY); SelectObject(memDC, oldBmp); DeleteObject(memBmp); DeleteDC(memDC); ReleaseDC(hwnd, hdc); }CELL是每个格子的像素边长,一般取 20 到 30。CreateSolidBrush每次循环都创建再DeleteObject,这是 GDI 对象泄漏的高发点,漏删几次程序就画不出来了。BitBlt的最后一个参数SRCCOPY表示直接拷贝,别改成SRCINVERT,否则颜色会反。双缓冲的核心价值是把 N 次屏幕刷新压成 1 次,闪烁问题基本消失。
2.4 键盘消息与游戏循环
VC++6.0 的 Win32 程序没有现代引擎那种while(true)游戏循环,它靠WM_KEYDOWN消息驱动。在WndProc里处理方向键,改玩家坐标,判断目标格是不是墙,不是墙就更新并InvalidateRect触发重绘。
case WM_KEYDOWN: switch (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; } InvalidateRect(hwnd, NULL, FALSE); // FALSE 表示不擦背景,配合双缓冲 break; void TryMove(int dx, int dy) { int nx = g_playerX + dx; int ny = g_playerY + dy; if (nx < 0 || nx >= MAP_W || ny < 0 || ny >= MAP_H) return; // 边界 if (g_map[ny][nx] == 1) return; // 撞墙 g_playerX = nx; g_playerY = ny; if (g_map[ny][nx] == 2) MessageBox(NULL, "到达终点", "恭喜", MB_OK); }InvalidateRect第三个参数传FALSE很关键,传TRUE会先擦背景再重绘,双缓冲就白做了。TryMove里先判边界再判墙,顺序不能反,反了会数组越界。这套消息驱动的结构就是「vc6.0 编小」最核心的骨架,理解它比抄代码重要。
3. 从零编译到跑起来:VC++6.0 环境配置与常见报错处理
3.1 新建工程与导入源码
如果你拿到的只有散装.cpp,按这个流程走。打开 VC++6.0,File → New → Projects 选项卡,选 Win32 Application,填工程名,Location 选一个纯英文路径,点 OK,弹窗选「An empty project」。然后 File → New → Files 选 C++ Source File,命名后 Add to Project 勾上,把源码粘进去。或者 Project → Add To Project → Files,把已有的.cpp、.h加进来。工程建好后,Project → Settings → Link 里确认没有多余的库依赖,Win32 小游戏一般只用user32.lib、gdi32.lib,这两个默认就链。
3.2 编译报错逐条对照
VC++6.0 的报错信息很老,但翻来覆去就那几类。下面这张表是我这些年攒下来的对照,遇到直接查。
| 报错信息 | 真实原因 | 处理方式 |
|---|---|---|
| cannot open file "xxx.h" | 头文件路径不对或没加进工程 | 检查文件是否在工程目录,Project → Settings → C/C++ → Preprocessor 加路径 |
| unresolved external symbol _WinMain@16 | 建成了 Console 工程 | 重建为 Win32 Application,或改 Link 里的入口 |
| 'for' loop initial declaration used outside C99 mode | 在 .c 文件里写for(int i=...) | 改后缀为 .cpp,或把变量声明提到循环外 |
| fatal error C1083: Cannot open precompiled header | 没生成 stdafx 或设置错 | Project → Settings → C/C++ → Precompiled Headers 选 Not using |
| LNK2001: unresolved external symbol __imp__xxx | 缺库 | Project → Settings → Link → Object/library modules 补库名 |
unresolved external symbol _WinMain@16这个坑最经典,本质是工程类型选错,编译器在找控制台入口main,但你的代码写的是WinMain。改工程类型比重写代码快。
3.3 在虚拟机里跑老工具
VC++6.0 在 Win10/Win11 上能装,但兼容性玄学,常见问题是装完打不开、一点「打开」就闪退、编译时提示no compile tool。稳妥做法是丢进虚拟机,装个 Windows XP 或 Windows 7 的 32 位系统,VC++6.0 在里面跑得最稳。虚拟机分配 1GB 内存、20GB 磁盘就够,装完记得装 VMware Tools 或 VirtualBox 增强功能,不然分辨率很难看。源码通过共享文件夹传进去,编译产物再传出来。这条路虽然绕,但省去了在新系统上跟兼容性死磕的时间。
3.4 编译产物与运行验证
编译成功后,Debug 或 Release 目录下会生成.exe。直接双击能跑,但注意它依赖的运行时库。VC++6.0 默认动态链接msvcrt.dll,这个库在 XP 之后系统里都有,一般不用额外带。如果换到别的机器报缺 DLL,就在 Project → Settings → C/C++ → Code Generation 里把 Use run-time library 改成 Multithreaded(静态),重新编译,体积会大一点但独立。验证方法很简单:跑起来,按方向键,方块能动、撞墙不动、到终点弹框,三条都满足就算通了。
4. 避坑与排查:VC++6.0 编小游戏最容易翻车的五个点
4.1 现象:程序一运行窗口就闪退
原因:WinMain里消息循环写错,或者WndProc里WM_DESTROY没调PostQuitMessage,消息循环拿不到退出消息直接崩。解决:检查消息循环是不是标准写法,GetMessage返回 0 才退出,WM_DESTROY里必须有PostQuitMessage(0)。
MSG msg; while (GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return msg.wParam;4.2 现象:画面疯狂闪烁,按键一次闪一次
原因:WM_PAINT里直接对窗口 DC 画图,且InvalidateRect传了TRUE。解决:上双缓冲,InvalidateRect第三参数传FALSE,背景刷在WM_ERASEBKGND里返回 1 屏蔽掉。
4.3 现象:编译通过但一按键就崩
原因:TryMove里数组下标越界,或者玩家坐标初始值就是墙。解决:初始坐标设成通路格,TryMove先判边界再判墙,顺序不能反。加个ASSERT在 Debug 下能提前暴露。
4.4 现象:GDI 对象泄漏,画着画着不显示了
原因:CreateSolidBrush、CreatePen这类创建的对象没DeleteObject。Windows 每个进程 GDI 对象有上限,默认一万个,超了就画不出来。解决:谁创建谁删除,循环里创建的尤其要盯紧,能提到循环外复用的就别在循环里建。
4.5 现象:换台电脑就报缺 msvcrtd.dll
原因:Debug 版依赖调试运行时库,那东西不是每台机器都有。解决:发布用 Release 版,或者把运行时库改成静态链接。Debug 版只在自己机器上调试用,别拿去分发。
5. 进阶技巧:把 migong 改成能玩的完整小游戏
5.1 加计时与步数统计
光能走迷宫太素,加个计时和步数就有游戏感了。用SetTimer每秒发一次WM_TIMER,在消息里累加秒数,步数在TryMove成功时自增,画在窗口标题或状态栏上。
// WM_CREATE 里启动定时器,间隔 1000 毫秒 SetTimer(hwnd, 1, 1000, NULL); // WndProc 里处理 case WM_TIMER: g_seconds++; UpdateStatus(hwnd); // 自己实现,把秒数和步数拼成字符串 SetWindowText break; // TryMove 成功移动后 g_steps++;SetTimer的第三个参数是毫秒,1000 就是 1 秒。WM_TIMER不精确,但对小游戏够用。UpdateStatus里用wsprintf拼字符串,注意缓冲区开够,char buf[64]起步。
5.2 用 BFS 自动寻路验证地图
想确认地图是不是有解,或者想加个「提示」按钮,BFS 最直接。从起点开始一层层扩展,记录每个格子的前驱,找到终点后回溯路径。
// BFS 求最短路径,返回步数,-1 表示无解 int BfsSolve(int sx, int sy, int ex, int ey) { int dist[MAP_H][MAP_W]; memset(dist, -1, sizeof(dist)); int qx[MAP_W * MAP_H], qy[MAP_W * MAP_H], head = 0, tail = 0; qx[tail] = sx; qy[tail] = sy; tail++; dist[sy][sx] = 0; int dx[4] = {0, 0, -1, 1}, dy[4] = {-1, 1, 0, 0}; while (head < tail) { int cx = qx[head], cy = qy[head]; head++; if (cx == ex && cy == ey) return dist[cy][cx]; for (int i = 0; i < 4; i++) { int nx = cx + dx[i], ny = cy + dy[i]; if (nx < 0 || nx >= MAP_W || ny < 0 || ny >= MAP_H) continue; if (g_map[ny][nx] == 1 || dist[ny][nx] != -1) continue; dist[ny][nx] = dist[cy][cx] + 1; qx[tail] = nx; qy[tail] = ny; tail++; } } return -1; }队列用数组模拟,head和tail两个指针,tail不会超过格子总数。dist初始化为 -1 兼作访问标记,省一个visited数组。返回 -1 说明地图设计有问题,终点被墙围死了,这时候就得回去改地图数组。
5.3 地图数据外置成文件
硬编码数组改起来烦,把地图存成文本文件,程序启动时读进来,改地图就不用重编译。格式很简单,第一行宽高,后面每行一串 0/1/2。
// map.txt 格式:第一行 "15 15",之后 15 行每行 15 个字符 bool LoadMap(const char* path) { FILE* fp = fopen(path, "r"); if (!fp) return false; int w, h; fscanf(fp, "%d %d", &w, &h); if (w != MAP_W || h != MAP_H) { fclose(fp); return false; } for (int y = 0; y < h; y++) { char line[64]; fscanf(fp, "%s", line); for (int x = 0; x < w; x++) { g_map[y][x] = line[x] - '0'; if (g_map[y][x] == 2) { g_exitX = x; g_exitY = y; } } } fclose(fp); return true; }fscanf读%s会跳过空白,正好按行读字符串。line[x] - '0'把字符转数字,这是 C 里最省事的转换。读进来后顺手记录终点坐标,省得每次遍历。文件路径用相对路径,.exe和map.txt放同一目录,不然找不到。
5.4 一个我自己的习惯
早些年我改这类小游戏,总想着一次加一堆功能,结果编译报错几十条,根本不知道哪条引起的。后来养成习惯:每加一个功能就编译一次,跑一次,确认没问题再加下一个。地图外置、BFS 寻路、计时统计,这三样我都是分开验证的,任何一步编译不过就立刻回退,绝不带着错误往下写。从那以后我每次动 VC++6.0 的老工程,都强制走一遍「改一点、编一次、跑一次」的循环,省下来的排查时间比多敲几行代码值多了。希望帮到你。
本文还有配套的精品资源,点击获取