ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

VC6+GDI横版过关游戏源码详解:地图、碰撞与动画实现

VC6+GDI横版过关游戏源码详解:地图、碰撞与动画实现 简介面向毕业设计项目实训的横版过关游戏完整源码包以Visual C 6.0结合GDI图形接口实现玩法与画面借鉴经典“超级玛丽”系列。项目采用C面向对象思想组织游戏逻辑通过位图绘制、定时器动画、键盘交互和分关地图加载完成场景切换适合计算机专业学生做课设、毕设或学习Windows图形编程时参考。资源共32个文件压缩包约161KB其中包含9个头文件、6个C源文件、6张BMP位图素材以及地图文本、工程配置与图标资源源码结构清晰便于按模块阅读。当前页面已有459人浏览学习可配合博文说明快速定位课程设计中的核心绘图与碰撞检测思路。下载后既能对照学习GDI绘制与关卡地图读取方法也能直接运行或二次修改作为移植功能、扩展关卡乃至撰写设计文档的技术基础。1. 一个 VC6 GDI 项目把横版过关游戏的底牌全亮出来了用 Visual C 6.0 和 GDI 手写的横版过关游戏源代码一眼看去像仿经典红白机水管工闯关作品但它和网上那些拿现成引擎拼出来的 Demo 有本质区别地图不是预先拼接好的整图而是每关开始时从map1.txt读取文本配置再用BitBlt把对应的瓦片一块块画到窗口里。这份东西作为毕业设计和项目实训素材价值在于把游戏里最绕的地图加载、角色碰撞、动画帧切换全部拆成了你可以逐行读的 VC6 工程。想搞懂“2D 横版游戏从一份文本文件到屏幕上会跑会跳的闭环”的同学可以直接拿它当解剖样本。2. 项目结构拆解VC6 工程里这些文件各管哪一段2.1 工程入口mario01、StdAfx、resource.h 的三层关系拿到压缩包先看根目录mario01.dsw和mario01.dsp是 Visual C 6.0 的工作区与工程文件。.dsw是工作区外壳里面可能只挂一个工程.dsp才是真正描述编译单元、链接选项、预处理宏的工程文件。双击mario01.dsw就能直接打开整个项目而mario01.opt、mario01.ncb、mario01.plg这三个是 VC6 自动生成的辅助文件opt存界面布局ncb存符号数据库plg是编译日志它们不参与最终程序逻辑删了也能重新生成。进入代码层mario01.cpp是主程序入口窗口类注册、消息循环、游戏主循环都在这mario01.h是对应主头文件通常在这里声明主窗口类、全局实例句柄和核心函数。resource.h是资源 ID 的统一出口所有菜单、图标、位图的 ID 宏都定义在这里。StdAfx.h/StdAfx.cpp是 MFC 和 Windows API 的预编译头把windows.h、mmsystem.h这类高频头文件放进去能明显缩短编译时间。给第一次读这种工程的人一个建议别从mario01.cpp第一行顺序读先看resource.h里的 ID 对照表再打开.rc资源脚本看每个位图资源的 ID 和文件名是否对上。GDI 游戏最容易翻车的地方就是位图加载失败后没有提示主程序照样运行画面却白屏。这个工程里SMALL.ICO和mario01.ICO是窗口和小图标的资源.rc文件会把它们绑到工程中如果手工精简文件时漏了其中一个编译能过运行后窗口图标却是空的。2.2 地图与素材map1.txt、bmp、ico 在游戏里的用途这一节的素材文件是最容易看花眼的先分清三类地图数据、静态背景、角色动画。map1.txt是第 1 关的地图数据整个代码包的核心逻辑都在围着它转。它不是二进制二进制地图格式而是纯文本每一行代表游戏世界的一行瓦片每一个字符代表一种图素地面、砖块、金币、尖刺、角色出生点、关卡终点等等。gamemap.cpp在主循环初始化时一次性把整个文件读进内存按行列转换成对应的瓦片 ID 数组所以游戏过程中不需要再拼地图滚动屏幕时只是改变绘制起点。压缩包里的map文件夹名和map1.txt要放对位置否则路径错一位就加载不到关卡。位图文件分成几组map.bmp是地图瓦片图集地面砖块、可碎砖、问号块、管道都按固定格子大小排布在里面role.bmp是角色动画帧每一帧等高等宽按横向或纵向排列绘制时用BitBlt按源矩形裁出对应帧pre1.bmp、mapbk.bmp、mapsky.bmp是场景背景层pre1.bmp可能是关卡前景装饰mapbk.bmp和mapsky.bmp用于远景天空、云朵这类静态层ani.bmp是动态物件比如旋转的金币、闪烁的星星需要按帧播放。bitmaptool.h、texttool.h、myclock.h这三个工具文件非常值得看。bitmaptool提供位图加载、透明色处理、快速绘制封装texttool负责在窗口上输出生命值、分数、FPS 这些调试信息myclock是一个简单时钟封装用来驱动帧动画和物理更新而不是依赖OnTimer到处散落定时器。filereport.cpp看名字是文件报告输出通常会往trace.txt里写运行日志跑通后打开trace.txt能看到加载了几个瓦片、地图行列数、角色初始坐标。2.3 工具类模块bitmaptool、texttool、myclock 把重复劳动收口看这类毕业设计代码先看工具类能少走一半弯路。最典型的是bitmaptool.cpp里的透明位图绘制函数它做的事在 GDI 里叫“透明色处理”先把目标区域用BitBlt刷成黑色用SRCAND把透明区抠掉再把源图里要保留的部分用SRCPAINT合上去。void DrawTransBitmap(HDC hdcDest, int x, int y, HBITMAP hBitmap, COLORREF crTransparent) { HDC hdcMem CreateCompatibleDC(hdcDest); HBITMAP hOld (HBITMAP)SelectObject(hdcMem, hBitmap); BITMAP bm; GetObject(hBitmap, sizeof(bm), bm); // 用单色掩码图分离透明区域 HBITMAP hMask CreateBitmap(bm.bmWidth, bm.bmHeight, 1, 1, NULL); HDC hdcMask CreateCompatibleDC(hdcDest); HBITMAP hOldMask (HBITMAP)SelectObject(hdcMask, hMask); COLORREF crOldBk SetBkColor(hdcMem, crTransparent); BitBlt(hdcMask, 0, 0, bm.bmWidth, bm.bmHeight, hdcMem, 0, 0, SRCCOPY); SetBkColor(hdcMem, crOldBk); BitBlt(hdcDest, x, y, bm.bmWidth, bm.bmHeight, hdcMask, 0, 0, SRCAND); BitBlt(hdcDest, x, y, bm.bmWidth, bm.bmHeight, hdcMem, 0, 0, SRCPAINT); SelectObject(hdcMask, hOldMask); DeleteDC(hdcMask); DeleteObject(hMask); SelectObject(hdcMem, hOld); DeleteDC(hdcMem); }这段代码的核心思想是“用掩码当模板”hMask是一张单色位图角色位图的背景色会被转成白色需要保留的像素变成黑色。第一遍SRCAND把目标区域里对应白色的地方保留第二遍SRCPAINT把源图非透明像素叠上去。参数里crTransparent就是角色位图里约定的背景色常见是品红RGB(255, 0, 255)。改素材时最怕把背景色弄成角色身上的颜色那会导致角色缺胳膊少腿。myclock模块一般提供Reset、GetElapsed、Tick几个方法主循环里每帧调用GetElapsed得到毫秒差累计超过固定间隔就切换动画帧。这样做的好处是角色跑动和机器性能解耦配置差的机器帧率高一点帧率低的时候动画仍然按时间推进不会出现“跑得越快跳得越高”的玄学问题。3. 核心机制拆解从地图文本到角色碰撞的完整链路3.1 地图加载gamemap.cpp 如何把 map1.txt 变成可碰撞的关卡gamemap.cpp承担的是整个游戏世界的数据底座。这类横版游戏通常不把地图做成一张大图而是做成“瓦片矩阵”每个格子固定大小。这个项目选择每关加载一次文本地图而不是常用的小地图拼接好处是关卡文件可以手工编辑换一关只需要换一个txt不用重新拼图。地图解析的常规做法是用标准 C 的文件读写接口一行一行读取文本对每个字符做一个switch映射成瓦片 ID。下面是一个简化但结构和真实工程一致的地图读入函数int GameMap::LoadMap(const char* szFileName) { FILE* fp fopen(szFileName, rt); if (fp NULL) return -1; // 文件不存在直接返回错误 int row 0; char ch; while (fgets(m_mapData[row], MAX_MAP_WIDTH, fp) ! NULL) { // 去掉行尾换行符 int len strlen(m_mapData[row]); if (len 0 m_mapData[row][len - 1] \n) m_mapData[row][len - 1] \0; // 逐字符映射成瓦片 ID for (int col 0; col MAX_MAP_WIDTH; col) { char c m_mapData[row][col]; switch (c) { case 0: m_tile[row][col] TILE_EMPTY; break; case 1: m_tile[row][col] TILE_GROUND; break; case 2: m_tile[row][col] TILE_BRICK; break; case 3: m_tile[row][col] TILE_QUESTION; break; case P: m_startX col; m_startY row; break; default: m_tile[row][col] TILE_EMPTY; break; } } row; } m_mapRows row; m_mapCols MAX_MAP_WIDTH; fclose(fp); return 0; }这段代码里TILE_EMPTY、TILE_GROUND这类常量在gamemap.h中定义数字 0 到 3 只是一种约定。MAX_MAP_WIDTH是地图最大列数也是每行缓冲区的长度限制。实际工程里还应该检查每行宽度是否一致否则按固定宽度遍历时会把换行符解析成瓦片。P字符在这里很关键它不产生绘制瓦片而是记录角色出生点m_startX和m_startY存的是瓦片坐标角色初始位置要再乘上瓦片宽高。地图加载完成后游戏里判断某个位置能不能走不再是拿角色矩形和每个像素比对而是把角色中心点换算成行列查m_tile[row][col]对应值是不是砖块。这一步把碰撞计算量从像素级降到瓦片级对 GDI 软件渲染来说是非常必要的性能取舍。3.2 角色移动与动画role.bmp 和时钟驱动的帧切换角色逻辑分布在主程序mario01.cpp和绘制工具之间。每次按键消息更新的是角色 x/y 坐标和方向标志然后由主循环根据myclock的计数值决定当前播放哪一帧。下面是最常见的角色水平移动处理void UpdatePlayer(Player* pPlayer, GameMap* pMap, int offset) { // 左移 if (GetAsyncKeyState(VK_LEFT) 0x8000) { pPlayer-x - pPlayer-speed; pPlayer-direction DIR_LEFT; } // 右移 else if (GetAsyncKeyState(VK_RIGHT) 0x8000) { pPlayer-x pPlayer-speed; pPlayer-direction DIR_RIGHT; } // 按瓦片碰撞检测修正坐标 if (CheckCollision(pPlayer, pMap)) { if (pPlayer-direction DIR_LEFT) pPlayer-x pPlayer-speed; else if (pPlayer-direction DIR_RIGHT) pPlayer-x - pPlayer-speed; } }这里用GetAsyncKeyState而不是等WM_KEYDOWN消息是因为它可以直接读取按键当前状态适合持续按住方向的横版游戏。 0x8000是判断高位是否为 1也就是按键是否处于按下状态。UpdatePlayer的offset参数在带滚动的关卡中用来计算角色相对关卡坐标系的位置屏幕上的实际画点往往是x - cameraOffset。碰撞回退的逻辑很关键先移动再加回而不是先检测再决定移动。代码里的做法是移动后调用CheckCollision一旦碰撞就把坐标退回去这样能避免角色卡进砖块里。这个思路简单但有效毕业设计级别项目里足够稳定。动画帧切换一般维护一个frameIndex和frameTimer当时间累积超过单帧持续时间后frameIndex再通过取模回到循环帧。绘制时用BitBlt从role.bmp中裁出当前帧的源矩形源矩形的位置由角色方向和帧序号决定。比如每帧宽 32、高 48向右走第 0 帧的源矩形就是(0, 0, 32, 48)第 1 帧是(32, 0, 64, 48)。3.3 碰撞检测与绘制瓦片坐标、GDI BitBlt 与双缓冲这部分是横版游戏最容易出 bug 的地方。角色和地面碰撞不能只检测一个点通常用角色矩形经过的四个角或上下左右四个探测点。常见写法是把角色位置换算成瓦片坐标后检测角色覆盖范围内的所有瓦片int CheckCollision(Player* pPlayer, GameMap* pMap) { // 角色矩形四个角的瓦片坐标 int left pPlayer-x / TILE_WIDTH; int right (pPlayer-x pPlayer-width - 1) / TILE_WIDTH; int top pPlayer-y / TILE_HEIGHT; int bottom (pPlayer-y pPlayer-height - 1) / TILE_HEIGHT; for (int row top; row bottom; row) { for (int col left; col right; col) { int tile pMap-GetTile(row, col); if (tile TILE_EMPTY) continue; // 碰到实心瓦片返回碰撞 if (tile TILE_GROUND || tile TILE_BRICK) return 1; } } return 0; }这里TILE_WIDTH和TILE_HEIGHT是每个地图格子的像素尺寸不能随便改必须和map.bmp里每个瓦片的绘制尺寸一致。GetTile返回越界时应该视为不可通行墙壁否则角色会跑出地图边界。在用这份源码二次开发时最容易踩的坑就是地图 txt 行宽不一致导致GetTile取到空字符碰撞检测失效角色直接穿地。绘制端通常是先画背景再画地图瓦片再画角色。为了不闪烁工程里应该有内存 DC先创建一个与窗口 DC 兼容的内存画布按摄像头的偏移量把可见范围的瓦片BitBlt上去最后一次性把整块内存画布拷到窗口 DC。如果源码里没有这一步运行起来画面会像老式电视机一样疯狂闪解决办法参考第 5 章避坑条目。4. 运行与调试在 VC6 里把这套代码跑起来4.1 打开工程与配置工作目录拿到压缩包解压后先不要急着双击mario01.cpp找到mario01.dsw才是正确入口。Visual C 6.0 支持直接打开.dsw它会自动关联同一目录下的.dsp工程文件。如果你的机器装的是 VS 2010 以上版本双击.dsw会弹转换向导最好保留原工程结构不要随便升级成新格式原因是 GDI 代码本身是纯 Win32 API新版编译器可能会把main的入口约定或资源脚本编译方式改出问题。打开工程后第一件事是确认工作目录。GDI 游戏最忌讳用绝对路径去LoadBitmap源码里基本都写的是相对路径比如map1.txt或pic\\role.bmp。VC6 默认的工作目录不一定在源码根目录运行时会因为找不到文件而静默失败。在 VC6 菜单栏选 “Project - Settings - Debug - Working directory”把目录指向源码解压路径确保map1.txt、pic文件夹和mario01.dsw在同一层。这一步不设置好后面所有位图加载全部失败画面只有背景色。建议在打开工程前先核对文件完整性。压缩包里至少要有mario01.dsw、mario01.dsp、mario01.cpp、mario01.rc、resource.h、gamemap.cpp、bitmaptool.cpp、texttool.cpp、map1.txt、pic下的位图文件以及SMALL.ICO、mario01.ICO。缺了.ico还能编译缺了map1.txt或位图文件则程序能启动但画面不对。4.2 编译、运行与查看输出VC6 的编译快捷键是F7编译信息会显示在下方 Output 窗口。如果编译成功最后一行会显示 “0 error(s)”然后按F5直接运行。第一次运行建议从菜单 “Build - Start Debug - Go” 进入调试模式这样如果程序崩溃VC6 会停在出错代码行而不是直接弹一个无提示的对话框。如果 Output 窗口出现fatal error RC1015: cannot open include file afxres.h说明当前 VC6 安装时缺少 MFC 相关组件。解决办法是打开.rc文件把#include afxres.h改写成#include winres.h这是纯 Win32 API 项目常见的资源脚本修正方式。这项工作也适合作为实训题目让学生理解资源脚本和预编译头的关系。编译通过后mario01.plg会记录最近一次编译的详细输出。遇到逻辑问题不弹窗时运行后打开trace.txt是优先方案。filereport.cpp一般会在初始化阶段写入地图加载成功还是失败、加载了多少行、角色出生点在哪里。如果trace.txt没有任何内容先怀疑文件路径和当前工作目录不匹配。4.3 常见运行状态验证程序跑起来之后不要急着按方向键先观察窗口标题和背景层。正常的启动状态是游戏窗口出现背景天空层mapsky.bmp或mapbk.bmp铺满地面瓦片已经逐块画出角色站在原地。如果窗口是纯白或纯灰说明BeginPaint和位图绘制链路有一环没执行优先查trace.txt。按方向键时角色如果能左右走、跳跃、落到平台上说明地图碰撞已经工作。如果角色直接掉出地面大概率是map1.txt首行前有空行导致地图矩阵整体偏移一行这属于最常见的运行期问题下面第 5 章会单独说排查办法。建议每次改地图文件后都做一个固定动作数清楚地图总行数检查每一行字符数一致再运行程序。5. 避坑指南GDI 横版游戏的编译与运行排障5.1 编译期问题头文件顺序、资源 ID 冲突现象编译时报错大量重定义比如WINGDI、_WIN32_WINNT被重复定义。这类错误在 VC6 里出现时先别急着改代码看看是不是有人把windows.h和afxwin.h同时包含而StdAfx.h里又开了WIN32_LEAN_AND_MEAN。原因是resource.h里的资源 ID 宏偶尔会和系统宏撞名尤其是IDC_STATIC这类通用宏。解决方法是保证resource.h在StdAfx.h之后包含并把自定义 ID 统一加前缀比如IDB_BG_01、IDB_ROLE_01。我在自己实训时就把IDR_MAINFRAME这类 MFC 自带的宏和自定义位图 ID 混在一起吃过亏TWO 个宏数值相同导致加载了错误的位图资源。现象RC 编译报错找不到位图文件。原因是.rc里BITMAP资源声明的路径写成相对路径但当前资源脚本目录和实际文件目录不一致。解决方法是打开.rc文件逐个核对IDB_ROLE BITMAP pic\\role.bmp IDB_MAP BITMAP pic\\map.bmp路径中的反斜杠必须写成\\文件名大小写不重要但路径要和实际目录完全对应。如果项目里pic文件夹在压缩包里被改名成pic1这里必须同步改否则编译器不会报错运行时位图加载会失败。VC6 的资源编译器对路径很敏感建议所有资源文件和.rc文件都放在同一层级下或使用相对根目录的路径。5.2 运行期问题花屏、卡顿、碰撞失灵现象画面闪烁严重角色移动时整个窗口像在抖。原因是窗口处理WM_PAINT时直接操作窗口 DC每次BitBlt都先被系统擦成背景色再重新画肉眼看到的就是闪烁。解决方法是建立一个内存 DC先在内存里完成全部绘制最后一次性BitBlt到窗口。很多 GDI 毕业设计源码其实已经有这个封装只是没有在窗口尺寸变化或OnPaint分支里正确复用。HDC hdcWin GetDC(hWnd); HDC hdcMem CreateCompatibleDC(hdcWin); HBITMAP hbmpMem CreateCompatibleBitmap(hdcWin, winWidth, winHeight); HBITMAP hbmpOld (HBITMAP)SelectObject(hdcMem, hbmpMem); // 先画背景、再画地图、最后画角色 DrawBackground(hdcMem); DrawMap(hdcMem, cameraX); DrawPlayer(hdcMem); BitBlt(hdcWin, 0, 0, winWidth, winHeight, hdcMem, 0, 0, SRCCOPY); SelectObject(hdcMem, hbmpOld); DeleteObject(hbmpMem); DeleteDC(hdcMem); ReleaseDC(hWnd, hdcWin);这里cameraX是摄像机偏移量也就是摄像头在关卡世界中的 X 坐标。内存位图的尺寸必须和客户区一致否则BitBlt时会拉伸或截断。如果源码里已经写了这段逻辑还是闪烁检查是不是每次在WM_PAINT里重新创建了内存位图频繁的CreateCompatibleBitmap会让 GDI 对象堆积最终对象耗尽后绘制函数全部失败。现象角色能跳进砖块里或者从地面侧面穿墙。原因是碰撞检测只检测了角色末尾位置没有把移动过程拆分成水平与垂直两次检测。横版游戏的标准做法是先水平移动并检测左右碰撞再垂直移动并检测上下碰撞这样角色在快速下落后就不会因为一次步长跨过一块薄瓦片。如果你解析的源码里只有一个统一的CheckCollision改成两个分轴函数是最值得先动的改动。现象地图看起来错行第一行消失最后一行多出来。原因是map1.txt文件以 UTF-8 编码保存VC6 运行时fgets读不到预期的 ASCII 字符或者每行行尾多了\r。古典 VC6 工程的地图文件通常要求 ANSI 编码文本编辑器保存时如果默认 UTF-8中文字符或 BOM 头会让解析器把第一个字符当瓦片。解决方法是把这几个文件统一另存为 ANSI 编码并使用纯 ASCII 字符设计地图。这条在二次开发换新关卡时特别常见改完地图先看trace.txt输出行数再谈玩法。6. 二次开发思路换新关卡、改角色动画、加简单开始界面想把这份源码改成自己的毕业设计不要急着推翻重写按下面三步走最稳。第一步是读懂map1.txt的字符表然后自己设计一张 20 行乘 60 列的新地图。每行字符数必须一致空余位置补0。关卡文件不需要新写解析代码只需要新增map2.txt再在游戏关卡切换处把文件名改掉。如果你想让地图更长同时修改MAX_MAP_WIDTH宏和m_mapData的列维度即可。第二步是扩展角色动画。role.bmp每帧大小固定在图片右侧追加新帧后需要同步修改动画帧序号计算逻辑。代码块里帧源矩形通常是RECT rcSrc; rcSrc.left frameIndex * FRAME_WIDTH; rcSrc.top 0; rcSrc.right rcSrc.left FRAME_WIDTH; rcSrc.bottom FRAME_HEIGHT;新帧只要在role.bmp尾部排布并增加FRAME_COUNT上限就能实现更多动作。完整动作需要更多帧时可以按行扩展在top上用动作编号做偏移。注意修改位图时保留原来的背景色透明色函数才能正确抠图。第三步是加一个最简单的开始界面不牵动地图逻辑。在主循环之前加一个STATE_MENU状态用texttool输出标题文字检测到回车键后切到STATE_GAME同时调用LoadMap开始关卡。这套改造放在mario01.cpp里即可数据结构上只需增加一个gameState枚举。我从这个工程里学到最深的习惯是每次改地图数据之前先手动数一遍行数和列数再进代码里核对MAX_MAP_WIDTH每次换位图素材之后强制运行一次透明色白边检查。这个习惯帮我省掉了至少五轮“黑匣子”式的排障时间也希望帮到你。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表