拓冰建站拓冰建站
首页 / 资讯中心 / 正文

用C++和EasyX仿制超级马里奥:完整解析2D游戏开发核心机制

简介这是一份基于C语言与EasyX图形库对经典《超级马里奥》进行还原仿制的完整项目源码包适合正在学习游戏开发、图形编程或希望模仿经典游戏玩法的初学者和爱好者。项目已实现1-1、1-2、1-3三个完整关卡涵盖移动、跳跃、加速/发射火球、下蹲/钻管道等核心操作并配有项目说明文档可直接在Visual Studio 2022与EasyX_20220901环境下编译运行。压缩包共239个文件约10.6MB素材与代码划分明确163个PNG图片用于角色、场景和道具25个MP3与1个WAV提供背景音乐和音效21个C头文件与21个源文件构成源码主体另有ICO图标、过滤器与工程配置文件方便按模块阅读。源码按事件处理、关卡场景、角色控制、怪物互动、墙体与道具等模块组织配合说明文档可快速理解EasyX绘图与游戏循环逻辑。目前已有567人下载学习适合作为入门游戏开发、理解像素级游戏还原的实践参考。1. 用 C 和 EasyX 仿制超级马里奥值得动手做一次用 C 和 EasyX 图形库仿制超级马里奥看起来像课程设计真正跑起来你会发现它比多数 c 小游戏 demo 更接近真实游戏项目。这个源码已经完整做出 1-1、1-2、1-3 三个关卡包含马里奥的移动、跳跃、加速、发射火球、下蹲、钻入管道等基础操作怪物与道具也拆成了独立模块而不是堆在单个文件里的玩具代码。对刚结束 C 语法学习、想找项目巩固类设计和 STL 用法的开发者来说这是一个能直接编译运行的 c 游戏源码对想了解 2D 平台游戏底层逻辑的人它把 EasyX 渲染、碰撞检测和关卡数据解析串成完整链路。下面按渲染循环、对象设计、碰撞处理、怪物与关卡配置、编译排错的顺序拆开讲并给出可照着修改的代码。2. EasyX 渲染循环与双缓冲绘图机制2.1 选型为什么这个项目不用 SDL2 而是 EasyXC 做 2D 游戏最常被推荐的是 SDL2 和 SFML但它们都需要先处理窗口创建、事件循环、渲染上下文对一个目标是还原红白机画面的练习项目来说这些前置代码反而会淹没游戏逻辑。EasyX 的定位是给 C/C 做教学用的图形接口它把 Win32 GDI 封装成类似早期 Turbo C 的 API画点、画矩形、贴位图都只要一条函数。从源码里的 image.cpp、platform.cpp 文件结构也能看出来作者想要的是“快速搭画面、慢慢调玩法”。EasyX 也不是没有代价它不跨平台只能在 Windows 下用 MSVC 编译没有场景图也没有自动脏矩形重绘。不过这两个缺点对这个项目不构成阻碍。关卡是固定背景的 2D 滚屏绘制顺序固定逻辑更新频率用 Sleep 控制就足够。对 C 学习者来说用 EasyX 可以把注意力放在“游戏怎么组织”而不是“渲染管线怎么调”。2.2 主循环骨架输入、更新、绘制、等待查看 main.cpp 或场景刷新函数看到的通常是下面这段逻辑#include graphics.h #include conio.h int main() { const int SCREEN_W 800; const int SCREEN_H 600; initgraph(SCREEN_W, SCREEN_H); BeginBatchDraw(); // 开启后台缓冲画布 while (true) { // 输入读取0x8000 表示当前帧按键被按住 if (GetAsyncKeyState(A) 0x8000) mario-MoveLeft(); // 逻辑更新按 16ms 逻辑帧推进 scene-Update(16); // 绘图严格按远近顺序 scene-Draw(); // 将后台画布一次性提交到屏幕 FlushBatchDraw(); // 简单限帧实际项目中建议替换为高精度定时器 Sleep(16); } closegraph(); return 0; }GetAsyncKeyState 与 _getch 的最大区别是它返回按键的“当前状态”而不是“是否发生过输入”。这意味着按住 A 键可以连续向左走不需要自己维护上一次按键的标记。对平台跳跃游戏这是必须的行为。0x8000 是掩码值代表高位 15 位置 1也就是按键处于按下状态。Sleep(16) 在普通桌面机上能把画面稳定在 60 FPS 附近但注意它让出 CPU 的最小粒度是系统时钟周期通常 15.6ms所以实际帧间隔可能在 16ms 到 31ms 之间抖动。手感要求高时应该改用 QueryPerformanceCounter 做高精度等待。这个项目采用 16ms 固定逻辑步长配合双缓冲即便绘制偶尔掉帧物理运算也不会出现突变。2.3 双缓冲BeginBatchDraw 到底解决了什么如果不做批量绘制每画一个矩形就交给 GDI 输出一次显卡和显示器之间会产生大量零碎的写入。在 CRT 时代这会造成闪烁刷新率与绘制速率不同步前一帧没画完后一帧就开始覆盖。BeginBatchDraw 会创建一个与屏幕尺寸相同的内存位图作为后台画布之后的 putimage、rectangle、fillrectangle 都先画在这块内存里直到 FlushBatchDraw 才整体复制到前台。用户只看到一次完整的帧切换这就是“双缓冲”。在 EasyX 里这个机制还有隐藏好处GDI 对象的创建开销很大批量模式下可以复用大量绘图调用。像关卡里几百个砖块如果每个都用独立 putimage性能会立刻劣化。更优做法是把所有静态地形先组合成一张背景位图绘制时一次 putimage 即可。platform.cpp 的作用往往就是这个——把地形批量光栅化到一个 IMAGE 对象中循环时只做一次整图张贴。2.4 绘制顺序画家算法与分层结构2D 游戏普遍采用画家算法先绘制远距离物体再绘制近距离物体覆盖关系自然正确。在这个项目中绘制顺序大致是远景背景 → 静态地形墙、砖块、管道 → 道具 → 怪物 → 马里奥 → 界面信息分数、剩余时间。void GameScene::Draw() { // 背景层天空、云和远景山脉整块张贴 putimage(0, 0, bgImage); // 地形层砖块和管道管道比砖块高放在同一层 for (auto* wall : walls) wall-Draw(); for (auto* block : blocks) block-Draw(); // 道具层金币、蘑菇、火花通常画在砖块之上 for (auto* prop : props) prop-Draw(); // 怪物层Goomba 等敌人画在道具层之后 for (auto* monster : monsters) monster-Draw(); // 玩家层马里奥最后画确保他出现在怪物和道具前方 player-Draw(); // UI 层分数与命数使用文本写入 drawUI(); }这段代码值得注意障碍物和砖块的绘制放在同一层是因为马里奥的碰撞盒需要同时与两者交互绘制顺序不影响逻辑只影响视觉。道具画在怪物之前所以同屏时道具会被怪物遮挡这也是原版马里奥里的常见现象。如果希望道具悬浮在怪物上就把道具循环挪到怪物之后。2.5 图片加载loadimage 的路径与透明问题image.cpp 通常会封装一个资源管理器IMAGE imgs[IMG_COUNT]; void LoadAllImages() { // 注意图片路径相对于 .vcxproj 所在目录而不是 cpp 文件目录 loadimage(imgs[IMG_BG], res/bg1.png); loadimage(imgs[IMG_BRICK], res/brick.png); loadimage(imgs[IMG_MARIO], res/mario.png); loadimage(imgs[IMG_GOOMBA], res/goomba.png); }loadimage 的结果放进数组后续绘制时从数组取避免每帧重复读文件。很多新手直接放在 Update 里导致每帧都触发磁盘 IO帧率会跌破个位数。EasyX 20220901 版本开始支持带透明通道的 32 位位图但 putimage 遇到半透明像素时会重新计算背景色效果与浏览器中的 PNG 不完全一致。自己画素材时建议统一导出为不透明背景的 24 位 BMP或使用纯色掩码图。如果一定要用 PNG可以使用两次 putimage 的掩码绘制法先贴 AND 掩码图再贴 XOR 原图这是 GDI 时代的经典做法。常用 EasyX 绘图函数对比函数用途易错点initgraph创建绘图窗口设置宽高时注意与屏幕分辨率匹配BeginBatchDraw开启后台缓冲必须与 FlushBatchDraw 配对FlushBatchDraw提交后台缓冲不调用则画面不更新loadimage从文件加载图片路径相对工作目录putimage将 IMAGE 贴到窗口默认不处理透明坐标是左上角rectangle画空心矩形边框颜色用 setlinecolor 设置putimage 的坐标参数是矩形左上角在窗口中的坐标而游戏对象坐标通常也存储为左上角所以绘制时可以直接传 x, y。如果以对象中心点作为坐标更顺手则在 Draw 里做putimage((int)(x - width / 2), (int)(y - height / 2), ...)的换算。这个项目用 EasyX 绕过了 Win32 复杂消息循环核心绘制参数都集中在上表函数里排错范围很小。实际测试时可以把 Sleep(16) 去掉观察画面明显闪烁再恢复就能直观理解双缓冲的作用。3. 对象模型与关卡数据驱动从 block.cpp 到 gamescene.cpp3.1 为什么要拆分出这么多 cpp 文件打开源码包能看到 check.cpp、event.cpp、block.cpp、gamescene.cpp、mario.cpp、monster.cpp、wall.cpp、prop.cpp、image.cpp、platform.cpp。这种文件命名很直白每个都对应游戏中的一个实体或系统。这样拆不是为了凑数而是为了让依赖方向清晰gamescene 是场景管理器它知道所有对象其他对象只依赖图形接口和事件定义不知道自己属于哪个场景。check.cpp 通常负责碰撞检测查询event.cpp 负责把碰撞结果变成事件这两个文件被场景和对象同时调用。如果不拆分把所有函数写在一个 main.cpp 里项目做到一半就会出现“改一个跳跃参数需要重新编译所有代码”的情况。按对象划分后改怪物逻辑时只用动 monster.cpp其他文件不受影响。对用 C 做课程设计的人来说这种组织方式也更接近真实项目的模块边界。3.2 用 GameObject 基类统一 Update 与 Draw虽然源码里可能没有显式定义一个基类头文件但从各对象提供的方法可以看出它们都可以归纳为三个能力更新自身状态、绘制自身、返回碰撞盒。这里补一个轻量基类让后续新增对象更省事// GameObject.h #pragma once #include graphics.h #include vector class GameObject { public: virtual ~GameObject() default; // 每帧逻辑更新入参为距离上一逻辑帧的毫秒数 virtual void Update(long deltaMs) 0; // 绘制到当前 EasyX 后台缓冲 virtual void Draw() const 0; // 返回与世界坐标对齐的碰撞矩形 virtual RECT GetBox() const 0; protected: float x 0.0f; // 世界坐标 X像素 float y 0.0f; // 世界坐标 Y像素向下为正 float width 32.0f; float height 32.0f; };为什么不推荐把所有对象都坐上继承链因为砖块和被顶出的道具差异不大但行为完全不同墙永远不会移动道具可以沿抛物线飞出去。用组合或扁平结构反而更容易实现例如给 GameObject 增加一个bool movable标志比层层继承来得直观。继承最好只用在“共享接口、不同实现”也就是这里的纯虚方法。3.3 关卡文本格式用字符矩阵描述三关地图gamescene.cpp 最重要的功能是加载关卡。常见做法是把 1-1、1-2、1-3 分别写成 level1.txt、level2.txt、level3.txt每行一个字符序列字符与对象类型对应。字符对象说明#Wall不可被顶动的墙Block普通砖块可被顶?Block(HiddenProp)顶出道具或金币TPipe管道由 platform 合并MMonster(Goomba)板栗仔出生点PMario马里奥出生点不创建新对象oCoin静止金币简化版关卡片段....M...............M.... ....????........... ....TT..........TT....... 最后一行表示地面实际用#填充更常见。解析代码bool GameScene::LoadLevel(int level) { std::string path level std::to_string(level) .txt; std::ifstream in(path); if (!in.is_open()) return false; ClearLevel(); // 先清空上一关的对象 std::string line; const float TILE 32.0f; // 每格像素大小 int row 0; while (std::getline(in, line)) { // 去掉末尾可能的 \r否则 Windows 换行符会影响字符比较 if (!line.empty() line.back() \r) line.pop_back(); for (int col 0; col (int)line.size(); col) { const float px col * TILE; const float py row * TILE; switch (line[col]) { case #: walls.push_back(new Wall(px, py)); break; case : blocks.push_back(new Block(px, py, BlockType::Normal)); break; case ?: blocks.push_back(new Block(px, py, BlockType::HiddenProp)); break; case T: platform-AddPipe(px, py); break; case M: monsters.push_back(new Monster(px, py, MonsterType::Goomba)); break; case P: player-SetSpawn(px, py); break; case o: props.push_back(new Prop(px, py, PropType::Coin)); break; default: break; // 空格直接跳过 } } row; } return true; }这段代码有四个参数值得注意TILE是瓦片大小本项目素材按 32×32 设计。如果换成 48×48 素材只需改这一个值所有对象坐标都会自动跟着乘算。row从 0 开始对应屏幕上从上到下的行数EasyX 的 Y 轴向下增长所以py row * TILE正好符合。P不创建对象因为马里奥在关卡开始时已经位于场景中SetSpawn 只是重置他的位置和速度。管道T交给 platform 对象处理是因为管道高度可能跨多行需要先收集所有 T 的位置再按 x 方向合并成完整矩形。否则每个 T 单独生成一个 32×32 方块碰撞时会把管道当普通砖块。如果出现角色卡在管道里的问题先检查管道 T 是否被正确合并。通常在 platform.cpp 里会做二次扫描按 x 排序把所有连续 T 合并为一个大的 RECT。3.4 对象生命周期谁负责 delete关卡对象使用 new 创建那么在游戏结束或切换关卡时必须有对应的 delete。简单做法是 GameScene 析构函数中统一清理GameScene::~GameScene() { for (Wall* w : walls) delete w; for (Block* b : blocks) delete b; for (Monster* m : monsters) delete m; for (Prop* p : props) delete p; walls.clear(); blocks.clear(); monsters.clear(); props.clear(); }如果想更安全可以把容器换成std::vectorstd::shared_ptr...但在循环引用下 shared_ptr 也救不了。这个项目规模小裸指针配统一析构点反而最透明能明确知道每个对象何时死亡。真正的坑在于怪物死亡动画期间对象是否还在碰撞检测列表里。建议在 Monster 里加bool alive标志DYING 状态也继续返回碰撞盒但碰撞逻辑忽略它的攻击性。否则死亡中的怪物会直接穿过马里奥身体看起来很不自然。事件分发可以放在 check.cpp 和 event.cpp 中。check 模块负责计算所有碰撞对event 模块把碰撞对转换为事件。例如马里奥顶砖块的事件流是碰撞检测发现头与砖块相交 → 判定运动方向为向上 → 调用 block-OnHit() → block 判断 hidden 属性并生成道具。事件机制的好处是 mario.cpp 不需要 include block.h通过事件 ID 通信编译依赖和时间都减少了。4. AABB 碰撞检测与马里奥按键状态机4.1 矩形碰撞为什么对马里奥足够原版 NES 马里奥的所有判定都是用多个轴对齐矩形完成的玩家、砖块、管道、金币分别有 AABB。AABB 的意思是矩形边不旋转始终与坐标轴平行。检测两个 AABB 是否相交只需要判断左右边界和上下边界是否全部重叠。像素级碰撞听起来更精确但有两个问题一是素材会有半透明部分判定边界不明确二是对 CPU 消耗高。在像素风 2D 游戏中矩形盒与素材误差很小玩家能接受贴图边缘略微碰不到砖块。因此 AABB 是性能和表现的最佳折中。4.2 判定函数与重叠深度inline bool IsCollide(const RECT a, const RECT b) { return a.left b.right a.right b.left a.top b.bottom a.bottom b.top; } // 求水平重叠深度用于 X 轴处理 inline float OverlapX(const RECT a, const RECT b) { return (a.right b.right ? a.right : b.right) - (a.left b.left ? a.left : b.left); }RECT 的 left/top/right/bottom 是LONG类型在两个对象边缘刚好接触时 left rightIsCollide 返回 false符合预期。由于物体移动按像素步进很少出现刚好相等。重叠深度用于修正方向。如果dx 0说明马里奥向右移动撞墙时应将 x 调整为墙的 left - 马里奥宽度/2如果dx 0则调整为墙的 right 宽度/2。这里建议以对象中心点存储位置因为马里奥下蹲时高度变化中心点不变只需要调整 height。4.3 分轴移动平台跳跃最关键的工程决策很多新手把碰撞响应做成“移动整个 (x,y) 后再统一检测”结果角色卡进墙角无法动弹。正确做法是分轴处理先移动 X检测所有固体修正 X再移动 Y检测并修正 Y。这样水平撞墙时 Y 轴不受影响踩到怪物时 X 轴不抖动。void Mario::MoveWithCollision(float dx, float dy, const std::vectorRECT solids, bool onGround) { onGround false; // 先处理水平方向 x dx; RECT box GetBox(); for (const RECT s : solids) { if (IsCollide(box, s)) { if (dx 0.0f) x s.left - width / 2; // 右边界被墙挡住 else if (dx 0.0f) x s.right width / 2; // 左边界被墙挡住 box GetBox(); // 修正后立刻更新碰撞盒 } } // 再处理垂直方向 y dy; box GetBox(); for (const RECT s : solids) { if (IsCollide(box, s)) { if (dy 0.0f) { y s.top - height / 2; // 落地 onGround true; dy 0.0f; // 关键清除下落速度 } else if (dy 0.0f) { y s.bottom height / 2; // 顶头 } box GetBox(); } } }参数说明dx 和 dy 是这一逻辑帧内的位移量单位为像素。s.left 和 s.right 都是整型在修正 x 时如果直接把 float x 赋给整型矩形会丢失小数。所以 GetBox 里应该用 floor/ceil 取整保证碰撞盒与绘制位置一致否则角色会随着帧数累积亚像素误差。另外注意当马里奥站在地面上时每帧重力都会给 dy 加一个正数。如果与地面碰撞检测成功后不把 dy 置 0下一帧会用更大的正速度再次下穿最终穿透地面。这个语句容易被忽略我把它放在onGround true;之后。4.4 马里奥状态机四种基础状态 火球mario.cpp 内部至少要维护一个枚举enum class MarioState { Idle, // 站立 Run, // 跑动 Jump, // 跳跃 Squat, // 下蹲 Fire // 发射火球后的短暂状态 };状态转换表当前状态按键输入新状态条件Idle / RunK 按下JumponGround trueJumpK 松开Run / Idley 速度从负变正跳跃衰减Idle / RunS 按下Squat站在可下蹲的固体上SquatS 松开Idle头顶无遮挡Idle / Run / JumpJ 按下Fire有火球能力且场上火球数 2FireJ 松开回到原状态射击瞬间切换实现跳跃手感时三个参数最关键重力加速度、起跳初速度、跳跃按键衰减。常见的初始值const float GRAVITY 0.45f; // 每帧下落加速度像素/帧^2 const float JUMP_SPEED -11.0f; // 起跳瞬间 y 方向速度负值向上 const float FALL_CAP 10.0f; // 最大下落速度避免穿地如果感觉跳跃太飘减小 JUMP_SPEED 或增大 GRAVITY如果总是够不到高处砖块适当调大 JUMP_SPEED。EasyX 的 Y 轴向下为正所以起跳是负速度。跳跃手感还有一个细节是“可变跳”玩家按住跳跃键时重力较小松开跳跃键后重力立刻变大。轻点按键是小跳长按是大跳。实现上是在 Update 里判断如果按住 K 且速度小于 0GRAVITY 用 0.3否则用 0.45。加了之后游戏才不像弹簧跳。4.5 check.cpp 与 event.cpp碰撞结果如何变成游戏事件check.cpp 的职责是批量计算碰撞对for (auto* m : monsters) if (IsCollide(player-GetBox(), m-GetBox())) eventQueue.push({EventType::PlayerHitMonster, m});event.cpp 消费事件队列根据事件类型做决策switch (ev.type) { case EventType::PlayerHitMonster: // 判断是踩到还是侧面撞到 if (player-IsFalling() player-GetBottom() ev.obj-GetBox().top 16) ev.obj-OnStomped(); else player-OnHurt(); break; }“踩到”的判断标准是玩家矩形底边在怪物顶部以下 16 像素内并且 y 方向速度为下落。16 像素约等于怪物高度的四分之一可以根据手感微调。数值越小越容易判定为受伤。check 和 event 的拆分解耦了对象依赖mario.cpp 里不再需要 include monster依赖图更干净。5. 怪物 AI、道具系统与三关差异化配置5.1 板栗仔的两态有限状态机monster.cpp 里的敌人行为不复杂。Goomba 只有 ALIVE、DYING、DEAD 三个状态。ALIVE 时以恒定 speed 水平移动碰到固体墙体或悬崖边缘就反转方向。DYING 是不参与碰撞的死亡动画500ms 后转为 DEAD 并被移除。void Monster::Update(long deltaMs) { if (state MonsterState::Dying) { dieTimer deltaMs; if (dieTimer 500) state MonsterState::Dead; return; } // 将速度换算到当前帧的位移 x speed * (deltaMs / 16.0f); RECT box GetBox(); if (HitSolid(box)) // 前面有墙或砖块 speed -speed; // 掉头 if (NoGround(box)) // 脚下没有地面 speed -speed; // 悬崖边缘掉头 }这里deltaMs / 16.0f是实用小技巧正常情况下 16ms 对应 1 逻辑帧如果某次更新用了 32ms就移动 2 倍距离。这样游戏在不同性能的机器上不会明显变速。但要注意不能直接把该值用于碰撞穿透判断因为大步长可能导致矩形直接跳过墙体。通常限制单帧最大步长比如一个逻辑帧内最多移动speed * 2。NoGround 判断要小心往下探测一格矩形如果检测不到任何固体就认为怪物走到悬崖边。探测矩形建议比怪物本体窄几像素否则在砖块边缘会提前掉头导致怪物卡在边缘反复横跳。5.2 道具金币、蘑菇和火花prop.cpp 中常见的道具类型类型触发方式拾取效果Coin直接加 100 分不生成移动物体分数 100显示浮动数字Mushroom从砖块内向上弹出后水平移动小马里奥变大FireFlower从砖块内弹出原地竖直放置变成火人允许发射火球蘑菇和火花的移动方式不同蘑菇是会行走的道具需要像怪物一样检测碰撞碰到墙或悬崖就变向火花不需要移动马里奥碰到即拾取。因此 prop 类里最好加一个bool movable标志。顶砖块弹出道具的动画也在这里处理道具出生后前 200ms y 坐标持续向上偏移之后切换到正常移动。void Prop::Update(long deltaMs) { if (state PropState::Emerging) { emergeTimer deltaMs; y - 0.8f * deltaMs / 16.0f; // 先向上钻出 if (emergeTimer 200) { state PropState::Active; y - BLOCK_TILE; // 弹出到砖块上方一格 } return; } if (movable) { x speed * (deltaMs / 16.0f); if (HitSolid(x prevX ? dirRight : dirLeft)) speed -speed; } }emergeTimer 控制弹出动画时长200ms 后道具移动到砖块上方 32 像素处玩家才能看到完整蘑菇。如果蘑菇卡在砖块内部检查 y 的修正是否用 BLOCK_TILE 而不是道具自身的 height。5.3 三关难度是怎么拉开的三关地图的参数差异是项目最值得借鉴的地方。1-1 是教学关敌人稀疏跳跃点之间的宽度不超过 3 格地面平整1-2 用大量砖块搭出高位通道玩家需要下蹲通过矮顶隧道1-3 加入管道包围和连续跳跃平台怪物密度明显增大。可以用配置表管理关卡敌人密度只/屏最大跳跃宽度是否地下新对象1-11–24 格否Goomba1-23–43 格是平台、砖块堆1-34–55 格否高密度怪物这些参数可以写在 gamescene.cpp 顶部的结构体中在每个关卡加载后使用。比如 1-2 的地下关卡天空背景变暗可以在 level 配置里加一个bg_id字段。判断“最大跳跃宽度”的方法是让马里奥以最高水平速度地面起跳看水平移动经过多少格。如果角色能轻松跳过 6 格说明重力太小或水平速度过快需要平衡。5.4 通关判定与“自动关闭”的真相项目说明里特意提到“通关 1-3 后程序自动关闭属正常现象”。原因是游戏循环中到达旗帜后关卡索引递增索引超过最大关卡时直接调用了 exit(0)。对应代码大致是void GameScene::CheckGoal() { if (player-ReachedFlag()) { currentLevel; if (currentLevel MAX_LEVEL) { exit(0); // 所有关卡完成演示结束 } else { LoadLevel(currentLevel); player-ResetToSpawn(); } } }exit(0) 会立刻终止进程不调用局部对象析构。如果想做“全通关”结算画面需要把 exit(0) 替换为gameFinished true然后在外层循环中显示胜出画面并等待按键。但作为课程设计直接退出符合项目说明。如果想循环游玩把 exit(0) 改成currentLevel 1即可。遇到旗帜时另一个常见 bug 是“卡在旗杆”马里奥碰到旗杆后仍然向右移动反复触发判定。解决方案是在碰到旗帜后禁用玩家输入同时把 x 速度清零等下滑动画完成后再进入下一关。6. VS2022 编译 EasyX 项目的排错与调试技巧6.1 装好 EasyX 后还差一步配置EasyX_20220901 默认会安装到 VS2022 的第三方库目录中但保险起见还是确认三个位置项目属性 → C/C → 常规 → 附加包含目录包含 EasyX 的include路径链接器 → 常规 → 附加库目录包含lib路径链接器 → 输入 → 附加依赖项填入EasyX.lib64 位或EasyXa.lib32 位只报找不到graphics.h多半是附加包含目录没写对报unresolved external symbol多半是附加依赖项没加。6.2 LNK2019 错误main 和 WinMain 的入口之争用 EasyX 写int main()链接时却报 LNK2019_mainunresolved这是最常见的问题。原因是图形库的链接配置默认指向图形程序入口WinMain而你的代码是控制台程序入口main。最简单的修复是在项目属性 → 链接器 → 系统 → 子系统中选择“窗口”或者在代码最前面加#pragma comment(linker, /subsystem:windows /entry:mainCRTStartup)这样告诉链接器用main作为程序入口同时以窗口子系统运行。注意mainCRTStartup是 CRT 启动函数它会初始化运行时环境后调用你的main所以控制台的 printf 输出仍然有效但不会弹出控制台窗口。6.3 中文乱码与资源路径EasyX 的loadimage接受LPCTSTRVS2022 默认使用 Unicode 字符集字符串常量要写成_T(res/bg.png)或Lres/bg.png。如果源码文件是 UTF-8 无 BOM中文注释和路径会在编译时被当成 GBK 解析。最稳妥的办法是项目属性 → C/C → 命令行添加/utf-8编译选项同时把源文件另存为 UTF-8 with BOM。6.4 用调试开关画出所有碰撞盒碰撞逻辑看起来没问题但角色老穿墙最有效的排查方式是可视化碰撞盒。EasyX 没有DrawRect可以用rectangle函数画空心矩形void GameScene::DrawDebugBox() { if (!showDebug) return; setlinecolor(RED); for (auto* w : walls) rectangle(w-GetBox().left, w-GetBox().top, w-GetBox().right, w-GetBox().bottom); for (auto* b : blocks) rectangle(b-GetBox().left, b-GetBox().top, b-GetBox().right, b-GetBox().bottom); for (auto* m : monsters) rectangle(m-GetBox().left, m-GetBox().top, m-GetBox().right, m-GetBox().bottom); player-DrawDebugBox(); }用 F1 键切换showDebug。观察时重点看两个位置马里奥站立时矩形是否刚好贴地下蹲时矩形顶部是否穿过头顶砖块。如果矩形比素材大调小 width/height而不是去改碰撞检测逻辑。6.5 区块化绘制与固定步长优化当关卡砖块超过上百个时逐块putimage会白白消耗 GDI 调用。一种有效优化是把静态地形预先绘制到一张离屏IMAGE上加载时只画一次渲染循环里一次putimage整块贴出。实现时可以用SetWorkingImage切换当前设备把staticMap作为绘图目标然后把所有 wall 和 block 的位置画上去。动态物体仍按对象绘制帧率能明显提升。另一个建议是把逻辑更新和绘制分离用固定 16ms 步长累加。不要直接用 Sleep 控制物理相反先让循环尽可能跑每累计满 16ms 才执行一次物理更新。这样在高刷新率屏幕上逻辑频率稳定跳跃高度不会随帧率变化。记得把 staticMap 的尺寸设置成与游戏区完全一致否则滚屏时边界会露出空白。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门