VC++ HGE引擎开发超级玛丽:2D游戏源码解析与实战
发布时间:2026/9/23 21:21:14 作者:尧图编辑部 阅读量:1,286

简介这是一份基于Visual C与HGE游戏引擎实现的超级玛丽马里奥2D游戏完整源代码面向想用VC/DirectX/HGE入门或进阶Windows游戏开发的读者。压缩包共183个文件大小8.93MB主要包含45个PNG图片、41个WAV音频、24个HPP头文件和多个CPP源文件同时配有ENT实体、DAT数据、DLL动态库、MOD等辅助资源覆盖图像、音效、逻辑与配置等游戏开发常见模块。代码中可以看到典型的游戏循环、渲染系统、碰撞检测、角色与敌人实体设计、帧同步动画、键盘输入响应、音频播放以及资源管理实现能帮助理解2D游戏引擎架构和状态机切换。已有719人学习下载适合具备一定C基础、希望直接阅读和调试游戏工程的学习者也可作为基于HGE开发其他横版过关游戏的设计参考。1. 别急着啃 DirectXVC 与 HGE 引擎下的超级玛丽源码能教会你什么很多想入门 2D 游戏开发的人第一步就死在 DirectX 的窗口初始化上。用 Visual C 配合 HGE 游戏引擎写的这份超级玛丽源代码直接绕开了这个坎——HGE 把渲染、输入、音频和资源加载都封装成了 C 接口你只需要写游戏循环、碰撞检测和状态切换。代码里能看到马里奥的移动、跳跃、撞砖块、敌人判定以及 splash、游戏、gameover 三个画面的完整切换逻辑适合有 C 基础、想动手拆一个最小 2D 游戏闭环的人。它不解决引擎选型问题但能让你一次看懂一套 2D 游戏是怎么跑起来的。2. 环境搭建HGE 引擎在 VC 6.0 下的编译前置条件2.1 版本搭配为什么这套老代码要配老工具链和 DirectX SDK拿到压缩包先别急着双击 .exe——这份资源里没有现成可执行文件只有工程源码和一堆 .bmp 图片。编译它需要三样东西VC 本身、HGE 引擎的头文件和静态库、DirectX SDK 的 include/lib。三者版本不匹配编译阶段就会翻车。我拆过不少这类 HGE 时代的工程最常见的组合是 VC 6.0 配 HGE 1.x 和 DirectX 8.1/9.0c SDK。HGE 这个引擎封装的底层就是 DirectX 8/9 的 2D 能力hge.h 里会直接 include d3d9.h、d3dx9.h 这些头文件。VC6 内置的 include 目录里没有这些东西必须手动把 DirectX SDK 的路径指进去。若是用 VS2003 之后的编译器硬编 HGE 1.x 的老工程会撞上大量 CRT 重定义问题属于典型的“新工具链编译老代码”的兼容债不建议踩。注意这套源码不一定限定 VC6高版本 Visual Studio 也能开 dsp/dsw但前提是 HGE 版本匹配。拆工程时先看 mk.bat 和工程文件里引用的 HGE 版本宏再决定编译器。我一般会先做一个版本匹配表再动手组件常见搭配选择理由编译器VC 6.0 / VS2003HGE 1.x 年代的主流通用编译器打开 dsp 直接编HGE 引擎1.xDX8/DX9 时代接口简单几千行就能撑起一个完整 2D 游戏DirectX SDK8.1 或 9.0c提供 d3d9.h / d3dx9.hHGE 头文件依赖它们图像资源24/32 位 BMP源码里的 splash.bmp、pic3.bmp 都是 BMPHGE 对 BMP 支持最稳DirectX SDK 装完后的配置顺序很关键。VC6 里打开 Tools → Options → Directories在 Include files 和 Library files 里把 DirectX SDK 的 Include/Lib 目录加进去并且放在 HGE 引擎目录之前。这样编译器会优先找到 SDK 版本正确的 d3dx9.h避免被老版本或重复安装的 SDK 干扰。2.2 cp.bat 与 mk.bat批处理里藏着资源部署逻辑压缩包里有 cp.bat 和 mk.bat 两个批处理很容易被忽略但 90% 的“编译成功但黑屏”问题出在它们身上。这类工程的作者通常会把源码放在 src 或工程子目录运行时需要的图片放在别处cp.bat 就是干“把图片搬运到 exe 输出目录”这件事的。常见内容长这样echo off copy /Y splash.bmp ..\bin\release\ copy /Y gameover.bmp ..\bin\release\ copy /Y pic3.bmp ..\bin\release\ copy /Y 1.5.bmp ..\bin\release\ echo resources copied这段批处理的逻辑是把四张 BMP 复制到 release 输出目录。copy /Y 表示覆盖不询问路径里的..\bin\release\是相对工程目录的输出位置。如果你不用批处理而是直接在 IDE 里按 F7 编译编译出来的 exe 在 Debug/Release 目录图片却在源文件目录运行时 HGE 按相对路径找不到纹理结果就是黑屏或者闪退。mk.bat 一般负责编译常见内容是调用 vcvars32.bat 设置环境变量后再用 msdev 命令行编译工程echo off call C:\Program Files\Microsoft Visual Studio\VC98\Bin\vcvars32.bat msdev mario.dsp /MAKE mario - Win32 Release /REBUILD echo build donevcvars32.bat 是 VC6 的环境变量脚本路径里VC98就是 VC6 的默认安装目录名。/MAKE指定编译哪个工程配置/REBUILD强制全量重建。这套写法的好处是能脱离 IDE 做一键构建坏处是电脑上没装 VC6或者安装路径不是默认值脚本就会停在这一行。所以拆工程的第一件事是把这两个 bat 打开看路径别直接双击。2.3 纹理文件清单splash、pic3、1.5 到底谁是谁图像资源是这类源码最容易看晕的地方。四张 BMP 各自负责一块文件名典型用途加载/使用时机splash.bmp标题/启动画面程序启动后的第一个状态gameover.bmp结束画面马里奥死亡或生命耗尽后pic3.bmp主角和关卡图集游戏主循环里动态切帧用1.5.bmp敌人或补充图集按关卡逻辑加载pic3.bmp 和 1.5.bmp 这类名字并不直观我拆过类似工程后判断它们大概率是把马里奥的多个动作帧、敌人、砖块拼在一张大图上的图集。HGE 里用 Texture_Load 加载整张 BMP然后用 Sprite 从上面按矩形区域切出当前需要的画面而不是分成几十个小图片文件。这也是老游戏源码的通用做法——减少资源文件数量也方便把相关贴图集中管理。这段对新手价值很大如果你看到某张图上有四个一模一样的马里奥那不是贴图重复那是行走动画的四帧代码里通过 SetTextureRect 依次切换。先理解图集再看下一章的主角绘制就不会被坐标搞懵。3. 源码结构拆解游戏主循环、回调函数与状态机切换3.1 WinMain 入口创建 HGE 实例与初始化系统状态HGE 有一套固定的启动流程用 hgeCreate 创建引擎实例通过 System_SetState 设置窗口参数和回调函数再调用 System_Initiate 初始化最后 System_Start 进入主循环。这套源码的 WinMain 大致长这样#include hge.h HGE *hge 0; enum GameState { STATE_SPLASH 0, STATE_GAME, STATE_OVER }; int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) { hge hgeCreate(HGE_VERSION); hge-System_SetState(HGE_WINDOWED, true); hge-System_SetState(HGE_SCREENWIDTH, 800); hge-System_SetState(HGE_SCREENHEIGHT, 600); hge-System_SetState(HGE_TITLE, Super Mario); hge-System_SetState(HGE_FRAMEFUNC, FrameFunc); if (!hge-System_Initiate()) { hge-System_Log(Init failed: %s, hge-System_GetErrorMessage()); hge-Release(); return 1; } hge-System_Start(); hge-System_Shutdown(); hge-Release(); return 0; }hgeCreate 是整个引擎的唯一入口参数 HGE_VERSION 是 hge.h 里定义的版本宏版本不对会导致后续所有接口不可用。System_SetState 用来设置窗口宽高、窗口模式、标题和帧回调。重点看 HGE_FRAMEFUNCHGE 是回调驱动的不是让你写一个 while(true) 循环而是每帧调用一次 FrameFunc 函数游戏逻辑就写在这个回调里。注意System_Initiate 失败时窗口不会出现程序直接返回。新手最容易忽略 System_GetErrorMessage 这个调试出口黑屏首先看它输出的日志比瞎猜路径快得多。3.2 FrameFunc 与 RenderFunc回调驱动的每帧逻辑HGE 的主循环里有两类回调一个负责逻辑FrameFunc一个负责绘制RenderFunc。逻辑回调里做输入检测、位置更新、碰撞判断渲染回调里做清屏、贴图绘制。两者分开是 2D 游戏的基本功——逻辑更新和画面渲染频率可以不一致也方便以后加暂停、慢动作这类功能。bool FrameFunc() { float dt hge-Timer_GetDelta(); switch (g_state) { case STATE_SPLASH: SplashFrame(dt); break; case STATE_GAME: GameFrame(dt); break; case STATE_OVER: GameOverFrame(dt); break; } return false; // 返回 true 会退出游戏循环 } bool RenderFunc() { hge-Gfx_BeginScene(); hge-Gfx_Clear(0xFF252525); switch (g_state) { case STATE_SPLASH: splashSpr-Render(0, 0); break; case STATE_GAME: bgSpr-Render(0, 0); marioSpr-Render(mario.x, mario.y); break; case STATE_OVER: overSpr-Render(0, 0); break; } hge-Gfx_EndScene(); return false; }FrameFunc 和 RenderFunc 的返回值语义需要重点记返回 false 表示“继续运行”返回 true 表示“退出主循环”不是反的。很多人第一次写 HGE 程序想退出游戏在回调里 return true结果发现窗口关了但进程还在跑或者反过来——这就是把返回值语义搞反了。GameFrame 里每一帧先取时间增量 dt。HGE 的 Timer_GetDelta 返回上一帧到这一帧的秒数正常在 0.016 到 0.033 之间浮动。所有跟速度有关的更新比如马里奥的 x speed * dt都必须乘这个 dt这样 60Hz 和 30Hz 的机器上角色移动速度才一致。这是老引擎时代最容易踩的坑后面避坑章节专门展开。3.3 状态机切换从 splash 到 gameover 的一次完整旅程这套源码的游戏流程可以抽象成三个状态标题画面、游戏中、结束画面。每个状态有自己的输入处理和绘制内容状态之间靠按键或游戏事件切换。这个设计叫有限状态机是 2D 游戏最经典的架构。bool SplashFrame(float dt) { if (hge-Input_KeyDown(HGEK_ENTER)) { ResetGame(); g_state STATE_GAME; } return false; } bool GameOverFrame(float dt) { if (hge-Input_KeyDown(HGEK_ENTER)) { g_state STATE_SPLASH; } else if (hge-Input_KeyDown(HGEK_ESC)) { return true; // 返回 true 退出游戏 } return false; }SplashFrame 里检测回车键按下后执行 ResetGame 重置马里奥位置、分数、生命值然后把状态切到 STATE_GAME。GameOverFrame 里回车回到标题画面ESC 退出整个程序。这看起来简单但它是状态机的标准写法每个状态只处理自己关心的事件不越权去改别的状态的变量状态切换集中在明确的位置。我在拆这类老工程时的一个习惯是先把所有状态切换点列出来画一张“什么事件导致什么状态变化”的草稿再读代码。读这份源码时你会发现切换点无非就几个——菜单按回车进游戏、游戏里死亡进结束画面、结束画面按回车回菜单。把这三条线串起来整个程序的骨架就立住了。接下来要看的重点就是游戏状态里那些真正的玩法代码。4. 碰撞检测、动画帧同步与资源管理经典关卡里的关键实现4.1 AABB 矩形相交为什么这份源码不用像素级碰撞超级玛丽里最核心的玩法判定是碰撞马里奥踩砖块、顶砖块、撞敌人、落地面归根结底都是“两个矩形是否重叠”的判断。这份源码用的几乎可以确定是 AABB轴对齐包围盒碰撞也就是用物体的 x、y、宽、高四个值描述碰撞区域然后做矩形相交检测。struct Rect { float x, y, w, h; }; bool AABB(const Rect a, const Rect b) { return a.x b.x b.w a.x a.w b.x a.y b.y b.h a.y a.h b.y; }这段代码判断两个矩形是否相交第一行判断 a 的左边是否在 b 的右边左侧第二行判断 a 的右边是否在 b 的左边右侧第三、四行对应上下方向。四个条件同时成立才算撞上。为什么不用像素级碰撞因为像素级检测要逐像素比对两张贴图的 alpha 通道性能消耗大而且对这个游戏来说没必要——马里奥和砖块都是规矩的矩形AABB 的误差在视觉上完全可以接受运算量却只有几次浮点比较。碰撞判定之后的“还原”比判定本身更值得看。马里奥撞到砖块后不能只是检测到相交就算了必须把位置推回合法区域否则马里奥会陷进砖块里抖。常见做法是记录上一帧位置发生碰撞后按运动方向把位置还原到碰撞面边缘if (AABB(marioBox, tileBox)) { // 从上往下撞到砖块顶vy 0 说明正在下落 if (mario.vy 0 mario.y mario.h - tileBox.y 8.0f) { mario.y tileBox.y - mario.h; mario.vy 0; Sound_PlayBlockHit(); } }这里的 8 像素是一个手感余量。如果严格按矩形边界判定马里奥头都快钻进砖块里才触发顶砖块视觉上就很别扭。给它 8 像素的容差“头刚碰到砖块边缘”就触发判定手感会舒服很多。这类工程里到处都是这种经验值参数改数值比改逻辑更能显著影响游戏体验。4.2 帧动画与帧同步从同一张贴图切出四个走路姿态马里奥走路不是一张静态图而是四帧循环播放。图集 pic3.bmp 里按顺序排列着四个走路姿态代码通过 SetTextureRect 切换到不同区域配合一个计时器控制切换频率就能形成流畅的走路动画。const float WALK_RECT[4][4] { {0, 0, 32, 48}, {32, 0, 32, 48}, {64, 0, 32, 48}, {96, 0, 32, 48} }; static float animAccum 0.0f; static int frame 0; // 帧同步累计时间够了才换下一帧 animAccum hge-Timer_GetDelta(); if (animAccum 0.12f) { animAccum 0.0f; frame (frame 1) % 4; } marioSpr-SetTextureRect(WALK_RECT[frame][0], WALK_RECT[frame][1], WALK_RECT[frame][2], WALK_RECT[frame][3]);WALK_RECT 数组里的四个矩形x 坐标依次是 0、32、64、96代表从图集左侧开始每 32 像素宽切一帧高度 48 像素。这是最基础的图集切法图片横向排列代码按固定步长移动起始点。(frame 1) % 4让帧号在 0、1、2、3、0、1、2、3 之间循环形成走路的循环动画。帧同步的关键是那个 0.12 秒。动画不能用“每帧都切图”来做因为帧率不稳定60Hz 下会切得飞快30Hz 下会显得卡顿。正确做法是累计真实流逝的时间达到 0.12 秒才切到下一帧这就是“时间驱动动画”而不是“帧率驱动动画”。跳跃时这个代码会换成固定的一帧马里奥在空中的姿态不参与走路循环落地后再恢复循环。注意SetTextureRect 修改的是“纹理坐标”也就是从图集哪个区域取图不是角色在屏幕上的位置。经常有人把这俩搞混把屏幕坐标传进去结果画面整个花掉。4.3 纹理引用计数与释放顺序一个隐形内存泄漏源头HGE 的纹理管理用的是引用计数。Texture_Load 加载一张纹理如果这张纹理已经被加载过引用计数加 1返回同一个句柄Texture_Free 释放时引用计数减 1减到 0 才真正从显存里删除。这套机制的好处是同一张贴图被多个 Sprite 共用时不会重复加载坏处是释放顺序错了就会泄漏。void ReleaseTextures() { // 第一步断开 Sprite 对纹理的引用 if (marioSpr) { marioSpr-SetTexture(0); delete marioSpr; marioSpr 0; } // 第二步释放纹理自身引用 if (marioTex) { hge-Texture_Free(marioTex); marioTex 0; } }释放顺序为什么重要Sprite 对象内部持有纹理句柄如果先 Texture_Free 纹理Sprite 还在引用这块已经释放的资源下次调用 Render 时就会访问非法内存轻则花屏重则崩溃。反过来先把 Sprite 的纹理置空、删掉 Sprite再对纹理句柄做 Texture_Free引用计数才能真正减到 0。我拆这套源码时特别注意它处理纹理的方式Sprite 和 Texture 是分开创建、分开释放的。理解这个模型后你以后写自己的 HGE 程序也会自然沿用“先删使用者再释放资源”的顺序。这也是所有基于引用计数的资源系统的通用规则——谁用谁先放手资源最后走。5. 常见问题与避坑编译失败、黑屏运行与速度失控的血泪经验5.1 编译期报错头文件找不到与新工具链的兼容债现象用 VC 6.0 打开工程后按编译报一堆错误最上面一条是fatal error C1083: Cannot open include file: d3dx9.h后面跟着几十个跟 D3D 相关的头文件缺失。原因HGE 引擎底层封装的是 DirectX 8/9hge.h 里直接 include 了 d3d9.h 和 d3dx9.h但 VC6 默认的 include 目录里没有 DirectX SDK 的头文件。编译器不知道上哪儿找这些文件自然一路报错。解决安装 DirectX SDK9.0c 即可然后在 VC6 的 Tools → Options → Directories 里把 SDK 的 Include 和 Lib 目录加到搜索路径最前面确保先找到 SDK 的 d3dx9.h再找 HGE 自己的头文件。装完重启 IDE重新编译。现象有人想用 2022 版 Visual Studio 去打开这份老工程结果报错里混着 LNK2005 重复定义、未定义 WIN32_LEAN_AND_MEAN 之类的问题还有关于运行库部署的提示。原因HGE 1.x 诞生在 VS2003 之前当时的 C 运行时和现在的差异太大。新版编译器的默认宏定义、CRT 函数实现都有变化老年人穿新鞋走两步就崴脚。这不是环境没配好是时代跨度太大。解决老工程用老工具链VC6 或者 VS2003 是最稳的。如果你一定要用新版 VS 编译需要手工加一堆兼容宏、改头文件引用顺序投入产出比极低。我不建议在这上面死磕。同样的道理编译出的 exe 在别的机器上跑VC6 编译产物依赖的是 msvcp60.dll 这类老运行库Win10 上缺失时单独补一下就行跟新版 microsoft visual c redistributable 是两码事。5.2 运行期问题黑屏、花屏与速度失控现象编译成功双击 exe窗口一闪就消失或者一直黑屏。System_Initiate 返回的是 true但画面出不来。原因八成是资源路径问题。cp.bat 没跑或者图片没有复制到 exe 所在目录。HGE 加载纹理默认用相对路径相对的是“当前工作目录”不是“exe 所在目录”。从 IDE 里按 F5 运行时工作目录可能是工程目录图片在线直接双击 exe 时工作目录变成 exe 目录图片不在加载失败返回空句柄Sprite 什么都没有画出来就是黑的。解决先手工确认 exe 同目录下有没有 splash.bmp、pic3.bmp、gameover.bmp、1.5.bmp 这四张图。没有就跑一遍 cp.bat或者把它们复制过去。再严谨一点在代码里把图片路径改成绝对路径做一次验证确认问题出在路径后再回来修批处理。这类问题几乎玄学但十次里有八次是路径。现象游戏能跑了但马里奥的贴图位置不对画出来像一块彩色条纹或者人物边缘有奇怪的错位。原因两个常见来源。一是 SetTextureRect 传了屏幕坐标而不是纹理坐标导致从图集里切图的区域错误。二是 BMP 图片本身的格式问题——HGE 对非 2 次幂尺寸的纹理支持不理想24 位 BMP 的行对齐也可能让切图偏移。解决打印一下当前 Sprite 的 GetTextureRect 返回值和图片实际尺寸对比确认切图区域是否在图集范围内。把图片统一转成 32 位 BMP宽高尽量保持 2 的幂256、512、1024能规避一大半显示异常。改完图片记得重新执行 cp.bat别让旧文件覆盖新文件。现象马里奥的移动速度跟着帧率走。高配机器上跑得飞起低配机器上慢吞吞同一个关卡在不同机器上难度完全不同。原因代码里用了固定步长比如 x 200而不是 x 200 * dt。固定步长意味着每帧移动量固定帧率越高每秒移动次数越多总速度就越快。这是 2D 游戏最经典的帧率耦合问题。解决全局取一次时间增量所有位置更新都乘它float dt hge-Timer_GetDelta(); mario.x mario.speedX * dt; mario.y mario.vy * dt;改完这处马里奥在 30Hz 和 60Hz 下的移动距离就一致了。凡是跟速度、位移相关的更新都必须过一遍 dt。我拿到任何 HGE 工程第一件事就是搜有没有没乘 dt 的更新语句——这一步能省掉后面大量“为什么手感不一样”的排查时间。6. 改造与验证调整手感参数与替换贴图的具体技巧6.1 手感从哪里来重力、初速度与跳跃高度的换算想改马里奥的手感不需要动碰撞逻辑只需要改两个数值重力加速度和跳跃初速度。它们的关系是跳跃最高点的高度 H v² / (2g)v 是起跳瞬间的垂直速度g 是重力加速度。这个公式直接决定马里奥能跳多高也就是能不能跳上砖块、能不能越过坑。参数建议初值说明gGravity1200 px/s²数值越大下落越快手感越“沉”vJump520 px/s配 1200 的重力跳高约 112 px砖块高度64 px标准砖块尺寸112 px 足够跳上一块砖想跳上两块砖128 px根据公式反推v 需要至少 sqrt(2 × 1200 × 128) ≈ 554 px/s。所以你会发现把跳跃初速度从 520 调到 560马里奥就从一个“过一格的矮子”变成“两格跳跃健将”手感差异巨大。改数值时切记动一个参数跑一次测试不要两个一起改否则你根本不知道手感变化来自哪一边。6.2 验证手段System_Log 输出与贴图替换测试改完参数怎么验证用 HGE 自带的 System_Log 往调试文件里打点观察关键变量的实际数值hge-System_Log(mario y%f vy%f state%d, mario.y, mario.vy, g_state);运行几分钟后打开日志文件能看到马里奥在哪个位置起跳、跳到了多高、下落速度是多少。这套打法比我盯着屏幕猜手感靠谱得多。换贴图同理——把 pic3.bmp 替换成你自己的图集时保持四帧的宽高比如依然是每帧 32×48和排列方式代码一行都不用改。替换后第一时间验证站立、走路、跳跃三个状态是否都切到了正确的帧别只盯着走路看。我最初在这套代码上动刀时第一件事就是把所有移动量乘上 dt等跳到参数阶段又因为同时改了重力和初速翻车大半晚上才定位到问题。从那以后每次拿 HGE 工程我都强制走一遍“先跑原版、再改一个参数、每步验证”的流程再也没被奇怪的手感折腾过。希望帮到你。本文还有配套的精品资源点击获取