ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Visual C++与GDI复刻横版过关游戏:毕业设计源码实战与避坑指南

Visual C++与GDI复刻横版过关游戏:毕业设计源码实战与避坑指南 简介这份资源是面向高校计算机相关专业学生与C初学者的毕业设计、项目实训素材提供一套基于Visual C与GDI技术实现的横版过关游戏完整源代码玩法仿照经典超级玛丽。项目重点在于运用C面向对象特性构建游戏逻辑并借助GDI的位图绘制、定时器动画、键盘事件响应等机制实现角色移动、跳跃、碰撞检测与关卡切换地图按关卡逐次从文本文件加载而非拼接生成。压缩包共32个文件约161KB包含6个cpp源文件与9个h头文件承载核心逻辑6个bmp位图与2个ico图标提供场景和角色素材另有dsw、dsp工程文件及txt地图数据可直接用VC6打开编译运行。目前已有458人学习下载。读者可借此理解GDI图形编程原理、掌握游戏循环与地图管理思路并积累将理论知识落地为可运行项目的完整经验为后续游戏开发学习打下基础。1. 用 Visual C 和 GDI 复刻横版过关游戏这套毕业设计源码到底能跑出什么很多同学做毕业设计时第一反应是找个仿超级玛丽的横版过关游戏源码用 Visual C 加 GDI 画出来编译能跑、答辩能演示就算交差。但真正动手才发现GDI 这套老 API 和现代游戏引擎的思路完全不一样——没有场景树、没有精灵组件、没有物理引擎所有东西都得自己从零搭。这套方案的核心价值在于它逼你把游戏循环、碰撞检测、地图滚动、帧动画这些底层机制全部手写一遍而不是调几个引擎接口就完事。适合两类人一是毕业设计选题偏 Windows 桌面编程、需要展示 C 基本功的本科生二是想理解 2D 游戏底层渲染原理、不想被 Unity 黑盒遮住视线的开发者。读完你能拿到一条从窗口创建到角色跑跳的完整落地路径以及几个我当年翻过车的坑。2. GDI 横版过关游戏的四层骨架窗口、循环、渲染、输入2.1 为什么选 GDI 而不是 Direct2D 或 OpenGL毕业设计场景下GDI 的优势不是性能而是零依赖。Direct2D 需要额外链接 d2d1.libOpenGL 要配 GLUT 或 GLEW而 GDI 是 Windows 自带Visual C 6.0 到 Visual Studio 2022 都能直接编译。对于答辩环境不可控的情况——比如机房电脑没装显卡驱动、或者老师要求现场用 VC6 打开工程——GDI 是唯一不会翻车的选择。代价也很明显GDI 的 BitBlt 在 60 FPS 下刷全屏会撕裂必须用双缓冲。另外 GDI 不支持 Alpha 混合做半透明特效只能靠手动逐像素处理或预乘颜色。所以这套方案的定位很清晰2D 横版、像素风、分辨率不超过 800×600完全够用。2.2 游戏主循环PeekMessage 替代 GetMessageWin32 消息循环的默认写法是while(GetMessage(...))但游戏不能用它——GetMessage 在没消息时会阻塞游戏就卡住了。正确做法是用 PeekMessage 非阻塞轮询// 游戏主循环非阻塞消息处理 固定时间步 MSG msg; while (g_bRunning) { if (PeekMessage(msg, NULL, 0, 0, PM_REMOVE)) { if (msg.message WM_QUIT) break; TranslateMessage(msg); DispatchMessage(msg); } else { // 没有消息时执行游戏逻辑 DWORD dwNow GetTickCount(); float dt (dwNow - g_dwLastTick) / 1000.0f; if (dt 0.05f) dt 0.05f; // 防止切窗口后跳帧 g_dwLastTick dwNow; UpdateGame(dt); // 更新角色、敌人、碰撞 RenderGame(hdcMem); // 渲染到内存 DC BitBlt(hdcScreen, 0, 0, WND_W, WND_H, hdcMem, 0, 0, SRCCOPY); } }这里的关键参数是dt上限 0.05 秒。如果不加这个钳制玩家拖动窗口或切换程序再回来时GetTickCount 的差值可能达到几秒角色会瞬移穿过墙壁。我当年调试时角色莫名其妙掉出地图查了半天才发现是这个原因。2.3 双缓冲消除闪烁的唯一手段直接在窗口 DC 上画精灵每帧先擦背景再画角色人眼会看到明显闪烁。解决办法是创建内存 DC 和兼容位图// 初始化双缓冲在 WM_CREATE 中调用 HDC hdcScreen GetDC(hwnd); hdcMem CreateCompatibleDC(hdcScreen); hbmMem CreateCompatibleBitmap(hdcScreen, WND_W, WND_H); SelectObject(hdcMem, hbmMem); ReleaseDC(hwnd, hdcScreen); // 每帧渲染流程 FillRect(hdcMem, rcClient, (HBRUSH)GetStockObject(BLACK_BRUSH)); DrawBackground(hdcMem, g_CameraX); // 视差背景 DrawTiles(hdcMem, g_CameraX); // 地图瓦片 DrawPlayer(hdcMem); // 角色 DrawEnemies(hdcMem); // 敌人 // 最后一次性 BitBlt 到屏幕注意hbmMem必须在窗口销毁时 DeleteObject否则每次重开窗口都会泄漏一块显存。这个坑在答辩演示时特别致命——连续开关几次窗口后程序直接崩溃。2.4 输入处理GetAsyncKeyState 还是 WM_KEYDOWNWM_KEYDOWN 的问题是它依赖消息队列如果主循环里 PeekMessage 处理不及时快速连按会丢键。横版过关游戏对跳跃手感要求高我一般直接用 GetAsyncKeyState 轮询// 每帧在 UpdateGame 开头调用 bool left (GetAsyncKeyState(VK_LEFT) 0x8000) ! 0; bool right (GetAsyncKeyState(VK_RIGHT) 0x8000) ! 0; bool jump (GetAsyncKeyState(VK_SPACE) 0x8000) ! 0; if (left) g_Player.vx -MOVE_SPEED; if (right) g_Player.vx MOVE_SPEED; if (!left !right) g_Player.vx 0; // 跳跃只在落地时触发防止空中连跳 if (jump g_Player.bOnGround) { g_Player.vy -JUMP_FORCE; g_Player.bOnGround false; }0x8000这个掩码表示按键当前处于按下状态不能省略。MOVE_SPEED 建议设 35 像素/帧JUMP_FORCE 设 1014具体数值要配合重力加速度调。3. 从零搭建可运行工程VC6 到 VS2022 的编译配置3.1 创建 Win32 应用程序工程打开 Visual Studio新建项目选Windows 桌面应用程序VS2019/2022或Win32 ApplicationVC6。不要选控制台程序否则没有 WinMain 入口。工程建好后删掉自动生成的 About 对话框相关代码只保留 WinMain 和 WndProc。如果你用的是 VS2022还需要在项目属性里把字符集改成使用多字节字符集因为 GDI 的 TextOutA 等函数在 Unicode 下需要额外转换。这个设置在新版 VS 里藏得比较深项目属性 → 配置属性 → 高级 → 字符集。3.2 资源文件位图与精灵表的加载横版过关游戏的素材通常是一张精灵表Sprite Sheet所有角色帧画在同一张 BMP 里。加载用 LoadImage// 加载精灵表 HBITMAP hbmPlayer (HBITMAP)LoadImage( GetModuleHandle(NULL), _T(res\\player.bmp), IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE | LR_CREATEDIBSECTION ); if (!hbmPlayer) { MessageBox(hwnd, _T(player.bmp 加载失败), _T(错误), MB_OK); return -1; }LR_CREATEDIBSECTION必须加否则 GetObject 拿不到位图的像素数据后面做透明色处理会失败。资源路径建议用相对路径res\\答辩时把 exe 和 res 文件夹一起拷贝就不会丢素材。3.3 透明色处理GDI 没有 Alpha只能手动抠GDI 的 BitBlt 不支持透明通道精灵图的背景色会直接盖住地图。常见做法是先把背景色设为纯色比如 RGB(255,0,255) 品红然后用 TransparentBlt// 绘制角色帧品红色作为透明色 TransparentBlt( hdcMem, destX, destY, frameW, frameH, hdcSprite, // 精灵表 DC frameIndex * frameW, 0, frameW, frameH, RGB(255, 0, 255) // 透明色 );TransparentBlt 需要链接 msimg32.lib在代码里加#pragma comment(lib, msimg32.lib)。注意这个函数在 VC6 里可能没有声明需要手动包含wingdi.h并确认_WIN32_WINNT 0x0500。3.4 地图瓦片用二维数组存关卡数据横版过关游戏的地图用瓦片Tile拼最省事。定义一个二维数组每个元素代表一种地形// 0空 1地面 2砖块 3问号块 4管道 const int MAP_W 200; // 关卡总宽度瓦片数 const int MAP_H 15; // 屏幕高度瓦片数 int g_Map[MAP_H][MAP_W]; // 初始化示例前 20 列铺地面 void InitMap() { memset(g_Map, 0, sizeof(g_Map)); for (int x 0; x MAP_W; x) { g_Map[13][x] 1; // 第 13 行是地面 g_Map[14][x] 1; // 第 14 行也是地面 } // 放几个砖块 g_Map[9][10] 2; g_Map[9][11] 3; g_Map[9][12] 2; }瓦片大小建议 32×32 像素屏幕 800×600 正好显示 25×18 个瓦片。MAP_W 设 200 意味着关卡宽度 6400 像素足够做一段完整的横版关卡。3.5 摄像机跟随让地图跟着角色滚动摄像机就是一个偏移量 g_CameraX渲染时所有瓦片和精灵的 X 坐标都减去它// 摄像机跟随角色但限制在地图范围内 void UpdateCamera() { int targetX g_Player.x - WND_W / 2; if (targetX 0) targetX 0; int maxX MAP_W * TILE_SIZE - WND_W; if (targetX maxX) targetX maxX; g_CameraX targetX; } // 渲染瓦片时只画屏幕可见范围 void DrawTiles(HDC hdc) { int startCol g_CameraX / TILE_SIZE; int endCol startCol WND_W / TILE_SIZE 1; for (int row 0; row MAP_H; row) { for (int col startCol; col endCol; col) { if (col 0 || col MAP_W) continue; if (g_Map[row][col] 0) continue; int sx col * TILE_SIZE - g_CameraX; int sy row * TILE_SIZE; // 从瓦片集位图中取对应瓦片绘制 BitBlt(hdc, sx, sy, TILE_SIZE, TILE_SIZE, hdcTiles, g_Map[row][col] * TILE_SIZE, 0, SRCCOPY); } } }只画可见范围的瓦片是性能关键。如果每帧遍历 200×15 全部瓦片在 VC6 编译的 Debug 版本下会明显掉帧。4. 角色物理与碰撞检测让马里奥跳得跟原版一样4.1 重力与跳跃的数值调参横版过关游戏的手感 90% 取决于重力和跳跃参数。我试过很多组数值下面这组最接近原版超级玛丽的手感参数建议值说明GRAVITY0.5 像素/帧²每帧 vy 增加 0.5JUMP_FORCE-11 像素/帧起跳瞬间的垂直速度MOVE_SPEED3.5 像素/帧水平移动速度MAX_FALL_SPEED12 像素/帧下落速度上限防止穿墙FRICTION0.8松开方向键后的减速系数跳跃高度由 JUMP_FORCE 和 GRAVITY 共同决定最大高度 ≈ JUMP_FORCE² / (2×GRAVITY) ≈ 121 / 1 ≈ 121 像素大约 3.8 个瓦片高度刚好能跳上问号块。4.2 AABB 碰撞先分轴再检测角色和瓦片的碰撞用 AABB轴对齐包围盒。关键技巧是 X 轴和 Y 轴分开处理否则斜向移动时会卡住// 先处理水平移动 g_Player.x g_Player.vx; if (CheckTileCollision(g_Player.rect)) { // 水平方向撞墙回退 if (g_Player.vx 0) g_Player.x GetTileLeftBound() - g_Player.w; else g_Player.x GetTileRightBound(); g_Player.vx 0; } // 再处理垂直移动 g_Player.y g_Player.vy; g_Player.bOnGround false; if (CheckTileCollision(g_Player.rect)) { if (g_Player.vy 0) { // 下落撞地 g_Player.y GetTileTopBound() - g_Player.h; g_Player.bOnGround true; } else { // 上升撞头 g_Player.y GetTileBottomBound(); } g_Player.vy 0; }CheckTileCollision 的实现是遍历角色包围盒覆盖的所有瓦片只要有一个非 0 就返回 true。注意角色包围盒要比精灵图小一圈否则视觉上还有空隙就判定撞墙玩家会觉得玄学。4.3 帧动画用计时器切换精灵帧角色跑动动画不能每帧都换图那样太快了。用累加器控制// 每帧更新动画 g_AnimTimer dt; if (g_AnimTimer 0.1f) { // 每 0.1 秒换一帧 g_AnimTimer 0; g_CurrentFrame; if (g_CurrentFrame 4) g_CurrentFrame 0; // 4 帧循环 } // 根据状态选不同的帧序列 if (!g_Player.bOnGround) { g_CurrentFrame 4; // 跳跃帧 } else if (g_Player.vx 0) { g_CurrentFrame 0; // 站立帧 }0.1 秒的切换间隔对应 10 FPS 的动画跑起来比较自然。如果觉得太慢可以调到 0.08。4.4 敌人巡逻与踩踏判定最简单的敌人 AI 就是左右巡逻碰到墙壁或悬崖掉头void UpdateEnemy(Enemy* e, float dt) { e-x e-vx; // 碰墙掉头 if (CheckTileCollision(e-rect)) { e-x - e-vx; e-vx -e-vx; } // 悬崖检测前方脚下没有瓦片就掉头 int frontX (e-vx 0) ? e-x e-w 1 : e-x - 1; int footY e-y e-h 1; if (!IsSolidTile(frontX / TILE_SIZE, footY / TILE_SIZE)) { e-vx -e-vx; } }踩踏判定角色下落时vy 0且角色底部在敌人上半部分就算踩中。踩中后角色弹起 vy -8敌人进入死亡动画。4.5 用矩形调试碰撞盒开发阶段一定要把碰撞盒画出来否则你永远不知道角色为什么卡住// 调试模式画碰撞盒 if (g_bDebugMode) { HBRUSH hOldBrush (HBRUSH)SelectObject(hdcMem, GetStockObject(NULL_BRUSH)); HPEN hRedPen CreatePen(PS_SOLID, 1, RGB(255, 0, 0)); HPEN hOldPen (HPEN)SelectObject(hdcMem, hRedPen); Rectangle(hdcMem, g_Player.x - g_CameraX, g_Player.y, g_Player.x - g_CameraX g_Player.w, g_Player.y g_Player.h); SelectObject(hdcMem, hOldPen); SelectObject(hdcMem, hOldBrush); DeleteObject(hRedPen); }按 F1 切换 g_bDebugMode。这个功能在答辩时最好关掉但开发阶段能省你几个小时。5. 避坑与排查GDI 游戏开发中最容易翻车的五个地方5.1 现象程序编译通过但运行闪退原因最常见的是资源加载失败后没有检查返回值后续代码对空指针操作。另一个高频原因是 VC6 在 Win10/Win11 上兼容性问题特别是win11 visual c 6.0 运行 闪退这个热搜词反映的情况。解决所有 LoadImage、CreateCompatibleDC 的返回值都要判空。如果必须用 VC6右键 exe → 属性 → 兼容性 → 勾选以兼容模式运行 Windows XP SP3同时勾选以管理员身份运行。更稳妥的做法是迁移到 VS2019/2022代码几乎不用改。5.2 现象角色移动时画面撕裂、闪烁原因直接在窗口 DC 上绘制没有用双缓冲。或者双缓冲位图没有在 WM_SIZE 时重新创建。解决确认每帧都是先画到 hdcMem 再 BitBlt 到 hdcScreen。如果窗口大小可变在 WM_SIZE 里先 DeleteObject(hbmMem) 再重新 CreateCompatibleBitmap。5.3 现象角色卡在墙里出不来原因碰撞检测的顺序不对。如果先处理 Y 轴再处理 X 轴角色从上方落到地面时会被水平方向的墙判定卡住。解决严格按先 X 后 Y的顺序处理并且每次移动后立即回退到碰撞前的合法位置而不是只把速度置零。5.4 现象TransparentBlt 报错或链接失败原因没有链接 msimg32.lib或者_WIN32_WINNT宏定义低于 0x0500。解决在 stdafx.h 或主文件顶部加#define _WIN32_WINNT 0x0500 #pragma comment(lib, msimg32.lib)如果用的是 VC6还需要从 VS2008 以上的版本拷贝 msimg32.lib 和对应的头文件。5.5 现象连续开关窗口后程序崩溃原因GDI 对象泄漏。每次 WM_CREATE 创建的 DC、位图、画笔没有在 WM_DESTROY 里释放。解决养成配对习惯——CreateCompatibleDC 对应 DeleteDCCreateCompatibleBitmap 对应 DeleteObjectSelectObject 后要恢复旧对象。在 WM_DESTROY 里统一清理case WM_DESTROY: if (hbmMem) { DeleteObject(hbmMem); hbmMem NULL; } if (hdcMem) { DeleteDC(hdcMem); hdcMem NULL; } if (hbmPlayer) { DeleteObject(hbmPlayer); hbmPlayer NULL; } PostQuitMessage(0); break;6. 进阶技巧用脏矩形和固定时间步把帧率稳在 606.1 脏矩形只重绘变化区域全屏 BitBlt 在 800×600 下每帧要拷贝 48 万像素VC6 Debug 版本跑不满 60 FPS。优化思路是只重绘发生变化的区域——角色移动前的位置和移动后的位置// 记录上一帧角色和敌人的位置 RECT rcDirty[16]; int nDirtyCount 0; void AddDirtyRect(int x, int y, int w, int h) { if (nDirtyCount 16) { rcDirty[nDirtyCount].left x; rcDirty[nDirtyCount].top y; rcDirty[nDirtyCount].right x w; rcDirty[nDirtyCount].bottom y h; nDirtyCount; } } // 渲染时只更新脏矩形区域 for (int i 0; i nDirtyCount; i) { BitBlt(hdcScreen, rcDirty[i].left, rcDirty[i].top, rcDirty[i].right - rcDirty[i].left, rcDirty[i].bottom - rcDirty[i].top, hdcMem, rcDirty[i].left, rcDirty[i].top, SRCCOPY); }脏矩形对横版过关游戏效果有限因为背景一直在滚动。但如果你的关卡有大量静态区域能省 30% 左右的绘制时间。6.2 固定时间步让物理不受帧率波动影响可变 dt 的问题是帧率波动时跳跃高度会不一致。固定时间步的做法是把物理更新固定为 1/60 秒渲染可以插值const float FIXED_DT 1.0f / 60.0f; float accumulator 0.0f; void GameLoop() { float frameTime GetDeltaTime(); if (frameTime 0.25f) frameTime 0.25f; // 防止螺旋死亡 accumulator frameTime; while (accumulator FIXED_DT) { UpdatePhysics(FIXED_DT); // 固定步长更新 accumulator - FIXED_DT; } float alpha accumulator / FIXED_DT; RenderGame(alpha); // 渲染时插值画面更平滑 }这个模式在 GDI 下可能有点过度设计但如果你打算把代码迁移到 DirectX 或 OpenGL这套循环结构可以直接复用。6.3 验证帧率的简单方法在窗口标题栏显示 FPSstatic DWORD s_dwFrameCount 0; static DWORD s_dwLastFpsTick 0; static float s_fFps 0; s_dwFrameCount; DWORD dwNow GetTickCount(); if (dwNow - s_dwLastFpsTick 1000) { s_fFps s_dwFrameCount * 1000.0f / (dwNow - s_dwLastFpsTick); s_dwFrameCount 0; s_dwLastFpsTick dwNow; TCHAR szTitle[64]; _stprintf(szTitle, _T(横版过关 - FPS: %.1f), s_fFps); SetWindowText(hwnd, szTitle); }如果 FPS 低于 50优先检查是不是每帧都在创建 GDI 对象。CreatePen、CreateSolidBrush 这类调用必须移到初始化阶段不能放在渲染循环里。6.4 我踩过的最大的坑当年做这个毕业设计时角色跳跃总是粘墙——贴着墙跳的时候会卡在半空。查了两天才发现是碰撞检测里用了而不是导致角色在墙边缘时同时触发水平碰撞和垂直碰撞vy 被反复清零。改成严格大于后问题消失。这个教训让我后来养成了一个习惯碰撞检测的边界条件永远用开区间并且一定要画碰撞盒可视化。希望帮到你。本文还有配套的精品资源点击获取
返回列表