news 2026/10/18 4:37:09

VC6 GDI横版过关游戏源码解析:整图加载与碰撞检测实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
VC6 GDI横版过关游戏源码解析:整图加载与碰撞检测实现

简介:基于Visual C++与GDI实现的横版过关游戏完整源码,玩法与画面参考经典超级玛丽,面向毕业设计、项目实训等场景,适合需要掌握C++面向对象特性(类、继承、多态)与Windows GDI图形编程的在校学生。资源共32个文件,以cpp源文件、h头文件为主,辅以bmp位图素材、ico图标和txt地图配置,压缩包仅161KB,是一个可用VC6直接打开编译的完整工程,工程内头文件、源程序、图片及地图文件分类存放,便于按模块阅读。目前已有459人学习下载。代码覆盖角色移动跳跃、碰撞检测、地图分关读取、键盘交互等核心机制,并展示了GDI绘制位图、定时器驱动动画、游戏地图管理等完整流程;其中地图以文本形式保存,每关单独加载,并非一次性拼接,这种设计对理解关卡扩展与数据驱动开发很有帮助。对准备课程设计、毕业答辩或初次尝试游戏开发的同学来说,既能参考C++游戏逻辑的编写方式,也能学习GDI在图形交互中的实际应用,是一份结构清晰、便于二次开发的示例。

1. 结论先行:这不是地图拼接游戏,而是一关一张整图的横版过关源码

先给结论:这份用 Visual C++ 6.0 和 GDI(Graphics Device Interface)编写的横版过关游戏源代码,核心思路反常识——它不在地图拼接算法里打转,而是把一关做成一张完整大图,碰撞数据写进 map1.txt,运行瞬间一次加载。这种“整图 + 文本逻辑”的做法,让美术出图快、逻辑调整不用动图,非常适合做毕业设计和项目实训的 C++ 游戏参考。

它适合三类人:一是找 C++、GDI 相关毕设或项目实训源码,想看到完整运行链路的同学;二是接手老工程、对着 dsw 却点不动的开发者;三是想搞懂 GDI 怎么写动画、碰撞检测的学习者。功能不算多,但地图加载、角色控制、碰撞修正、用户交互这些游戏基础设施全齐了。后面每一章沿着工程结构、核心代码、编译运行、避坑、改造逐步展开。

2. 架构与工程拆解:一份 VC6 工作区里藏着的完整图纸

2.1 技术选型:不是炫技,是够用的图形接口

毕设选型很多时候看机房装了啥。某高校的实训环境给的是 VC6,DirectX SDK 并不是每台机器都有,而 GDI 作为 Windows API 的一部分随系统自带,打开工程就能画。所以这份源码用 GDI 做渲染不是退而求其次,而是在 VC6 环境下最稳的方案。对 640x480 分辨率的 2D 平台跳跃来说,GDI 的 BitBlt 性能完全够用,瓶颈通常在碰撞检测写法和无效区更新策略,不在绘制函数本身。明白这一点,后面读代码就快了。

GDI 在这个项目里的角色集中在三处:位图装载(LoadImage/CreateBitmap)、位块传输(BitBlt/StretchBlt)和文本绘制(DrawText)。横版过关游戏需要频繁更新画面区域,GDI 没有硬件加速也没有场景图,但它能高效地做“把一张位图画到窗口”这件事,配合双缓冲就能解决刷屏问题。剩余逻辑,比如键盘状态、玩家位置、碰撞盒子,全部由 C++ 代码直接管理,不走引擎,所有状态一目了然,这也是毕设答辩时最容易被问清楚的架构。

2.2 目录结构:从 dsw 到 map1.txt 的文件地图

解压这份资源看到的是标准 VC6 工作区布局:

文件作用
mario01.dsw / mario01.dspVC6 工作区与工程文件,双击 dsw 打开
mario01.cpp / mario01.h主程序入口、窗口过程、游戏主循环
gamemap.cpp / gamemap.h地图数据加载、碰撞体链表管理
bitmaptool.cpp / bitmaptool.h位图装载与透明贴图封装
texttool.cpp / texttool.h文本绘制与 trace 日志输出
myclock.h计时器封装,控制帧节奏
mario01.rc / mario01.ICO资源脚本与程序图标
map / map1.txt关卡数据目录,第一关逻辑层
pic / 多个 bmp背景整图、角色序列帧、道具图标
trace.txt运行期调试记录文件

