跳到主要内容 C++ 超级马里奥项目架构与实现解析 | 极客日志
C++ 算法
C++ 超级马里奥项目架构与实现解析 本文介绍了一个基于现代 C++ 实现的超级马里奥项目。文章从架构和实现两个层面分析了项目的优点,包括清晰的类层次、模块分工、智能指针管理资源、数据驱动设计以及精细的冲突检测和物理运动系统。此外,文中还总结了 8 点优化建议,涵盖纹理缓存、空间分割、代码去重、资源集中化、精灵表使用、状态机重构、减少临时容器分配及对象池管理等方向,旨在帮助开发者理解游戏编程的软件工程实践与现代 C++ 特性应用。
前言
在童年回忆中,有两个游戏是最想复刻的,其中一个就是《超级马里奥》。上大学后学会了编程,做过俄罗斯方块、扫雷、贪吃蛇、飞机大战、坦克大战、打砖块等经典游戏,但一直没有从头到尾做完一个超级马里奥,因为它比前这些游戏要稍微复杂一些,之前找过几个别人的实现,都有大几千行以上。
几年前我在 YouTube 看到一个大佬的视频,他用了非常简洁的思路,借助现代 C++ 的语法,只用 2000 行就做了一个超级马里奥(用 clang-format 将大括号换行后只有 1800 行)。
我迫不及待地学习了一遍,当时也记录了一些文档。前几天有人要我推荐项目,我马上想到它,然后顺便把前的文档重新清理和补充了一下,构成此文,按介绍把该项目推荐给大家。
根据理解,该代码是作者一气呵成的,并未做故意优化,所以如果要优化,可更为精简。优化可当作作业去做,我也总结了 8 点优化建议放这里,大家可参考。
该约 2000 行的 C++ 项目不仅完整恢复了初代马里奥的核心玩法,更是一个精心设计的软件工程示例。无论你是否要学习游戏编程,学习该项目的源码都能带给你意外的收获。
对不想学习游戏编程的人:
理解软件设计的艺术:剖析一个经典游戏的实现,可见优秀代码如何像乐高一样模块化、可扩展,该设计思想同样适合网络开发、移动应用乃至系统软件。
提升代码阅读能力:读高质量的、注释幽默的代码是一个享受,它能帮你培养'代码审美',在未来审核同事代码或学习新框架时事半功倍。
查看游戏开发的幕后:了解游戏背后的检测冲突、物理模拟、动画系统等机制,能让你以更深度的视角欣赏游戏,甚至变成'硬核玩家'时的谈资。
入门 2D 游戏开发的绝佳教材:该项目覆盖了游戏循环、管理资源、实例组件、检测冲突、动画系统等核心概念,代码量适中,非常适合按第一个深入研究的 游戏项目。
学习现代 C++ 实践:项目大量使用智能指针、标准库算法、常量表达式等现代 C++ 特性,并展示了面向对象设计在游戏中的实际应用。
取得可复用的架构模板:你可直接借鉴其架构(如 MapManager、Animation、实例继承体系)到自己的 2D 平台游戏中,大幅降低起步难度。
理解性能与可维护性的平衡:分析代码中哪些地方为了性能而妥协,哪些地方为了可读性而抽象,你能积累宝贵的工程平衡经验。
下面我简单地从架构和实现两个层面解析此项目的优点。
一、架构优点
1. 清晰的类层次 基类与继承类:Enemy 按抽象基类,定义了敌人共有的接口(update、draw、die)。Goomba 和 Koopa 按具体继承类,实现各自的行为逻辑。
该设计符合开闭原则,方便扩展新的敌人类型。
多态运用:游戏主循环中 std::shared_ptr 容器统一管理所有敌人,利用虚函数动态调用具体类的更新和绘画方法。
2. 模块分工显式
地图管理:MapManager 类负责地图数据的加载、检测冲突、粒子效果和硬币动画,与游戏实例逻辑完全解耦。
动画系统:动画类封装了精灵动画的帧切换、速度控制和绘画,可复用马里奥、敌人等多个对象。
实例独立:Mario、Mushroom、Goomba、Koopa 等各自管理自身状态、位置、速度和渲染,统一的接口与地图和交互系统通信。
3. 现代 C++ 管理资源 智能指针:Enemy 继承 std::enable_shared_from_this,使用 std::shared_ptr 管理敌人生命周期,避免泄漏内存,并安全地传递指针。
常数集中:Global.hpp 集中定义了所有游戏常数(如重力、速度、大小等),方便调整和保持一致性。
4. 数据驱动设计 地图草图:关卡设计图像文件(LevelSketch*.png)定义,不同颜色代表不同地图元素(砖块、问号块、管道等)。该数据与代码分离的方式使得易于修改和扩展关卡设计。
颜色地图:读取草图像素颜色决定生成蘑菇还是硬币,实现灵活的关卡配置。
5. 视图跟随与帧率独立 视图跟随:根据马里奥的水平位置动态调整视点,保证马里奥总是在屏幕中央(除边界外)。
时间管理:使用 std::chrono 计算增量时间,实现与帧率无关的游戏循环,确保在不同硬件上游戏速度一致。
二、实现优点
1. 精细的检测冲突系统
单元格冲突:map_collision 方法基于网格检测冲突,返回一个二进制向量表示每一行的冲突情况,同时支持返回冲突单元格的坐标。
多冲突类型:可指定需要检测的单元格类型(砖块、问号块、管道等),并分别处理不同冲突结果(如析构砖块、激活问号块)。
动态冲突盒:马里奥的冲突盒根据其状态(大小、蹲下)动态调整,确保检测冲突准确。
2. 平滑的物理运动
加速度模拟:马里奥的移动带加速度和减速度,按键响应有惯性,释放按键后逐渐减速,手感接近原版游戏。
跳跃蓄力:jump_timer 实现长按跳跃键跳得更高的效果,增加操作深度。
重力与最大速度:垂直速度受重力影响,并限制最大下落速度,避免穿透。
3. 丰富的状态管理 马里奥状态:包括正常、蹲下、死亡、成长、无敌等多种状态,growth_timer、invincible_timer 等计时器控制状态持续时间,逻辑清晰。
敌人状态:Koopa 有行走、龟壳静止、龟壳滑动三个状态,状态转换自然,行为符合经典设置。
4. 动画系统
通用动画类:动画类封装了帧动画的核心逻辑,支持设置帧宽、纹理路径、动画速度、翻转等,可复用所有需要动画的对象。
动画速度与运动同步:马里奥行走动画的速度与其水平速度成正比,动画更自然。
5. 视觉反馈与粒子效果
砖块粒子:顶碎砖块时生成四向飞溅的粒子,增加破坏的视觉表现。
硬币弹出:问号块中的硬币以跳跃动画渲染,增强收集感。
精灵闪烁:无敌状态和成长状态下的精灵闪烁,直观提示玩家当前状态。
6. 代码可读性与注释 幽默注释:作者在关键逻辑处添加了大量幽默且详细的注释,不仅解释代码意图,还共享了开发中的趣事和漏洞修复经历,极大提升了代码的可读性和趣味性。
清晰函数命名:方法名如 get_hit_box、add_brick_particles、map_collision 等直观表达了功能。
7. 标准库的充分利用 算法容器:大量使用 std::all_of、std::remove_if、std::min、std::max 等标准算法,使代码简洁高效。
向量操作:利用 std::vector 存储敌人、蘑菇、粒子等动态集合,并使用 erase-remove 惯用法清理死亡对象。
8. 管理资源优化
按需加载纹理:每个对象在构造器中加载自己的纹理,避免全局重复加载。
纹理切换:根据马里奥状态实时切换纹理,而不是预加载所有纹理,节省内存。
三、优化建议:让代码更精简高效 尽管当前代码已可流畅运行,但若以更高标准审视,仍有不少可优化的地方。以下列举若干优化点,涉及性能、可维护性、精简代码等方面,并附上具体的改进思路。
1. 纹理重复加载 -> 纹理缓存 问题:在 Mario::draw、Goomba::die、Koopa::die 等函数中,频繁调用 texture.loadFromFile。每次绘画都可能触发磁盘 I/O,严重浪费性能。
改进:使用一个全局纹理管(如 TextureManager 单例),在初始化游戏时预加载所有纹理,后续按字符串标识取得。如:
class TextureManager {
std::unordered_map<std::string, sf::Texture> textures;
public :
sf::Texture& get (const std::string& name) {
auto it = textures.find (name);
if (it == textures.end ()) {
sf::Texture tex;
tex.loadFromFile ("Resources/Images/" + name + ".png" );
textures[name] = std::move (tex);
it = textures.find (name);
}
return it->second;
}
};
在 Mario 等类中,只需存储纹理引用或指针,绘画时直接使用。
2. 检测冲突优化性能 问题:MapManager::map_collision 每次都会遍历整个地图单元格,即使只有一小部分在视图内。当地图较大时,成本随地图面积线性增长。
改进:引入空间分割(如网格空间索引)。按若干块(Chunk)划分地图,只检测与玩家冲突的盒相交的块。如:
std::vector<unsigned char > MapManager::map_collision_fast (const sf::FloatRect& hitbox) {
int left_chunk = hitbox.left / CHUNK_SIZE;
int right_chunk = (hitbox.left + hitbox.width) / CHUNK_SIZE;
}
3. 清除代码重复 问题:Mario::update 中多次出现类似的检测冲突代码块(水平冲突、垂直冲突、地面检测),每次都需要调用 map_collision 并检查 std::all_of。
改进:取一个私有助手函数,统一处理检测冲突与响应。如:
bool Mario::check_collision (const sf::FloatRect& hitbox, std::vector<Cell> types, sf::Vector2i& out_cell) {
auto collision = map_manager.map_collision (types, hitbox);
}
4. 管理资源集中化 问题:每个实例(Mario、Goomba、Koopa、Mushroom)都在自己的构造器中加载纹理,导致可以多次加载相同纹理(如地下/地上版本)。
改进:将所有纹理路径集中到配置文件中,由 ResourceManager 统一加载和管理。同时使用引用计数,确保同一纹理只加载一次。
5. 使用精灵表(SpriteSheet)减少纹理切换 问题:每个动画帧使用单独的图片文件,导致渲染时需要频繁绑定不同纹理,降低 GPU 效率。
改进:在一张大图合并同一角色的所有动画帧(精灵表),在动画类中用纹理矩形(sf::IntRect)定位。这样只需绑定一次纹理即可绘画所有帧。
6. 敌人状态机重构 问题:Koopa 类使用多个布尔变量(state、flipped、no_collision_dying)表示状态,逻辑分散在更新的各个条件分支中,难以维护。
改进:用状态模式(StatePattern)或枚举 + 状态函数表。如:
enum class KoopaState { Walking, Shell, Sliding, Dying };
void (Koopa::*state_handlers[4 ])() = {
&Koopa::update_walking,
...
};
7. 减少临时容器分配 问题:map_collision 返回 std::vector,每次调用都会在堆上分配内存。在游戏循环中频繁调用可能导致内存碎片和额外成本。
改进:改用静态线性存储的容器,或输出参数传递预分配的内存。如:
void map_collision (const std::vector<Cell>& types, const sf::FloatRect& hitbox, std::vector<unsigned char >& out) {
out.clear ();
}
8. 使用对象池管理动态实例 问题:频繁创建和析构敌人、蘑菇、粒子等对象,导致动态分配内存成本。
改进:实现简单对象池,预分配一定数量的对象,需要时从池中取出,析构时放回池中。尤其适合粒子效果(brick_particles、question_block_coins)。
四、总结 该超级马里奥 Bros 项目不仅是个可运行的游戏,更是一个值得学习的 C++ 的游戏编程示例。你可在其基础上轻松添加新关卡、新敌人或新功能,体现了良好的软件工程实践。
微信扫一扫,关注极客日志 微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具 加密/解密文本 使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown 转 HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML 转 Markdown 互为补充。 在线工具,Markdown 转 HTML在线工具,online
HTML 转 Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML 转 Markdown在线工具,online
JSON 压缩 通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online