十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

从MFC老游戏源码看C++游戏循环与碰撞检测的实现

从MFC老游戏源码看C++游戏循环与碰撞检测的实现 简介陨石撞飞机游戏源码包适合作C/MFC学习者的游戏开发入门材料项目核心模拟陨石与飞机的碰撞交互源码围绕对象定义、碰撞检测、事件响应、游戏循环和计分系统展开能帮助理解Windows环境下传统MFC架构搭建完整小游戏的典型流程。压缩包共55个文件体积4.46MB其中cpp与h源文件承载主要逻辑bmp与ico提供图形素材rc/rc2负责资源描述dsw/dsp/clw/ncb等为VC 6.0工程管理文件另有exe、obj等编译产物可对照运行整体覆盖源码、资源、工程配置与可执行程序。已有135人学习下载工程代码量适中可作为C入门后的完整小游戏练习项目。研读源码可以掌握MFC应用建立方法、碰撞检测与事件响应思路、图像资源加载释放技巧还能看到工程中如何组织素材并处理得分、生命周期等游戏逻辑。这些经验对后续接触Unity、Godot等现代引擎也有认知上的帮助。1. 从 Plane.dsw 与 Plane.dsp 认识 VC6 时代的游戏工程结构拿到陨石撞飞机-源代码.zip解压后的文件列表比游戏本身更早暴露了它的底细Plane.dsw、Plane.dsp、PlaneView.cpp、PlaneDoc.cpp、MainFrm.cpp、StdAfx.cpp 这些名字是 Visual C 6.0 的 MFC 文档/视图架构的标准布局不是 Unity也不是 Cocos2d-x。这个游戏是用 C 基于 MFC 写的一个单文档程序核心玩法在 PlaneView 里完成PlaneDoc 负责数据保存MainFrm 提供主窗口框架。对 5 年以上 C 开发者来说这份代码的价值在于它是理解 MFC 消息映射、GDI 绘制和游戏循环如何共存的极佳样本项目规模小没有额外依赖解压后一个 .dsw/.dsp 就能在 VC6 或 Visual Studio 的 MFC 环境里重新编译跑起来。2. MFC 的 CFrameWnd 与 View游戏循环和事件分发机制2.1 文档/视图结构在游戏里扮演的角色PlaneDoc.h 与 PlaneView.h 分别对应文档类和视图类。MFC 把数据Doc与显示View分离PlaneDoc 保存游戏状态如得分、生命数、陨石列表PlaneView 负责把每一帧画面画出来。打开 Plane.cpp 会看到CPlaneApp theApp;从CWinApp派生它的InitInstance()里创建了单文档模板CSingleDocTemplate* pDocTemplate; pDocTemplate new CSingleDocTemplate( IDR_MAINFRAME, RUNTIME_CLASS(CPlaneDoc), RUNTIME_CLASS(CMainFrame), RUNTIME_CLASS(CPlaneView)); AddDocTemplate(pDocTemplate);这段代码把三个类绑定为一个可运行的文档/视图框架。游戏复杂度不高时把逻辑写在 View 里是常见做法因为 View 直接持有窗口句柄容易挂定时器和画图。不过要注意MFC 默认在窗口OnDraw里刷新画面游戏若想做到流畅一般不会依赖系统的 WM_PAINT而是用定时器驱动重绘。2.2 定时器游戏循环的廉价实现MFC 没有内置游戏引擎的主循环很多老项目用SetTimer模拟。在CPlaneView::OnInitialUpdate()里通常会有类似这样的启动逻辑SetTimer(GAME_TIMER, 30, nullptr); // 每 30ms 触发一次约 33 FPS srand((unsigned)time(nullptr)); // 初始化随机种子OnTimer是消息映射后的处理函数负责更新飞机位置、陨石下落坐标、检查碰撞并调用Invalidate(FALSE)触发重绘。Invalidate(FALSE)的 FALSE 表示不擦除背景能减少闪烁但在 GDI 绘制时还是要用双缓冲否则窗口尺寸变化后会留下拖影。这里的关键点在于区分“逻辑更新”与“绘制”。定时器只负责逻辑真正画图发生在OnDrawvoid CPlaneView::OnDraw(CDC* pDC) { DrawPlane(pDC); DrawMeteors(pDC); DrawScore(pDC); }pDC是设备上下文指针所有 GDI 函数都需要它。项目里的PlaneView.cpp一般会对应提供DrawPlane和DrawMeteors这样的自定义函数内部用MoveToEx/LineTo或Ellipse画几何图形不加载外部图片资源这也是为什么项目里没有 .bmp 文件却照样能显示飞机的直接原因。2.3 MFC 消息映射入门键盘和定时器怎么进 OnKeyDown在 MFC 中事件处理不靠虚函数重载而是靠消息映射宏。PlaneView.h里会有如下声明protected: afx_msg void OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags); afx_msg void OnTimer(UINT_PTR nIDEvent); DECLARE_MESSAGE_MAP()对应PlaneView.cpp里的映射表和实现BEGIN_MESSAGE_MAP(CPlaneView, CView) ON_WM_TIMER() ON_WM_KEYDOWN() END_MESSAGE_MAP() void CPlaneView::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags) { switch (nChar) { case VK_LEFT: planeX - 10; break; case VK_RIGHT: planeX 10; break; case VK_UP: planeY - 10; break; case VK_DOWN: planeY 10; break; } Invalidate(FALSE); }ON_WM_TIMER()可以带但不带 ID因为WM_TIMER消息本身不带 lParam 之外的 ID需要用nIDEvent判断多个定时器场景。飞机移动的步长 10 像素是手工调出来的如果改成 5 会更细腻但更慢改成 20 会很飘。这个参数是初学调游戏手感最值得反复试的地方后续做变速功能也只需要把这块计算改成moveSpeed变量。3. 飞机移动、陨石生成与矩形碰撞检测的 C 实现3.1 用 CRect 表示飞机与陨石的边界这份代码中的“陨石”和“飞机”通常是简单的矩形。CRect是 MFC 封装的矩形类成员left/top/right/bottom可以直接参与碰撞判断。定义对象时我建议这样组织class CMeteor { public: CRect rc; int speed; BOOL alive; };飞机则是PlaneView的成员变量CRect m_planeRect; int m_planeX, m_planeY; // 飞机左上角 int m_meteorSpeed; // 陨石下落基础速度这里有个容易踩的坑CRect的坐标是屏幕坐标还是客户区坐标。MFC 里 view 默认客户区坐标原点在客户区左上角所以初始化时m_planeX 200; m_planeY 400;是相对于窗口内部的偏移而非屏幕全局坐标。若在OnDraw中直接拿m_planeX画窗口移动后游戏逻辑不会跟着出问题因为OnPaint时客户区原点会随窗口位置变化。3.2 陨石生成随机位置与固定下落陨石从顶部生成向下移动超出底部就回收。常见的代码模式是在OnTimer中维护一个数组或CArray。由于项目小直接用一个固定数组CMeteor meteors[MAX_METEORS]是工程上最直观的做法。void CPlaneView::UpdateMeteors() { for (int i 0; i MAX_METEORS; i) { if (!meteors[i].alive) { if (rand() % 100 30) // 30% 概率生成新陨石 { meteors[i].rc.SetRect(rand() % 500, 0, rand() % 500 30, 30); meteors[i].speed 5 rand() % 5; meteors[i].alive TRUE; } } else { meteors[i].rc.OffsetRect(0, meteors[i].speed); if (meteors[i].rc.top 600) meteors[i].alive FALSE; // 飞出屏幕回收 } } }逻辑说明rand() % 100 30用来控制生成概率数值越大陨石越密。rc.SetRect的四个参数是左、上、右、下把宽 30 左右、高 30 的一块矩形作为陨石。OffsetRect(0, speed)表示在 Y 轴正方向移动 speed 像素speed 越大下落越快。后面判断rc.top 600时回收600 这个值要与客户区高度匹配否则陨石飞到窗口一半就消失或者延迟很久才消失。如果要让陨石更像“陨石”而不是方块可以在绘制时保留 CRect 但用Ellipse画圆CBrush brush(RGB(150, 80, 40)); pDC-SelectObject(brush); pDC-Ellipse(meteors[i].rc); pDC-SelectObject(oldBrush);这样碰撞检测仍然用外接矩形视觉效果则是圆形属于游戏开发的常见折中。3.3 矩形碰撞判定何时算“撞上”碰撞检测是游戏中最核心的数学部分。矩形碰撞可以用CRect::IntersectRect判断两个矩形交集是否为空BOOL CPlaneView::HitTest(CMeteor meteor) { CRect inter; return inter.IntersectRect(meteor.rc, m_planeRect); }IntersectRect返回 TRUE 表示有重叠部分FALSE 表示不相交。这样实现的是 AABB轴对齐包围盒碰撞计算量极小足够满足这个游戏场景。如果需要更精确的像素级碰撞需要看位图掩码但这份源码没必要引擎复杂度会指数增加。处理碰撞后的效果通常是减少生命值或直接游戏结束。我建议把飞机命中和陨石销毁分成两个阶段因为同一帧里可能多个陨石同时撞上飞机if (HitTest(meteors[i])) { meteors[i].alive FALSE; // 陨石消失 m_lives--; // 扣命 if (m_lives 0) GameOver(); }逻辑说明先标记陨石消失再扣命避免重复扣命。GameOver()里应该KillTimer(GAME_TIMER)并MessageBox(Game Over)。这个顺序放反的话定时器会继续触发OnTimer在GameOver之后还有新帧更新状态容易乱。3.4 完整的一帧逻辑把上面函数串联进 OnTimer以上函数要在OnTimer里按正确顺序调用才能表现顺畅void CPlaneView::OnTimer(UINT_PTR nIDEvent) { if (nIDEvent ! GAME_TIMER) { CView::OnTimer(nIDEvent); return; } UpdateMeteors(); // 1. 移动陨石、生成新陨石 CheckCollisions(); // 2. 碰撞检测 Invalidate(FALSE); // 3. 请求重绘下一帧 }顺序说明理论上应该先根据输入更新飞机位置再更新陨石再做碰撞检测。如果把碰撞检测放在陨石移动之前逻辑会慢一帧肉眼不易察觉但调试时用断点看变量会发现坐标顺序不对。Invalidate(FALSE)不擦除背景为避免闪烁还需要结合OnEraseBkgnd返回 TRUE 或使用双缓冲。老代码里经常见到一条杠的飞机拖影问题根因就是这两处没配合好。4. 用 Visual C 6.0 编译调试常见报错、断点与性能分析4.1 从 .dsw/.dsp 装载工程解压 zip 到英文路径后双击Plane.dsw即可在 Visual C 6.0 中打开工程。如果你使用的是 Visual Studio 2010 以上的版本MFC 向导项目多数仍能打开 .dsp但会提示“安全升级”。升级时保留原工程建议复制一份到新目录因为 VC6 的 .dsp 和新版 .vcxproj 格式完全不同直接覆盖会丢失原来的调试工作区信息。编译前先确认工程配置是 Win32 Debug菜单 Build - Set Active Configuration然后 Build - Rebuild All。常见的fatal error C1010一般是 StdAfx.h 包含路径问题这个文件在列表里存在通常把 Precompiled Header 设置成“Create/Use Precompiled Header”并统一包含StdAfx.h即可解决。4.2 断点失效调试信息不匹配调试时最典型的问题是“当前不会命中断点”原因通常有几种编译的是 Release 配置优化导致行号信息缺失代码没有重新编译pdb 版本与已加载模块不一致断点设在宏或内联函数上。针对这份工程优先检查 Debug 配置并在OnTimer和OnKeyDown开头各打一个断点。如果提示“当前不会命中断点”先在 Build - Clean Solution 后重新生成。Plane.ncb是 IntelliSense 数据库损坏时会导致提示和跳转失灵删掉后 Visual C 6.0 会自动重建不用手动恢复。// 直接在 OnTimer 入口加断点确认定时器是否启动 void CPlaneView::OnTimer(UINT_PTR nIDEvent) // 断点停在这里 { if (nIDEvent ! GAME_TIMER) return; int x 0; // 临时变量用于观察 }逻辑说明如果断点没停下来问题不在碰撞检测而是定时器没被创建或消息没路由到 View。此时优先检查OnInitialUpdate里的SetTimer是否执行以及PlaneView是否真正成为活动视图。如果点击运行后窗口显示但一直不进定时器多半是SetTimer的调用被放在OnDraw里每帧重复创建得不偿失。4.3 用 Debug 输出和性能观测验证游戏帧率定时器的间隔是 30ms理论 33 FPS。你可以在OnTimer里用GetTickCount()计算真实帧耗时static DWORD lastTick 0; DWORD now GetTickCount(); if (lastTick ! 0) OutputDebugString(m_strFps); // 输出到输出窗口 lastTick now;用OutputDebugString不会影响游戏运行也不会弹 MessageBox适合在 Release 下看性能趋势。若发现耗时远大于 30ms多半是陨石数量过多或Invalidate(FALSE)导致OnDraw频繁画整块矩形。把陨石数量从 50 降到 20同时把碰撞检测范围先缩小为每个陨石的外接矩形能立刻改善。对一个 MFC 游戏来说性能瓶颈很少在算法层面而是 GDI 绘制频率。OnDraw里如果有大量CreatePen/CreateBrush而没有及时删除GDI 对象会爆。一个红线指标是任务管理器里 GDI 对象数持续上涨。解决方式是创建画笔后立即SelectObject结束后用DeleteObject释放或者使用 GDI 的Graphics来简化生命周期管理。4.4 Release 与 Debug 的行为差异Debug 版本的变量没有优化断点里能看到每个成员值Release 版本更接近真实用户体验但逻辑错误可能只在 Release 出现。最常见的一个坑是ASSERT在 Release 下被移除原先用ASSERT(meteors[i].alive ! TRUE)这种断言验证的地方不执行了导致异常状态流入后续逻辑。还有一个坑是rand()种子未初始化在 Debug 下可能总是同一组数字但在 Release 下也被随机化如果不想每次调试都固定陨石路径可以在开发时把srand(1)写死发布时再改成时间种子。5. 让游戏更耐玩变速、连击与资源替换的改造技巧5.1 陨石速度随得分平滑提升如果只是把meteor.speed写成固定值玩家不会感受到压力曲线。常见做法是把基础速度和得分挂钩int baseSpeed 5; // 初始速度 int levelFactor m_score / 500; // 每得 500 分升一级 if (levelFactor 10) levelFactor 10; for (int i 0; i MAX_METEORS; i) { if (meteors[i].alive) meteors[i].speed baseSpeed levelFactor rand() % 3; }逻辑说明这里的levelFactor上限设为 10避免后期陨石速度突破物理边界玩家根本反应不过来。速度提升不仅体现在下落快还应该同步缩小陨石尺寸否则提速后保护区域过大难度反而下降。5.2 连击计分消除连续命中带来的单调感游戏原版只有简单的得分递增改造时可以加入连击条。用一个成员变量记录连续无失误击中的陨石数int m_combo; int m_score; void CPlaneView::AddScore() { m_combo; m_score 10 * (m_combo 5 ? 2 : 1); }说明10 * (m_combo 5 ? 2 : 1)表示连击超过 5 次后得分翻倍这能刺激玩家改变操作习惯。要注意在碰撞检测里同时判断陨石是主动撞上飞机还是玩家主动撞上陨石游戏规则里后者一般只算命中一次而不加分否则“自杀刷连击”会破坏平衡。5.3 用 24 位位图替换矢量飞机原项目的DrawPlane用 GDI 画了几条线代表飞机如果想要真正的飞机贴图可以直接替换DrawPlane的内核加载位图资源CBitmap bmp; bmp.LoadBitmap(IDB_PLANE); CDC memDC; memDC.CreateCompatibleDC(pDC); CBitmap* pOld memDC.SelectObject(bmp); pDC-BitBlt(m_planeX, m_planeY, 64, 64, memDC, 0, 0, SRCAND); memDC.SelectObject(pOld);参数说明IDB_PLANE在resource.h里定义资源 ID图片尺寸要在 resources 中设定为 64x64。BitBlt的SRCAND是让透明色和背景做与运算真正做透明需要两次绘制第一次SRCAND画掩码第二次SRCPAINT画图像。这里的坑是位图资源必须驻留不能把它定义成局部变量后在函数返回时析构否则下一次OnDraw会访问到空 GDI 对象表现就是飞机闪烁或消失。5.4 记录高分并校验高分存档是很多入门项目没做的事做出来会让游戏完整度高一个量级。写一个简单的文本保存函数void CPlaneView::SaveHighScore() { FILE* fp fopen(score.dat, w); if (fp) { fprintf(fp, %d, m_highScore); fclose(fp); } }注意score.dat应该写到用户目录或 exe 同级目录。如果它在可执行文件系统目录需要先取得模块路径否则在 Vista 及以上系统会因为 UAC 权限问题写入失败。比较稳妥的是用GetModuleFileName得到 exe 所在目录再拼上score.dat。读取时同样不要用fopen(score.dat,r)直接读否则系统会先找应用数据目录而不是 exe 目录造成高分记录失效。到这里陨石撞飞机这份源码能做的扩展基本就到工具层面了。把变速、连击、位图替换和高分存档这套组合跑通再回头看最开始的Plane.dsw你会发现 MFC 老工程里确实藏着现代游戏循环所有基本要件的早期影子。本文还有配套的精品资源点击获取
返回列表