值得留意的是 map 目录下只有 map1.txt,说明源码默认只有一关可玩,“1关加载1次地图.txt”这个备注在文件结构上直接体现。图片资源分得极细:map.bmp 是整张背景,mapsky.bmp 是天空层,pre1.bmp 是前景装饰,role.bmp 是角色的多帧序列图。资源命名的层次感就是代码里绘制顺序的提示,懂了这个顺序,后面换图片素材时就不会贴错层。

2.3 程序主线:从 WinMain 到 WM_PAINT 的帧循环

这份工程是 MFC 外壳,入口在 InitInstance,真正的游戏循环不在 while,而在 Windows 消息循环和 WM_TIMER 的配合里。窗口过程收到定时器消息就推进逻辑帧,收到 WM_PAINT 就重绘画面,两者通过 InvalidateRect 解耦。

// mario01.cpp 简化结构:窗口过程里靠 WM_TIMER 推动逻辑帧 case WM_TIMER: if (!g_game.IsPause()) { g_game.UpdatePlayer(m_deltaTime); // 读键盘 → 算加速度 → 更新坐标 g_game.CheckCollisions(); // 遍历 map1.txt 生成的碰撞盒 g_game.UpdateCamera(); // 摄像机跟随角色,限制滚动范围 InvalidateRect(hWnd, NULL, FALSE); // 请求重绘,但不立即刷新 } break; case WM_PAINT: { PAINTSTRUCT ps; HDC hdc = BeginPaint(hWnd, &ps); g_game.DrawScene(hdc); // 内部分层 BitBlt + 角色序列帧 EndPaint(hWnd, &ps); break; }

这段代码是整份工程的中枢。WM_TIMER 负责逻辑帧,WM_PAINT 只做读取和绘制,不承担计算。逻辑帧里改了玩家坐标,渲染帧拿来画,画面卡顿时角色不会瞬移。m_deltaTime 一般取 50ms 或 100ms,对应 20FPS 或 10FPS 的刷新上限。老项目为了控制 CPU 占用,常用 SetTimer(hWnd, 1, 50, NULL),这个数值直接影响手感:数值越小越跟手,但 CPU 消耗越大。毕设演示时建议 50ms,答辩机器普遍配置不高,太高的刷新率反而暴露双缓冲没做好的闪烁。

3. 核心模块拆解:地图加载、角色动画与碰撞修正的实现链路

3.1 地图加载:整图一次性读入,碰撞数据交给文本

先回答那个关键设计:为什么说“不是地图拼接的,是1关加载1次地图”。传统 Tile 地图的做法是把少量小图元按矩阵拼出整关,地块数据写在大数组里。这份工程反过来:美术直接把一整张长条 BMP 画完,运行时代码把整图读入内存,同时用 map1.txt 描述哪些区域是实心的、哪里放道具、哪里生成敌人。这样地图的逻辑层和美术层完全分离,改个砖块位置只需编辑 txt,不用动图。

// gamemap.cpp 关卡数据加载:一次打开 map%d.txt,逐行读取 bool CGameMap::LoadFromFile(int level, const char* dir) { char path[128]; sprintf(path, "%smap%d.txt", dir, level); // 拼出 map1.txt 路径 FILE* fp = fopen(path, "r"); if (!fp) return false; // 文件不存在就加载失败 char line[128]; int type, x, y, w, h; while (fgets(line, sizeof(line), fp)) { if (line[0] == ';' || line[0] == '\n') continue; // 注释/空行跳过 if (sscanf(line, "%d %d %d %d %d", &type, &x, &y, &w, &h) != 5) continue; Block* b = new Block(type, x, y, w, h); m_blocks.AddTail(b); // CObList 链表,方便动态增删 } fclose(fp); return true; }

