ARTICLE DETAIL

资讯详情

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

C++与EasyX实战:超级马里奥游戏源码解析与改造

C++与EasyX实战:超级马里奥游戏源码解析与改造 简介这是一份面向 C 初学者与游戏开发爱好者的实战型源码资源基于 C 与 EasyX 图形库对经典《超级马里奥》进行还原仿制涵盖移动、跳跃、加速发射火球、下蹲钻管道等核心玩法并已完整实现 1-1、1-2、1-3 三个关卡适合作为图形编程入门、课程设计或毕业设计的参考项目。压缩包共 239 个文件约 10.6MB其中 163 个 png 与 25 个 mp3 构成角色、场景贴图与音效素材21 个 h 头文件与 21 个 cpp 源文件按马里奥、怪物、砖块、道具、平台、事件等模块拆分另含 ico、wav、sln、vcxproj 等工程与资源文件结构清晰便于二次开发。目前已有 567 人学习下载。读者可借此理解 EasyX 绘图、碰撞检测、关卡数据组织与游戏主循环的实现思路并参考 Visual Studio 2022 与 EasyX_20220901 的编译环境快速跑通项目是学习 C 小游戏开发的实用范例。1. 从一份 C 源码包说起EasyX 还原超级马里奥到底能跑成什么样很多人第一次搜「C 小游戏」的时候脑子里想的是控制台里那个黑框框里跑贪吃蛇但真正能让人坐下来玩十分钟的往往是带图形界面的东西。这份基于 C 与 EasyX 图形库的超级马里奥仿制源码就是那种「打开 Visual Studio 按一下 F5 就能看到马里奥站在地面上」的项目。它把 1-1、1-2、1-3 三个关卡完整做出来了移动、跳跃、加速、发射火球、下蹲钻管道这些核心操作都有对应实现通关 1-3 之后程序自动关闭是设计好的行为不是崩溃。适合谁适合刚学完 C 基础语法、想找一个能跑起来、能改、能拆的图形项目练手的人也适合想看看一个 2D 平台跳跃游戏在 EasyX 这套 API 下怎么组织代码结构的人。源码包里的文件命名很直白——mario.cpp、monster.cpp、block.cpp、wall.cpp、prop.cpp、platform.cpp、gamescene.cpp、event.cpp、image.cpp、check.cpp——基本上一眼就能看出每个文件负责什么。接下来我不打算讲「C 多重要」这种废话直接拆这份源码怎么用、参数怎么调、哪里容易翻车。2. 编译环境与工程结构把 VS2022 和 EasyX 配到能出画面2.1 为什么是 VS2022 EasyX_20220901 这个组合EasyX 是一个面向 C 的图形库本质上是把 Windows GDI 封装成了一套类似 TC 风格的绘图接口画圆、画线、贴图、读键盘都只需要一行调用。它最大的好处是上手门槛低不需要配 OpenGL、不需要装 SDL、不需要理解渲染管线initgraph一调窗口就出来了。但它的代价也很明确只支持 Windows只支持 Visual Studio 系列编译器或者兼容 MSVC 的环境而且不同版本的 EasyX 对 VS 版本有对应关系。这份源码标注的编译环境是 Microsoft Visual Studio 2022 加 EasyX_20220901这个组合是经过验证能直接编译通过的。如果你用 VS2019 或者更老的版本EasyX 的头文件和库文件路径可能对不上会出现graphics.h找不到或者链接阶段报unresolved external symbol的情况。常见做法是去 EasyX 官网下载对应 VS 版本的安装包安装时会自动把graphics.h、easyx.h和EasyXa.lib、EasyXw.lib塞进 VS 的 include 和 lib 目录省得手动配。2.2 工程目录里每个 .cpp 到底管什么拿到源码包之后别急着编译先花五分钟把文件结构看一遍后面改代码的时候能省很多时间。下面这张表是我拆完之后整理的职责划分不是官方文档是我自己读代码时的理解文件名职责关键点check.cpp碰撞检测马里奥与砖块、怪物、管道的矩形相交判断event.cpp输入事件键盘按键映射到移动、跳跃、火球、下蹲block.cpp砖块逻辑可顶碎砖、问号砖、金币砖的状态切换gamescene.cpp场景管理关卡加载、摄像机滚动、游戏主循环mario.cpp马里奥实体状态机站立/跑/跳/蹲/火球形态monster.cpp怪物 AI板栗仔、乌龟的移动与踩踏判定wall.cpp墙体与边界不可穿越的地形碰撞prop.cpp道具蘑菇、火之花、金币的生成与拾取image.cpp图像资源贴图加载、精灵图切割、透明色处理platform.cpp平台可站立平台与移动平台这个划分方式在小型游戏项目里算比较清晰的每个文件对应一类游戏对象改马里奥的跳跃手感就去 mario.cpp改怪物巡逻逻辑就去 monster.cpp不会牵一发动全身。如果你之前写代码习惯把所有东西塞一个 main.cpp 里这份源码的结构值得参考。2.3 从零到出画面的完整步骤假设你机器上已经装好了 VS2022下面是让项目跑起来的具体操作# 第一步确认 EasyX 已安装 # 打开 VS2022新建一个空项目尝试编译下面这段最小代码#include graphics.h // EasyX 核心头文件 #include conio.h int main() { initgraph(640, 480); // 创建 640x480 的绘图窗口 setbkcolor(WHITE); // 设置背景色为白色 cleardevice(); // 用背景色清屏 setfillcolor(RED); // 设置填充色为红色 fillcircle(320, 240, 50); // 在窗口中心画一个半径 50 的红圆 _getch(); // 等待按键防止窗口一闪而过 closegraph(); // 关闭绘图窗口 return 0; }这段代码的作用是验证 EasyX 环境是否正常。initgraph(640, 480)创建窗口两个参数分别是宽和高单位是像素。setbkcolor和cleardevice配合使用才能让背景色生效只调setbkcolor不调cleardevice是看不到变化的这是新手最容易踩的一个坑。fillcircle的第三个参数是半径。_getch()来自conio.h作用是阻塞程序等待你按任意键不加这行窗口会瞬间关闭。如果这段代码能编译运行并看到红圆说明 EasyX 装好了可以打开源码包里的工程文件了。# 第二步打开源码工程 # 解压源码包找到 .sln 文件双击用 VS2022 打开 # 如果提示重定向项目选择确定即可 # 第三步检查字符集 # 右键项目 - 属性 - 高级 - 字符集确认是使用多字节字符集 # EasyX 的某些版本对 Unicode 支持不完整用多字节字符集更稳 # 第四步编译运行 # 按 CtrlF5不调试运行避免调试模式下某些断点干扰字符集这一步很多人会忽略。VS2022 新建项目默认是 Unicode 字符集而这份源码如果是在多字节字符集下写的直接编译可能会在字符串处理的地方报错或者出现乱码。改成多字节字符集之后char*和const char*的转换问题会少很多。如果编译时提示无法打开源文件 graphics.h说明 EasyX 没装或者装到了别的 VS 版本目录下重新运行 EasyX 安装程序选择 VS2022 对应的选项即可。3. 核心玩法实现拆解马里奥的状态机、碰撞检测与关卡数据3.1 马里奥状态机在 mario.cpp 里怎么写的马里奥这个角色看起来简单实际上状态不少小马里奥、大马里奥、火球马里奥每种形态又有站立、跑动、跳跃、下蹲、死亡这几个动作状态。如果不用状态机代码里会到处是if (isJumping !isCrouching hasFireFlower)这种条件判断改一个地方崩三个地方。这份源码在 mario.cpp 里用枚举加 switch 的方式做了状态管理大致结构是这样的// mario.cpp 中的状态定义根据源码逻辑还原 enum MarioState { STATE_SMALL_STAND, // 小马里奥站立 STATE_SMALL_RUN, // 小马里奥跑动 STATE_SMALL_JUMP, // 小马里奥跳跃 STATE_BIG_STAND, // 大马里奥站立 STATE_BIG_RUN, // 大马里奥跑动 STATE_BIG_JUMP, // 大马里奥跳跃 STATE_FIRE_STAND, // 火球马里奥站立 STATE_FIRE_RUN, // 火球马里奥跑动 STATE_FIRE_JUMP, // 火球马里奥跳跃 STATE_CROUCH, // 下蹲 STATE_DEAD // 死亡 }; void Mario::update() { switch (currentState) { case STATE_SMALL_STAND: // 检测移动键切换到 RUN 状态 if (isKeyPressed(A) || isKeyPressed(D)) { currentState STATE_SMALL_RUN; } // 检测跳跃键切换到 JUMP 状态 if (isKeyPressed(K)) { vy -JUMP_SPEED; // 给一个向上的初速度 currentState STATE_SMALL_JUMP; } break; case STATE_SMALL_JUMP: vy GRAVITY; // 每帧施加重力 y vy; // 更新垂直位置 if (y groundLevel) { // 落地检测 y groundLevel; vy 0; currentState STATE_SMALL_STAND; } break; // ... 其他状态类似 } }JUMP_SPEED和GRAVITY是两个关键参数。JUMP_SPEED决定跳跃初速度值越大跳得越高GRAVITY决定下落加速度值越大下落越快、手感越重。这两个值在源码里应该是宏定义或者常量调它们就能改跳跃手感。我一般会把JUMP_SPEED设在 10 到 15 之间GRAVITY设在 1 到 2 之间具体看窗口尺寸和地面高度。如果跳跃之后马里奥在空中飘太久说明GRAVITY太小如果跳起来还没到砖块就掉下来了说明JUMP_SPEED不够。3.2 碰撞检测check.cpp 里的矩形相交与方向判定2D 平台游戏的碰撞检测核心就是矩形相交判断。马里奥是一个矩形砖块是一个矩形怪物是一个矩形两个矩形在 x 轴和 y 轴上的投影都有重叠时就算碰撞。check.cpp 里应该封装了类似这样的函数// check.cpp 中的碰撞检测逻辑 struct Rect { int x, y; // 左上角坐标 int w, h; // 宽度和高度 }; bool isColliding(const Rect a, const Rect b) { // 两个矩形在 x 轴和 y 轴上的投影都重叠才算碰撞 return (a.x b.x b.w // a 的左边界在 b 的右边界左侧 a.x a.w b.x // a 的右边界在 b 的左边界右侧 a.y b.y b.h // a 的上边界在 b 的下边界上方 a.y a.h b.y); // a 的下边界在 b 的上边界下方 }这个函数只告诉你碰了但游戏里还需要知道从哪个方向碰的。比如马里奥从下方顶砖块砖块要碎从上方踩怪物怪物要死从侧面撞怪物马里奥要受伤。所以实际代码里会在isColliding返回 true 之后再比较两个矩形的重叠深度判断哪个轴上的重叠更小就认为碰撞发生在那个轴上。常见做法是// 判断碰撞方向简化版 int overlapX min(a.x a.w, b.x b.w) - max(a.x, b.x); int overlapY min(a.y a.h, b.y b.h) - max(a.y, b.y); if (overlapX overlapY) { // 水平方向碰撞马里奥从侧面撞到东西 if (a.x b.x) { // 马里奥在左侧向右移动被挡住 a.x b.x - a.w; } else { // 马里奥在右侧向左移动被挡住 a.x b.x b.w; } } else { // 垂直方向碰撞马里奥从上方或下方撞到东西 if (a.y b.y) { // 马里奥在上方踩到了东西 a.y b.y - a.h; vy 0; // 落地垂直速度归零 isOnGround true; } else { // 马里奥在下方顶到了东西 a.y b.y b.h; vy 0; // 顶头垂直速度归零 } }这段逻辑是平台游戏里最核心的部分之一。overlapX和overlapY分别表示两个矩形在水平和垂直方向上的重叠像素数哪个小就说明碰撞主要发生在哪个方向。判断完之后把马里奥的位置推到刚好不重叠的地方避免出现卡在砖块里的情况。如果这部分写错了典型症状是马里奥贴着墙走的时候会抖动或者跳到砖块上方时直接穿过去。3.3 关卡数据怎么组织1-1、1-2、1-3 的加载方式三个关卡的数据不太可能硬编码在 gamescene.cpp 里常见做法是用二维数组或者外部文件来描述地图。考虑到这份源码是教学向的用二维数组的可能性更大。比如用数字代表不同的地形// 关卡地图数据示例根据源码逻辑还原 // 0空 1地面 2砖块 3问号砖 4管道 5怪物生成点 int level1_1[15][200] { {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, /* ... */}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, /* ... */}, // ... 中间省略 {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, /* ... */} // 最底下一行是地面 };数组的第一个维度是行数垂直方向第二个维度是列数水平方向。每个数字对应一种地形类型加载关卡的时候遍历这个数组根据数字在对应位置创建砖块、管道、怪物生成点。这种方式的优点是直观改关卡就是改数字缺点是关卡大了之后数组会很长而且不方便做可视化编辑。如果你想把三个关卡改成四个就在 gamescene.cpp 里找到关卡切换的逻辑加一个level1_4数组然后在通关判断里把1-3改成1-4。注意通关 1-3 后程序自动关闭是源码里写好的行为如果你不想让它关闭找到exit(0)或者closegraph()那行注释掉就行。4. 避坑与排查编译报错、画面异常、手感不对的常见原因4.1 编译时报无法打开源文件 graphics.h现象VS2022 里打开工程编译直接报fatal error C1083: 无法打开源文件 graphics.h。原因EasyX 没有安装或者安装到了别的 VS 版本目录下。EasyX 的安装程序会检测你机器上的 VS 版本如果你装了多个版本可能装到了 VS2019 的目录里而你现在用的是 VS2022。解决重新运行 EasyX_20220901 安装程序在选择 VS 版本的界面勾选 VS2022。如果安装程序没有检测到 VS2022可以手动把 EasyX 安装目录下的include文件夹里的graphics.h和easyx.h复制到 VS2022 的VC\Tools\MSVC\版本号\include目录下把lib文件夹里的EasyXa.lib和EasyXw.lib复制到对应的lib\x86或lib\x64目录下。复制完之后在项目属性里确认附加包含目录和附加库目录是否包含这些路径。4.2 编译通过但运行时报unresolved external symbol现象代码没有语法错误但链接阶段报一堆unresolved external symbol通常和initgraph、closegraph、fillcircle这些 EasyX 函数相关。原因项目属性里的字符集和 EasyX 库的字符集不匹配。EasyX 提供了两个版本的库EasyXa.lib对应多字节字符集EasyXw.lib对应 Unicode 字符集。如果你的项目是 Unicode 但链接的是EasyXa.lib就会找不到符号。解决右键项目 - 属性 - 高级 - 字符集改成使用多字节字符集然后重新编译。如果改成多字节之后出现字符串相关的编译错误检查代码里有没有用L...这种宽字符串字面量有的话改成普通字符串。4.3 画面闪烁严重或者贴图显示为黑色方块现象游戏能跑但画面一直在闪或者马里奥、砖块的贴图显示成黑色方块。原因画面闪烁通常是因为每帧都调用了cleardevice()清屏但绘制和清屏之间没有做双缓冲。EasyX 默认是在窗口上直接绘制清屏和绘制交替进行就会闪。贴图变黑方块通常是图片加载失败或者透明色设置不对。解决双缓冲用BeginBatchDraw()和EndBatchDraw()包住绘制代码或者用SetWorkingImage在内存里画好再一次性输出。贴图问题检查loadimage的路径是否正确以及putimage的透明参数有没有传对。EasyX 的loadimage支持从文件加载也支持从资源加载如果图片路径里有中文建议改成英文路径避免编码问题。4.4 马里奥跳跃手感发飘或者太重现象跳跃之后在空中停留时间太长或者跳起来很快就掉下来操作起来不舒服。原因JUMP_SPEED和GRAVITY两个参数不匹配。跳跃初速度决定最高点重力加速度决定下落速度。如果JUMP_SPEED大但GRAVITY小就会飘如果JUMP_SPEED小但GRAVITY大就会感觉很重。解决先调JUMP_SPEED让马里奥能跳到预期高度再调GRAVITY让下落速度符合预期。我一般会先把GRAVITY固定在一个值然后只调JUMP_SPEED直到跳跃高度合适再微调GRAVITY让下落不拖沓。另外注意帧率如果游戏主循环没有做帧率控制GRAVITY每帧加一次帧率越高下落越快手感会随机器性能变化。常见做法是用Sleep(16)或者getTickCount做固定时间步长。4.5 按键没反应或者按键映射和说明不一致现象按 A/D 马里奥不动或者按 K 不跳但源码说明里写的是这些键。原因EasyX 的键盘检测有两种方式一种是_getch()阻塞式读取一种是GetAsyncKeyState或者peekmessage非阻塞式读取。游戏里通常用非阻塞方式每帧检测按键状态。如果 event.cpp 里用的是_getch()那必须按一下键程序才会继续和游戏循环冲突。解决检查 event.cpp 里的按键读取方式确认用的是GetAsyncKeyState或者 EasyX 封装的peekmessage。另外注意大小写GetAsyncKeyState(A)和GetAsyncKeyState(a)在 Windows 下是等价的但如果你用的是_getch()返回值判断大写和小写是不同的。源码说明里写的是 a/d/k/j/s对应小写但实际检测时通常用大写虚拟键码。5. 进阶改造把三个关卡扩成五个、加个计时器、换个贴图风格5.1 加关卡不是复制数组那么简单想把 1-1、1-2、1-3 扩成五个关卡最直接的做法是复制一份关卡数组改个名字然后在 gamescene.cpp 的关卡切换逻辑里加分支。但有几个地方容易漏第一关卡结束的判定条件通常是马里奥走到某个 x 坐标或者碰到旗杆新关卡要加对应的判定点第二关卡切换时的资源释放上一关的砖块、怪物、道具对象要清理掉不然内存会越吃越多第三通关 1-3 后程序自动关闭的逻辑要改成跳转到 1-4否则你加了关卡也进不去。我一般会在 gamescene.cpp 里维护一个currentLevel变量关卡切换时先调clearLevel()释放当前关卡所有对象再调loadLevel(currentLevel)加载新关卡。5.2 加计时器从 getTickCount 到显示在画面上超级马里奥原版是有时间限制的这份源码里可能没有做加一个计时器是个不错的练手改造。核心思路是用GetTickCount()获取系统启动以来的毫秒数在关卡开始时记录一个起始值每帧计算差值换算成秒数显示在窗口角落。// 计时器实现示例 DWORD startTime GetTickCount(); // 关卡开始时记录 int timeLimit 300; // 限时 300 秒 void updateTimer() { DWORD elapsed (GetTickCount() - startTime) / 1000; // 换算成秒 int remaining timeLimit - elapsed; // 剩余时间 if (remaining 0) { // 时间到马里奥死亡 mario.die(); } // 在窗口左上角显示剩余时间 char buffer[32]; sprintf_s(buffer, TIME: %d, remaining); settextcolor(WHITE); outtextxy(20, 20, buffer); // EasyX 的文字输出函数 }GetTickCount()返回的是DWORD类型单位毫秒大约 49 天会溢出一次对于游戏来说完全够用。outtextxy是 EasyX 在指定坐标输出文字的函数配合settextcolor设置文字颜色。注意sprintf_s是安全版本的字符串格式化VS2022 里用sprintf会报安全警告。如果你想让计时器在暂停时停止就在暂停时记录一下已经过去的时间恢复时把startTime往后推相应的毫秒数。5.3 换贴图精灵图切割与透明色处理源码里的贴图大概率是一张精灵图sprite sheet所有角色和地形的图案拼在一张大图上通过指定坐标和宽高来截取。换贴图风格的话你需要准备一张同样布局的精灵图或者改切割参数。EasyX 里加载和显示精灵图的典型流程是// 加载精灵图并截取指定区域 IMAGE spriteSheet; loadimage(spriteSheet, _T(res/mario.png)); // 加载图片 // 从精灵图中截取马里奥站立帧假设在左上角 32x32 区域 IMAGE marioStand; SetWorkingImage(spriteSheet); // 把精灵图设为工作区 getimage(marioStand, 0, 0, 32, 32); // 截取 0,0 开始的 32x32 区域 SetWorkingImage(NULL); // 恢复默认工作区 // 绘制到窗口指定透明色 putimage(x, y, marioStand, SRCAND); // SRCAND 用于透明贴图SetWorkingImage的作用是切换绘图目标默认是窗口切换成IMAGE对象后getimage就是从这张图上截取。putimage的第四个参数是光栅操作码SRCAND和SRCPAINT配合使用可以实现透明效果具体用法是先SRCAND画一次遮罩再SRCPAINT画一次原图。如果嫌麻烦EasyX 也支持loadimage直接加载带透明通道的 PNG然后用putimage的SRCAND模式绘制。换贴图的时候注意尺寸要和原来的对齐否则碰撞检测的矩形对不上会出现看起来没碰到但判定碰撞的玄学问题。5.4 验证改造是否成功的几个检查点改完之后别急着说好了按下面几个点过一遍第一编译有没有新增警告尤其是类型转换和未初始化变量第二运行之后三个原有关卡能不能正常通关别改新功能把旧功能改崩了第三新加的关卡能不能从上一关正常跳转进去跳转时有没有残留对象第四计时器在暂停和恢复时行为是否正确第五换贴图之后碰撞检测有没有偏移马里奥站在地面上时脚底和地面是否贴合。我自己的习惯是每次改完都从 1-1 开始完整玩一遍到新加的关卡虽然费时间但比事后查 bug 省事。从那以后我每次改游戏逻辑都强制走一遍完整流程希望帮到你。本文还有配套的精品资源点击获取
返回列表