ARTICLE DETAIL

资讯详情

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

HGE引擎超级玛丽源码实战:Visual C++游戏开发拆解

HGE引擎超级玛丽源码实战:Visual C++游戏开发拆解 简介这份源代码工程将Visual C与开源2D引擎HGE结合完整重现了超级玛丽的核心玩法适合希望学习2D游戏框架搭建、对象管理和事件驱动的开发者研读。压缩包共183个文件除可直接运行的exe和依赖dll外还包含png/bmp/gif图像素材、wav/mod音效、hpp/h头文件、cpp源码以及dat/ent配置实体文件整体约8.93MB目录结构清晰方便对照代码与资源。已有719人学习下载。代码覆盖HGE环境下的游戏主循环、渲染与帧同步、物理碰撞检测、输入响应、音频播放及状态机管理并演示了如何用资源管理器加载和复用图片、声音。对想通过经典马里奥项目掌握DirectX/HGE开发路径的读者而言这是一份不错的实战参照。1. visual c与HGE游戏引擎的超级玛丽源代码一份可以直接拆的2D游戏教材第一次打开这个工程时我习惯性地先把里面的bmp文件看了一遍splash.bmp、gameover.bmp、pic3.bmp再加上1.5.bmp一个经典马里奥游戏的画面闭环已经摆在眼前。这是一套用visual c配合HGE游戏引擎开发的超级玛丽超级马里奥完整源代码核心是用DirectX做2D渲染把游戏循环、输入、碰撞、动画这些最基础也最关键的模块全部落到了能编译的C工程里。它适合两类人一类是刚学完C语法、想搞清楚“游戏到底怎么跑起来”的新手另一类是手上有老VC工程、需要快速理解HGE引擎组织方式的开发者。接下来我从引擎初始化拆到具体逻辑给你一份可以直接照着跑起来的实战笔记。2. 把HGE引擎先跑起来初始化、回调函数与老工程的构建脚本HGEHighly Efficient Game Engine是一款基于DirectX的2D游戏引擎它替你管好了窗口创建、DirectX设备初始化、消息循环和资源加载你只需要回答“每一帧游戏逻辑做什么”和“每一帧画什么”。这套源码的入口不复杂甚至有点复古main函数里几行初始化剩下的逻辑全在回调函数里。2.1 hgeCreate到System_Start引擎初始化这十行不能省先看最典型的HGE入口代码几乎每个HGE工程都是这个结构// main.cpp #include hge.h hge* hge 0; // 引擎全局指针所有接口都通过它调用 bool WINAPI Initialize(); // 初始化游戏资源 bool WINAPI FrameFunc(); // 每帧逻辑回调 bool WINAPI RenderFunc(); // 每帧渲染回调 void WINAPI Cleanup(); // 退出时清理资源 int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) { hge hgeCreate(HGE_VERSION); // 创建引擎实例 // 设置引擎行为窗口模式 800x600分辨率 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); hge-System_SetState(HGE_FRAMEFUNC, FrameFunc); hge-System_SetState(HGE_RENDERFUNC, RenderFunc); hge-System_SetState(HGE_LOGFILE, hge.log); if (hge-System_Initiate()) { // 初始化DirectX Initialize(); // 加载游戏资源 hge-System_Start(); // 进入游戏循环 } else { // 初始化失败日志文件hge.log里会写原因 MessageBoxA(0, Initiate failed, check hge.log, HGE, MB_OK); } Cleanup(); // 清理资源 hge-System_Shutdown(); // 关闭引擎 hge-Release(); // 释放引擎实例 return 0; }HGE_VERSION是引擎版本的宏hgeCreate返回一个全局引擎指针之后所有功能都从hge指针上调。System_SetState是HGE的配置入口参数很多但最关键的就是上面这几个窗口模式、分辨率、标题、逻辑回调、渲染回调和日志路径。这套源码里没有用resource.xml做资源管理而是直接在Initialize里一份份加载位图这是老HGE工程的常见做法。注意System_Initiate内部会创建DirectX设备这一步失败多半是显卡驱动不兼容或者窗口模式设置冲突。System_Start才是真正进入循环的地方它会在内部反复调用你设置好的FrameFunc和RenderFunc直到FrameFunc返回true为止。2.2 FrameFunc与RenderFuncHGE驱动游戏的方式就是反复调两个回调HGE没有暴露完整的游戏循环代码但你可以把FrameFunc理解成“游戏主循环的每一帧”。马里奥的移动、跳跃、敌人AI、碰撞检测全都在这个函数里写。RenderFunc则专门负责绘图。// FrameFunc每帧逻辑 bool WINAPI FrameFunc() { // 按ESC退出游戏 if (hge-Input_KeyDown(HGEK_ESCAPE)) return true; // dt是这一帧经过的秒数所有运动量都乘以它 float dt hge-Timer_GetDelta(); // 更新马里奥速度、位置、碰撞…… UpdateMario(dt); UpdateEnemies(dt); CheckCollisions(); return false; // 返回false继续循环返回true退出 } // RenderFunc每帧渲染 bool WINAPI RenderFunc() { hge-Gfx_BeginScene(); // 开始一帧的绘制 hge-Gfx_Clear(0xFF000000); // 清屏为黑色 DrawBackground(); // 画背景 DrawTiles(); // 画砖块和地面 DrawEnemies(); // 画敌人 DrawPlayer(); // 画马里奥 DrawHUD(); // 画分数和生命 hge-Gfx_EndScene(); // 结束这一帧 return false; }逻辑和渲染分开是HGE的约定不是强制。关键是理解dt的作用HGE默认不锁帧直接跑满显示器刷新率。如果运动量不乘dt同样的移动速度在60Hz和120Hz显示器上就是两倍差距。Timer_GetDelta返回的是上一帧到这一帧的秒数单位是秒所有速度参数都应该按“像素/秒”来设计。2.3 mk.bat与cp.bat旧VC工程靠两个批处理完成构建这套源码里没有.sln解决方案文件构建靠的是mk.bat和cp.bat两个批处理这是当年VC6时代很常见的工程组织方式。echo off rem cp.bat把游戏资源复制到exe输出目录 copy /Y pic3.bmp Release\pic3.bmp copy /Y splash.bmp Release\splash.bmp copy /Y gameover.bmp Release\gameover.bmp copy /Y 1.5.bmp Release\1.5.bmp echo off rem mk.bat用命令行完成编译链接 cl /nologo /GX /O2 /I include main.cpp /link /out:Mario.execp.bat负责把bmp资源拷到Release目录mk.bat用cl命令直接编译。这种脚本最大的坑是工作目录如果你在调试器里F5运行工作目录默认是工程文件所在目录资源能加载到如果直接双击exe工作目录变成exe所在目录资源路径就对不上了。文件常见用途splash.bmp启动/标题画面gameover.bmp游戏结束画面pic3.bmp马里奥、敌人、道具的精灵图集1.5.bmp背景或地形贴图命名风格类似“关卡1.5”cp.bat资源拷贝脚本mk.bat编译链接脚本我一般拿到这类老工程第一步不是打开IDE而是先看资源清单和批处理内容确认哪些bmp是必须存在的。这份源码的资源依赖很朴素没有打包成HAP资源包所以缺一个bmp就会直接导致Texture_Load返回0。3. 把马里奥的移动、跳跃与碰撞写成代码从按键到砖块判定的完整链路这一章是游戏玩法的核心。超级玛丽的玩法本质上是三件事读取输入、更新物理、检测碰撞。HGE提供了干净的输入接口物理和碰撞则需要你按游戏逻辑自己组织。3.1 Input_KeyDown与Input_GetKeyState区分“按下瞬间”和“按住不放”HGE输入系统有两类判断Input_KeyDown判断按键被按下的那一瞬间Input_GetKeyState判断按键当前是否处于按住状态。跳跃用KeyDown左右移动用GetKeyState这是2D平台游戏的基本分工。// 输入与移动控制 void UpdateMario(float dt) { // 左右移动按住才生效 if (hge-Input_GetKeyState(HGEK_LEFT)) { mario.vx -MOVE_SPEED; } else if (hge-Input_GetKeyState(HGEK_RIGHT)) { mario.vx MOVE_SPEED; } else { mario.vx 0.0f; } // 跳跃起跳瞬间判断一次必须在onGround状态 if (hge-Input_KeyDown(HGEK_SPACE) mario.onGround) { mario.vy -JUMP_SPEED; mario.onGround false; // 离开地面禁止二段跳 } // 水平方向先移动再做碰撞修正 mario.x mario.vx * dt; }关键参数MOVE_SPEED是横向移动速度典型值在160到240像素/秒之间太小了马里奥走不动太大了砖块碰撞会穿透。onGround标志位用来限制跳跃次数马里奥只有站在地面上才能起跳这是平台游戏防“空中二段跳”的基本做法。3.2 重力与跳跃参数为什么跳起来的高度刚好是两块砖2D平台游戏的重力实现很简单每帧给垂直速度叠加一个重力加速度再把速度应用到位置。但参数选得不合适跳跃手感会非常奇怪。// 物理参数 const float GRAVITY 1500.0f; // 重力加速度像素/秒^2 const float MOVE_SPEED 180.0f; // 横向移动速度像素/秒 const float JUMP_SPEED 520.0f; // 起跳初速度像素/秒 void UpdatePhysics(float dt) { // 垂直方向积分速度 重力 * 时间 mario.vy GRAVITY * dt; mario.y mario.vy * dt; // 触地判定马里奥底部碰到底面重置onGround if (mario.y MARIO_H groundY) { mario.y groundY - MARIO_H; mario.vy 0.0f; mario.onGround true; } }参数单位常见取值说明GRAVITY像素/秒²1200~1800重力加速度影响下落速度MOVE_SPEED像素/秒160~240横向移动速度JUMP_SPEED像素/秒480~560起跳初速度MARIO_W/H像素24×44碰撞盒宽度和高度跳跃高度的物理公式是 h v² / (2g)。把JUMP_SPEED520、GRAVITY1500代进去520² / (2 × 1500) ≈ 90.1 像素如果一块砖高32像素马里奥跳约2.8块砖高这就是超级玛丽经典的跳跃幅度。调手感时先改JUMP_SPEED和GRAVITY这两个值不要动碰撞盒尺寸这是我调参数的血泪经验。3.3 与瓦片地图的碰撞检测一次只修正一个轴超级玛丽的地图是典型的瓦片地图砖块、地面、管道都铺在一个二维数组上。碰撞检测的核心思路是把马里奥的像素碰撞盒映射到瓦片格子上检查覆盖到的格子是否有砖块。const int TILE_W 32; const int TILE_H 32; const int MAP_W 64; const int MAP_H 15; // 取格子的砖块类型0表示空地非0表示有砖块 int TileAt(int tx, int ty) { if (tx 0 || tx MAP_W) return 1; // 地图边界视为墙 if (ty 0 || ty MAP_H) return 0; return tileMap[ty][tx]; } // 检测马里奥碰撞盒覆盖了哪些格子 bool CheckMarioVsMap() { int tx0 (int)(mario.x / TILE_W); int tx1 (int)((mario.x MARIO_W - 1) / TILE_W); int ty0 (int)(mario.y / TILE_H); int ty1 (int)((mario.y MARIO_H - 1) / TILE_H); for (int ty ty0; ty ty1; ty) { for (int tx tx0; tx tx1; tx) { if (TileAt(tx, ty) ! 0) return true; } } return false; }这段代码的问题是只告诉你“有没有碰上”没告诉你“从哪边碰上的”。实际工程里我一般把碰撞拆成两步先做水平移动检测一次碰撞如果碰撞了把x方向的位置修正回碰撞前同时把vx清零再做垂直移动检测一次修正y。这样能避免一次移动xy两个轴时马里奥被砖块“卡”在墙里的问题。先处理哪个轴取决于游戏逻辑——俯视角游戏通常先处理移动方向平台游戏我习惯先处理垂直方向让下落撞头的判定更准确。4. 精灵、动画与镜头pic3.bmp怎么变成会跑的超级玛丽HGE把2D渲染封装成了精灵Sprite和动画Animation两个类。马里奥的奔跑、跳跃、死亡动作不可能每次重画一张图而是从一张图集里裁出不同区域来切换这也是pic3.bmp存在的意义。4.1 Texture_Load与hgeSprite把位图切成可复用的精灵HGE的典型流程是先用Texture_Load把bmp加载为纹理再用hgeSprite截取纹理的某一块区域作为精灵。// 加载马里奥所在图集 HTEXTURE tex hge-Texture_Load(pic3.bmp); if (tex 0) { // 加载失败多半是文件不存在或工作目录不对 return; } // 从图集(0, 0)位置截取32x48区域作为站立帧 hgeSprite* marioIdle new hgeSprite(tex, 0, 0, 32, 48); // 从图集(32, 0)位置截取另一块作为行走第一帧 hgeSprite* marioWalk1 new hgeSprite(tex, 32, 0, 32, 48);hgeSprite构造函数的四个参数是图集坐标和宽高(tx, ty, w, h)单位是像素。关键是把图集坐标量准截歪了会出现马里奥脑袋上顶着一块砖的诡异画面。我给位图做精灵时一般会在测试里直接渲染一个带边框的调试精灵确认裁剪区域正确再做动画。4.2 hgeAnimation帧动画FPS参数决定走路节奏逐帧手写切换太麻烦HGE提供了现成的hgeAnimation类你只需要指定帧数、帧率和回放模式。// 8帧行走动画12帧/秒循环播放 hgeAnimation* walkAnim new hgeAnimation( tex, // 纹理 8, // 帧数 12.0f, // 播放帧率FPS 0, 48, // 起始坐标 (x, y) 32, 48, // 每帧宽高 HGEANIM_LOOP // 循环模式 ); walkAnim-SetMode(HGEANIM_LOOP); // 循环播放行走动画 // 每帧在FrameFunc里更新动画 void UpdateMario(float dt) { if (mario.vx ! 0.0f) { walkAnim-Update(dt); // 走路时播放动画 } else { walkAnim-SetFrame(0); // 停下时回到站立帧 } }hgeAnimation的参数顺序容易记混纹理、帧数、播放速度、起始坐标、宽高、模式。SetFrame(0)是停住动画的常用手段。播放速度12FPS意味着每1000/12毫秒切换一帧这个值决定马里奥走路的急切程度过快看起来像在跑过慢像在滑冰。4.3 渲染顺序与摄像机偏移为什么马里奥永远在画面中间渲染顺序决定遮挡关系2D游戏的铁律是先画背景再画地面和砖块然后画敌人最后画玩家。如果顺序颠倒马里奥会被砖块盖住。bool WINAPI RenderFunc() { hge-Gfx_BeginScene(); hge-Gfx_Clear(0xFF1E90FF); // 天空蓝 // 摄像机偏移让马里奥保持在画面中心 float cameraX mario.x - SCREEN_W / 2; if (cameraX 0) cameraX 0; float maxX MAP_W * TILE_W - SCREEN_W; if (cameraX maxX) cameraX maxX; // 按顺序绘制 DrawGround(cameraX); DrawTiles(cameraX); DrawEnemies(cameraX); walkAnim-Render(mario.x - cameraX, mario.y - cameraY); DrawHUD(); hge-Gfx_EndScene(); return false; }摄像机偏移的实现很简单没有真正的摄像机只是把所有物体的渲染坐标都减去同一个偏移量cameraX。马里奥位置减去偏移后正好等于屏幕中心看起来就是摄像机在跟着他走。关卡边界要限制偏移范围否则开局和通关时画面会越过地图边缘露出黑色背景。这份源码里应该也有类似的限制逻辑没有的话加上这个判断基本就不会穿帮。5. 避坑重新编译这套超级玛丽源码时最容易翻车的五个位置老HGE工程在今天的Visual Studio上重编问题比新工程多得多。下面是几个高频翻车点每一条都是能编译通过但跑不起来或者资源加载失败的典型场景。5.1 现象hge.h和hge.lib无法找到编译器报fatal error C1083: Cannot open include file: hge.h: No such file or directory。链接器报无法打开hge.lib。原因HGE引擎的头文件和库文件没有加入编译器搜索目录。老工程通常用相对路径引用引擎包一旦换目录就找不到。解决拿到引擎包后先把include和lib目录的绝对路径记下来在项目属性里配置两个位置C/C → 常规 → 附加包含目录填include路径链接器 → 常规 → 附加库目录填lib路径。然后把hge.dll复制到exe同级目录。HGE老库多为32位项目平台要选Win32不要用x64。5.2 现象编译通过运行后黑屏闪退没有任何提示启动后窗口一闪而过或者一直是黑屏。原因HGE初始化DirectX设备失败。这是HGE最典型的故障但它有一个好习惯——会把失败原因写进日志文件。解决检查代码里有没有System_SetState(HGE_LOGFILE, hge.log)没有就加上。编译运行后在exe目录打开hge.log看到类似“Direct3D init failed”的提示基本可以锁定是窗口模式、分辨率与显卡驱动的兼容性问题。我把窗口模式改为true分辨率降为800x600黑屏问题基本就解决大半。5.3 现象资源文件都在但马里奥显示成黑块或透明方块程序能跑画面也在但玩家角色和敌人全是黑色方块或者看不见。原因Texture_Load返回了0纹理加载失败。最常见的是工作目录问题调试器默认工作目录是.vcproj所在目录而你双击exe运行时工作目录是exe所在目录。bmp放在exe目录旁F5运行时就会找不到。解决先在初始化代码里写一行if (tex 0) { hge-System_Log(load pic3.bmp failed: %s, GetLastErrorText()); }然后用绝对路径测一次。如果绝对路径正常就在项目属性 → 调试 → 工作目录设置为exe输出目录。老代码里路径有中文也是这个症状bmp文件路径尽量全英文。5.4 现象跳跃高一次矮一次手感忽轻忽重同样的按键操作有时候跳得老高有时候贴地就落下来像中了邪。原因FrameFunc里直接用固定速度累加或者用了Timer_GetDelta但没有正确应用到每个运动量上。HGE默认不锁帧在刷新率高的显示器上每帧时间变短固定的位移值会让整体速度变快而跳跃是基于瞬间速度的表现就时高时低。解决把HGE_FPS设为60统一所有运动参数为“每秒”单位并在每个位置更新语句里乘以dtmario.x mario.vx * dt; mario.y mario.vy * dt;这种帧率问题最容易看成玄学实际上就是dt没有乘或者乘得不全。某帧漏了dt那一帧的移动量就瞬间翻倍。5.5 现象换电脑运行提示缺少MSVCP140.dll或MSVCR110.dll双击exe弹出“无法启动此程序因为计算机中丢失MSVCP140.dll”。原因工程编译时链接的是VC运行库动态库目标机器上没有对应的运行库版本。MSVCP140对应VS2015到2022的Redistributable老工程里有时还会混着2010、2013的库。解决在编译配置里选择“多线程(/MT)”静态链接运行库入口在项目属性 → C/C → 代码生成 → 运行库把“多线程DLL(/MD)”改为“多线程(/MT)”生成的exe就不依赖外部运行库了。如果只改一两个配置直接安装对应的Microsoft Visual C Redistributable软件包也能解决但分发给别人时不如静态链接一劳永逸。6. 从跑通到改造给这份超级玛丽加一个无敌星星跑通这份源码只是第一步真正的学习价值在于改造。这里演示一个最简单的功能扩展吃星星后15秒无敌期间碰到敌人直接把敌人顶飞。6.1 定义一个15秒的invincible状态核心思路是在马里奥状态结构里加两个字段然后在更新函数里做倒计时// 状态结构体中新增字段 struct PlayerState { // ……原有字段…… bool invincible; // 无敌状态标记 float invincibleTimer; // 剩余无敌时间 }; void UpdatePowerUp(float dt) { if (player.invincible) { player.invincibleTimer - dt; if (player.invincibleTimer 0.0f) { player.invincible false; // 时间到无敌结束 } } } // 碰撞敌人时判断 bool HitEnemy(Enemy* enemy) { if (player.invincible) { enemy-vy -400.0f; // 顶飞敌人 enemy-dead true; return true; // 无敌不掉血 } return false; // 普通状态走死亡逻辑 } // 渲染时用颜色闪烁提示剩余时间 void RenderPlayer() { if (player.invincible ((int)(player.invincibleTimer * 8) 1)) { marioSprite-SetColor(0xFFFFAA00); // 橙色闪烁 } else { marioSprite-SetColor(0xFFFFFFFF); // 正常颜色 } marioSprite-Render(player.x - cameraX, player.y); }关键参数是invincibleTimer的单位。它与dt保持一致用秒做单位所以赋值是15.0f而不是15。闪烁频率由乘以8再按位与1控制等价于每0.125秒切换一次颜色视觉上就是快速闪烁的效果比一直高亮更能提示“马上要失效了”。敌人被顶飞的-400.0f是垂直初速度配合重力能产生一个漂亮的抛物线掉落到屏幕外。6.2 用日志输出验证改造我改完这种功能的第一件事不是进关卡找星星吃而是加日志确认状态切换正确hge-System_Log(invincible%d timer%.2f pos(%.1f,%.1f), player.invincible, player.invincibleTimer, player.x, player.y);System_Log会把文本追加到hge.log里。跑一遍确认三件事吃到星星后invincible变为115秒后自动变回0碰到敌人时没有走死亡流程。这套验证流程比肉眼盯着屏幕靠谱得多尤其是timer这类秒级数据靠看画面判断误差太大。话说回来我调试这份源码时最深刻的教训不是引擎接口有多难记而是老工程里那些看起来多余的bat脚本和bmp文件恰恰是工程能跑起来的前提。从那以后我每次拿到老游戏源码都强制自己先把资源清单和构建脚本完整过一遍再动代码编辑器。这份超级玛丽源码不多但足够把HGE引擎这套“初始化 → 回调 → 资源 → 逻辑”的套路走通你下载后值得完整跑一遍看看到底哪里会翻车。希望帮到你。本文还有配套的精品资源点击获取
返回列表