地图数据一次解析完,之后每个关卡的碰撞体都存在 m_blocks 链表里。碰撞检测每帧遍历链表,关卡长度几十个块时开销完全可忽略。这里不用固定数组,是因为做第二关时块的数量不确定,链表增删更灵活。每一行五个字段按顺序是:类型、左上角 x、左上角 y、宽度 w、高度 h。类型有约定,比如 1 代表实心砖块、2 代表金币、3 代表敌人出生点、4 代表终点旗杆。这个约定在工程内部形成契约,后面加新类型只需要扩展 switch,主循环不用改。

这个方案直接收益是:美术出图快,逻辑数据调整不用改图;代价是换一关就要换一整张大图,内存占用偏高。但毕设项目里“一关一张图”足够跑出效果。注意 Block 类在 gamemap.h 里继承了 CObject,这是放进 CObList 的前提,如果自己动手改成 CList<Block*> 也能跑,但序列化能力会丢失。

3.2 角色驱动:键盘状态、加速度和序列帧动画

角色逻辑对应 mario01.cpp 里的 Player 相关代码和 role.bmp 位图。role.bmp 通常是一行多帧的序列:站立、奔跑、跳跃各占若干帧,绘制时按玩家方向键切换帧索引形成跑步动画。这个工程的绘制封装在 bitmaptool.cpp 里,先用掩码位图处理透明,再 BitBlt 到目标 DC,透明色一般用品红 RGB(255,0,255),素材里尽量避免这种颜色。

// 玩家移动逻辑:做了加速和摩擦,比匀速更像游戏 void CGame::UpdatePlayer(double dt) { bool left = (GetAsyncKeyState(VK_LEFT) & 0x8000) != 0; bool right = (GetAsyncKeyState(VK_RIGHT) & 0x8000) != 0; bool jump = (GetAsyncKeyState(VK_SPACE) & 0x8000) != 0; m_accX = 0.f; if (left) m_accX -= m_walkAccel; // 向左加速度 if (right) m_accX += m_walkAccel; // 向右加速度 m_velX += m_accX * (float)dt; // 速度积分 if (m_velX > m_maxSpeed) m_velX = m_maxSpeed; if (m_velX < -m_maxSpeed) m_velX = -m_maxSpeed; m_posX += m_velX * (float)dt; // 位置积分 if (jump && m_onGround) { // 只有在地面才能起跳 m_velY = -m_jumpSpeed; // 负方向向上 m_onGround = false; } m_velY += m_gravity * (float)dt; // 重力每帧累加 m_posY += m_velY * (float)dt; }

这里把横版过关的手感拆成了四个参数:walkAccel 决定加速快慢,maxSpeed 封顶上限,jumpSpeed 决定起跳力度,gravity 决定下落手感。四个数值配合起来,角色就不再是“匀速移动 + 瞬时跳跃”的演示程序,而是有滞空感和惯性的游戏角色。dt 是帧时间,用秒做单位,VC6 里一般用 GetTickCount() 前后取差值得到。0.05 秒的帧间隔下,跳一个 2 块高的台阶大概需要 250 到 350 毫秒滞空,如果觉得跳得不够高,优先调 jumpSpeed 或 gravity,而不是改地图。

序列帧的绘制顺序也有讲究:先画背景层 map.bmp,再画前景装饰 pre1.bmp,最后画角色和道具。这个顺序在 DrawScene 里是固定的,原因很简单——角色必须在装饰之上、背景之下,否则会出现角色被前景遮住半边身子的视觉 bug。换素材时保持帧宽度一致,否则跑步动画会跳。

3.3 碰撞修正:先移动后回退,分方向把角色“压”出障碍

碰撞是横版过关最容易翻车的地方。这份工程的处理方式是:先更新坐标,再逐个碰撞体判断相交,发现进入砖块后把位置按最小位移推出去。之所以不用“碰撞前预测”,是因为平台跳跃里角色可能从四个方向撞向砖块,预测法要维护一堆边缘状态,不如回退法简单直接。

