前言
在童年回忆中,有两个游戏是最想复刻的,其中一个就是《超级马里奥》。上大学后学会了编程,做过俄罗斯方块、扫雷、贪吃蛇、飞机大战、坦克大战、打砖块等经典游戏,但一直没有从头到尾做完一个超级马里奥,因为它比前这些游戏要稍微复杂一些,之前找过几个别人的实现,都有大几千行以上。
几年前我在 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 方法基于网格检测冲突,返回一个二进制向量表示每一行的冲突情况,同时支持返回冲突单元格的坐标。
- 多冲突类型:可指定需要检测的单元格类型(砖块、问号块、管道等),并分别处理不同冲突结果(如析构砖块、激活问号块)。
- 动态冲突盒:马里奥的冲突盒根据其状态(大小、蹲下)动态调整,确保检测冲突准确。