// 玩家与实心块碰撞:比较重叠量,往最小的一侧推出 void CGame::ResolveCollisions(CGameMap* map) { POSITION pos = map->m_blocks.GetHeadPosition(); while (pos) { Block* b = map->m_blocks.GetNext(pos); if (b->type != BLOCK_SOLID) continue; // 只处理实心类型 CRect rcBlock(b->x, b->y, b->x + b->w, b->y + b->h); CRect rcPlayer((int)m_posX, (int)m_posY, (int)(m_posX + m_width), (int)(m_posY + m_height)); if (!rcPlayer.IntersectRect(&rcPlayer, &rcBlock)) continue; int overlapLeft = (rcPlayer.right - rcBlock.left); int overlapRight = (rcBlock.right - rcPlayer.left); int overlapTop = (rcPlayer.bottom - rcBlock.top); int overlapBottom = (rcBlock.bottom - rcPlayer.top); int minOverlap = min(min(overlapLeft, overlapRight), min(overlapTop, overlapBottom)); if (minOverlap == overlapTop && m_velY > 0) { m_posY = (float)(b->y - m_height); // 落在砖块顶面 m_velY = 0.f; m_onGround = true; // 重置跳跃可用 } else if (minOverlap == overlapBottom && m_velY < 0) { m_posY = (float)b->y + b->h; // 头顶撞墙 m_velY = 0.f; } } }

先移动后回退的问题在于穿透方向不唯一,所以按重叠量最小的一侧决定推出去的方向。判断 m_velY 的正负可以区分“从上方落下”和“从下方顶起”,这样角色站在砖块上时 onGround 会被置回 true,起跳条件成立。这里有个隐藏细节:水平方向的回退被省略了,实际工程里还应该有一段把 m_posX 推出砖块左右的逻辑,否则角色贴墙走时会卡进墙里 1 像素,看起来像被墙“吸住”。这类实现的直角边碰撞对斜面地形不友好,但毕设体量下,“能踩、能顶、不穿墙”已经合格。

4. 编译运行指南:在老工具链下把游戏跑起来

4.1 用 VC6 打开工作区:dsw、dsp、ncb 的角色分工

拿到这份源码最直接的方式是安装 VC6 后双击 mario01.dsw。VC6 的工程体系由 dsw(工作区)、dsp(项目)、ncb(类浏览器缓存)、opt(窗口状态)组成。dsw 是入口,dsp 里记录了源文件列表和编译选项;ncb 和 opt 删掉没关系,重新打开会自动生成,这是接手别人工程混进一堆缓存文件时不必担心的原因。编译前看一眼 dsp 里的设置:工程配置建议选 Win32 Release 或 Win32 Debug,字符集保持默认的多字节字符集。

这份资源没有用到 UNICODE,强行切成 Unicode 会导致 CString 和文件操作的编码全乱掉,出现中文乱码或路径打不开。如果装了 VC6 绿色版或精简版,缺失库时会提示找不到 MFC42D.dll,直接去同目录检查是否有 MFC 头文件即可,这个错误几乎都来自不完整安装路径。另外,VC6 在较新系统上编译时常会误报 scanf 这类函数不安全,这是工具链年代差异造成的警告,不是工程本身的逻辑错误,不用理它。

4.2 工作目录与图片加载失败的排查

GDI 游戏最常见的黑屏细节不是代码错误,是工作目录不对。gamemap.cpp 里 sprintf 拼出来的是相对路径,比如 map\map1.txt 和 pic\map.bmp,这些路径都以当前工作目录为基准。从 VC6 里 F5 调试时没问题,因为 IDE 会把工作目录设为工程目录;但直接去资源管理器双击 exe 就会花屏或退出,因为 exe 的当前目录是它所在目录,而 bmp 在 pic 子目录下,相对路径失效。

cd /d E:\mario01 dir pic\*.bmp mario01.exe

运行前先确认 exe 与 pic、map 目录的相对位置,这三个目录必须同级。如果非要让 exe 独立运行,可以把资源路径改成绝对路径,或者用 GetModuleFileName 算出 exe 所在目录再拼路径,后者更稳妥。图片格式也有要求:VC6 的 CImage 不是标配,工程里的 bmp 全部是 8 位或 24 位无压缩位图,不要混入 PNG 或 JPG。换素材时用画图程序另存为 24 位 BMP 再替换。判断是不是路径问题,最快的方法是在 WM_PAINT 里加一个临时 DrawText,输出当前工作目录,一目了然。

4.3 逻辑调试:从 trace.txt 和 Output 窗口看运行痕迹

工程里带了一个 trace.txt,这是老式 printf 式调试的产物。texttool.cpp 里封装了写日志的函数,代码埋点会把玩家坐标、碰撞结果、地图加载状态追加进去。跑不起来的阶段,盯着 Output 窗口不够用,因为 GDI 游戏的日志在 WM_PAINT 里可能被重复刷屏,落盘才看得全。排错顺序建议是:能编译 → 先看 trace.txt 有没有 “load map fail” → 再看窗口闪不闪 → 最后才调手感参数。多数问题不是算法错了,是某个位图路径没加载出来。

提示:写完日志记得 fflush 强制刷新缓冲区,否则程序崩溃时最后几条记录会丢。

这里还要提一个 Release 和 Debug 的差别。Debug 版跑得动但 Release 版跑不动,八成是某个变量没初始化,VC6 在 Debug 下会自动清零栈内存,Release 不会。我拿到这种老工程,第一件事就是用 Release 配置编译一遍,如果只有 Debug 能跑,就去查构造函数里所有成员变量是否都有初始值。

5. 避坑记录:GDI 游戏在编译和调试中的典型翻车现场

5.1 现象:双击 dsw 打不开,VC6 直接闪退

原因:VC6 的 IDE 在较新系统上兼容性差,常见于 Win7 以上版本,窗口初始化时崩溃。解决:不要硬顶,先确认 VC6 组件完整,然后右键 exe 设置兼容模式运行,或者干脆装 XP 虚拟机在里面编译。这是最省时间的路,老工程拿到手先别纠结 IDE 版本,跑起来才是目的。

5.2 现象:运行后画面闪烁,像老幻灯片

原因:WM_PAINT 里直接贴位图,没有双缓冲,GDI 每次擦背景加重绘,刷新不同步。解决:在 WM_PAINT 里创建内存 DC,把整帧画面画到兼容位图上,最后一口气 BitBlt 回窗口。另外把 WM_ERASEBKGND 直接 return TRUE,禁止系统擦背景,闪烁会进一步减轻。双缓冲的代价是一帧多拷贝一次整图,但 640x480 分辨率下开销可以忽略。

5.3 现象:map1.txt 里的中文注释变成乱码,部分路径读不到

原因:VC6 默认 ANSI 编码,而文件被编辑器存成了 UTF-8 带 BOM 格式,sscanf 解析行时遇到 BOM 头会读入一个不可见字符,导致字段错位或跳过。解决:地图文本统一用记事本另存为 ANSI 编码,行尾保持 CRLF 不要用 LF。改完文件后,重新检查第一行有没有乱码符号,有就去掉。代码层面可以在主循环里对 BOM 做一次过滤,但治本还是统一编码。

5.4 现象:角色跳上砖块后持续上下抖动,甚至被卡住

原因:碰撞修正后 onGround 状态与 m_velY 没有同步,下一帧重力又把角色压回砖内,产生抖动。解决:每次落地修正时把 m_velY 归零并设置 m_onGround = true,同时把垂直位置精确置到地块顶面坐标。有的写法会在落地后加一个极小的正偏移,但这属于临时手法,不如把重叠判定改成“允许 1 像素误差”更干净。这个坑排查时要特别注意速度值是否残留,残留速度是抖动的首要嫌疑。

5.5 现象:Release 版运行正常但 Debug 版卡顿严重

原因:Debug 版的迭代器检查和内存校验会让 STL 和 MFC 容器操作慢好几倍,m_blocks 链表每帧遍历在 Debug 下放大明显。解决:毕设答辩时用 Release 版演示,Debug 版留给开发阶段。这不是项目 bug,是工具链特性,但第一次遇到的人多半会误判成死循环。

6. 二次开发思路:从改关卡到换角色,把手感调成自己的

先把关卡加出来。复制 map1.txt 改名 map2.txt,用记事本打开,把所有实心砖的 x 坐标整体右移 100 像素,给新关卡的布局腾出空间;然后把出生点移到新位置,替换背景图 map.bmp 为第二关的整图。代码侧不需要动,GameMap::LoadFromFile 的 level 参数已经支持 map2.txt。

copy map\map1.txt map\map2.txt notepad map\map2.txt

逻辑说明:关卡切换由 level 变量控制,通关后执行 LoadFromFile(level + 1),文件不存在就弹胜利框。再往深做一个存档点就不只是加关卡了,需要记录角色当前坐标和已吃金币,这属于后续扩展,但工程里的链表结构已经留了数据挂载的位置。改完关卡后,每次编辑地图文本都做一次“编码体检”——用记事本另存为 ANSI,再确认没有 BOM 头,这是第五章踩坑教育出来的习惯。

换角色要动 role.bmp。保持帧总数和帧宽高不变,直接替换整张图;如果角色尺寸变了,顺带改 m_width 和 m_height 以及碰撞盒宽高。换图后重点看透明色区域有没有残留品红边,GDI 透明贴图对颜色敏感,素材边缘最好预处理成透明色同色。调整手感的四个参数在 UpdatePlayer 代码里标得已经很清楚:walkAccel、maxSpeed、jumpSpeed、gravity。想跳得高一点就加大 jumpSpeed 或减小 gravity;想让移动更跟手就加大 walkAccel 并适度放开 maxSpeed。每次改完跑一遍第一关,重点验证两步台和深坑处的碰撞。

从那以后我每次拿到老工程,都会先原版完整跑通一遍,再动第一个参数。这样回头看,每处改动都能对上号,不会把“我改的 bug”和“原来就有的毛病”混在一起。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/18 4:36:19

campus-imaotai 实战教程:如何用 Docker 快速搭建 i茅台自动预约系统

campus-imaotai 实战教程&#xff1a;如何用 Docker 快速搭建 i茅台自动预约系统 【免费下载链接】campus-imaotai i茅台app自动预约&#xff0c;每日自动预约&#xff0c;支持docker一键部署&#xff08;本项目不提供成品&#xff0c;使用的是已淘汰的算法&#xff09; 项目…

作者头像 李华
网站建设 2026/10/18 4:34:30

从补习班到名校:韩国精英教育如何“制造”阶层?

1. 这个标题到底在说什么&#xff1a;从“译文”二字看韩国精英教育朋友把“Elite South Korean of an Elite Education”发给我的时候&#xff0c;她自己都笑了——这英文本身读着有点别扭&#xff0c;更像是先用中文想好了“精英教育下的韩国精英”再翻译回去的。但正是这种“…

作者头像 李华
网站建设 2026/10/18 4:33:56

OpenCV零基础手势识别:从摄像头到实时标注的完整实践

简介&#xff1a;本资源是一份面向Python初学者与人工智能入门者的简易手势识别实践项目&#xff0c;聚焦OpenCV图像处理核心流程&#xff0c;解决从视频帧预处理到手势特征提取的完整技术链路问题。压缩包共2个文件&#xff08;1个含超详细逐行注释的Python源码、1份配套课题论…

作者头像 李华
网站建设 2026/10/18 4:30:00

AirLLM:低显存设备跑70B大模型的分层推理方案

1. AirLLM是什么&#xff1a;一台低配电脑跑70B大模型的思路1.1 从一次折腾说起&#xff1a;显存不够怎么办我接触大模型推理比较早&#xff0c;但绝大多数时间都被同一个问题卡住&#xff1a;显存不够。常见的中型开源模型也有几十亿参数&#xff0c;按FP16精度计算&#xff0…

作者头像 李华
网站建设 2026/10/18 4:28:16

从Spring Boot自动装配到微服务架构:Java后端面试深度复盘

面试官把电脑往桌上一推&#xff0c;抬头问我&#xff1a;Spring Boot你用了挺久吧&#xff1f;那你先讲讲&#xff0c;自动装配到底装了什么&#xff1f;我愣了一下。过去大半年我每天都在写 RestController、Service&#xff0c;面试前也背过自动装配的几句话&#xff0c;但那…

作者头像 李华