简介这是一套经典超级玛丽游戏的Python源码实现完整还原了原版跳跃、敌人、碰撞与关卡推进等核心机制适合Python游戏开发初学者、爱好者以及想研究2D游戏架构的开发者参考。资源共59个文件包含19个.py源码与18个.pyc编译文件、15张.png图片素材、6个.json配置数据以及1个说明文档压缩包整体仅423KB目录清晰地将主程序、常量、组件与工具模块分层组织。描述中提到可自行编译后续关卡说明文档和所需图片素材均已内置便于读者在源码基础上扩展新关卡或调整游戏逻辑。目前已有762人学习浏览对于想快速上手经典游戏复刻、理解pygame游戏循环与碰撞检测的读者来说这是一份轻量而完整的实战样本。1. 超级玛丽源码不是在找素材是在拆手感搜超级玛丽源码的人多数是想把现成工程拿下来换换贴图就以为自己做了个游戏。真下回来几十个包你会发现能跑的没几个能跑的也调不通角色跳不起来、跳起来在空中滑、撞到砖块会被卡住、摄像机要么不动要么甩尾。问题不在美术素材而在每个下载包背后的物理参数、帧循环和碰撞求解方式。横版平台跳跃不是一个渲染问题是一个按帧推进的状态机问题。这套源码拆解会以 SDL2 C 为例把移动、跳跃、碰撞、卷轴、敌人这几块按工程里的实现顺序讲清楚。游戏开发第一年的人能照着把工程搭起来做过后端或客户端的熟手也能通过这套代码快速理解平台跳跃的核心数据结构。2. 超级玛丽源码实现的准备SDL2 工程与固定帧循环2.1 选 SDL2 看源码比 Unity/Godot 更直接如果目的只是搞懂超级玛丽这类横版动作游戏的内在逻辑SDL2 比 Unity 或 Godot 更合适。后两者把渲染、物理、输入全部封装成编辑器组件你看到的脚本只是行为逻辑碰撞发生在引擎内部参数调完也看不到中间计算过程。SDL2 只负责窗口、事件和纹理上传剩下所有逻辑都写在你自己能看能改的 C 代码里。SDL2 的 API 面不大做一个 2D 平台跳跃用到的接口不超过二十个一个下午就能进入状态。常见做法是直接装系统包不搞额外的依赖管理sudo apt install libsdl2-dev g -stdc17 -O2 -o mario main.cpp -lSDL2这两条命令把头文件和运行时库装好再链接 SDL2 编出可执行文件。-O2是发布构建SDL2 的绘制调用本身开销低但平台跳跃每帧有大量像素级 AABB 计算不开优化会白吃 CPU。如果你更喜欢 CMake最少配置是这样cmake_minimum_required(VERSION 3.16) project(mario) find_package(SDL2 REQUIRED) add_executable(mario main.cpp) target_link_libraries(mario SDL2::SDL2)find_package(SDL2 REQUIRED)让 CMake 去系统路径找 SDL2 的配置文件。碰到SDL2::SDL2not found 的报错基本是包管理器装的是libsdl2-dev但 CMake 版本太老换成target_link_libraries(mario ${SDL2_LIBRARIES})也能绕过。2.2 游戏主循环与固定时间步长写游戏源码最常见的错误是把移动量乘以真实时间差然后默认显示器是 60Hz。在 144Hz 屏上角色会跑得快一倍在低帧率机器上又慢得难受。SDL2 提供SDL_GetTicks()返回开机到现在的毫秒数配合累加器实现固定时间步长是最稳妥的做法const int TPS 60; const double dt 1.0 / TPS; Uint32 last SDL_GetTicks(); double accumulator 0.0; while (running) { Uint32 now SDL_GetTicks(); accumulator (now - last) / 1000.0; last now; SDL_Event e; while (SDL_PollEvent(e)) { /* 窗口事件 */ } while (accumulator dt) { updateGameLogic(); // 只在这里更新物理与输入状态 accumulator - dt; } renderScene(); }TPS是每秒逻辑更新次数固定 60 次。accumulator把多余的时间存起来攒满一个dt才走一步。渲染循环不设上限跟随显示器刷新率物理计算却始终按 60 步/秒推进。这个结构直接解决了两个问题物理行为不随跑分波动碰撞检测也不会因为某一帧时间差过大而漏穿墙壁。2.3 输入采样与动作状态机输入部分要采键盘状态而不是逐帧消费事件。SDL_KEYDOWN事件在按下瞬间只触发一次如果你在这时才设置跳跃标志按住跳时只有第一帧生效跳跃垂直初速度会很快被重力盖掉。用SDL_GetKeyboardState拿当前帧所有按键的布尔状态每帧采样逻辑层只依赖这组状态const Uint8* keys SDL_GetKeyboardState(nullptr); bool left keys[SDL_SCANCODE_LEFT]; bool right keys[SDL_SCANCODE_RIGHT]; bool space keys[SDL_SCANCODE_SPACE]; bool xkey keys[SDL_SCANCODE_X];这里用SDL_SCANCODE_*而不是SDLK_*区别在于扫描码表示物理按键位置与键盘布局无关。后续做改键功能时逻辑层不感知具体按键只关心left、jump这类抽象输入。超级玛丽的手感状态机可以精简成四个状态站立、跑步、跳跃上升、下落。不要从一开始就设计一堆继承类状态多到一定程度再抽象也不迟enum class MarioState { Idle, Run, JumpUp, Fall }; MarioState state MarioState::Idle; if (space (state MarioState::Idle || state MarioState::Run)) { vy jumpSpeed; state MarioState::JumpUp; }onGround标志放在碰撞模块里维护状态机只消费它。移动、跳跃、碰撞、渲染各自独立。输入映射用一张表就能把当前所有动作说清楚按键扫描码抽象动作说明左方向键SDL_SCANCODE_LEFTleft水平方向 -1右方向键SDL_SCANCODE_RIGHTright水平方向 1空格SDL_SCANCODE_SPACEjump跳跃触发X 键SDL_SCANCODE_Xrun加速跑可选状态机不直接把物理按键存进来而是保存left、right、jumpPressed这几个语义值。这样按键映射调整只改输入层物理参数和状态判断一行不动。3. 超级玛丽源码的主角速度、重力与跳跃手感参数3.1 水平加速度、摩擦力与空中控制超级玛丽的主角在地面移动时并不是直接给一个恒定速度而是每帧施加加速度方向键松开后再通过摩擦力让速度衰减回零。这个设计决定了角色从静止到全速需要一个短暂过程体感是“有重量”而不是“推一下就滑出去”。const float ACCEL 0.5f; const float MAX_VX 4.0f; const float FRICTION 0.85f; const float AIR_CONTROL 0.5f; float dir 0.0f; if (left) dir - 1.0f; if (right) dir 1.0f; if (dir ! 0.0f) { float accel onGround ? ACCEL : ACCEL * AIR_CONTROL; vx dir * accel; } else { vx * onGround ? FRICTION : 1.0f; } if (vx MAX_VX) vx MAX_VX; if (vx -MAX_VX) vx -MAX_VX;AIR_CONTROL设为 0.5表示空中加速能力只有地面的一半。这样跳跃后想在空中变向响应会比地面慢半拍却不会完全失能。直接把FRICTION设成 0 是常见误用松键瞬间速度变零角色会显得僵硬设成 0.85 后两个方向都松开速度逐渐收敛行走动画也能在减速过程中正常播放。每帧砍掉 15% 只成本帧向量的标量乘法开销完全可以忽略。3.2 跳跃缓冲、土狼时间与释放乘数跳跃手感不佳大部分问题出在“玩家按下跳的时机”与“角色落地时机”之间有一个微小的时间差。玩家在空中最后几帧按跳等到落地时按键已经松开程序认为“没有跳跃输入”于是这跳就吞掉了。实现上要加一个跳跃缓冲记录玩家按下跳的最近几帧const int JUMP_BUFFER_TICKS 6; int jumpBufferTimer 0; if (jumpPressed jumpBufferTimer 0) { jumpBufferTimer JUMP_BUFFER_TICKS; } else if (jumpBufferTimer 0) { jumpBufferTimer--; } const int COYOTE_TICKS 5; int coyoteTimer 0; if (onGround) { coyoteTimer COYOTE_TICKS; } else if (coyoteTimer 0) { coyoteTimer--; } if (jumpBufferTimer 0 coyoteTimer 0) { vy JUMP_VY; jumpBufferTimer 0; coyoteTimer 0; onGround false; }jumpBufferTimer记录按键动作发生后还能等待多少帧。coyoteTimer则反过来记录角色离开平台后仍保留多少帧“地面”资格。二者相遇时执行跳跃玩家实际看到的体感是从平台边沿走出去之后半空中按跳仍然能起效而不是必须提前一百毫秒精准预判。还有第三个因子是跳跃释放乘数。按住跳与轻点跳的高度应当不同但不需要做两套跳跃动画。做法是在按键松开时给垂直速度乘一个削减系数if (!jumpHeld vy 0) { vy * 0.4f; }vy 0表示当前还在上升阶段。按键一松开上升速度立刻打折角色很快进入下落形成低跳。如果按键一直按住上升速度不被打断跳跃高度自然更高。这个逻辑一帧生效不需要维护一个“是否长按”的累计计时器。3.3 物理参数表与判定顺序超级玛丽工程里物理参数集中放在一个头文件或结构体里不要在 update 函数里散落魔法数字。下面这张表覆盖了这套最小实现的大部分可调项参数典型值单位手感影响TPS60次/秒调高碰撞更细CPU 开销上升ACCEL0.5像素/帧²越大起步越利落MAX_VX4.0像素/帧整体游戏节奏AIR_CONTROL0.5无量纲空中变向能力FRICTION0.85无量纲松键后减速快慢GRAVITY0.25像素/帧²越大跳得越低、下落越快JUMP_VY8.0像素/帧跳跃初速决定最高点RELEASE_MULT0.4无量纲轻按跳与长按跳的高度差COYOTE_TICKS5帧约 83ms 出沿缓冲JUMP_BUFFER_TICKS6帧约 100ms 提前量更新顺序也有讲究先输入采样再水平加速度计算再速度 clamp再应用重力修改vy最后位移与碰撞。跳跃触发放在水平计算之后、重力应用之前。如果把跳跃放在重力之后vy已经被重力累加了一个正数抵消掉一部分跳跃初速跳跃高度会缩水而且帧率不稳时这个缩水幅度还会波动。4. 超级玛丽源码的地图瓦片碰撞、摄像机与实体4.1 瓦片地图与 X/Y 分离碰撞地图数据最常见的存储方式是二维网格。每个格子 16x16 像素地面、砖块、管道都有对应的块 ID。用字符串数组写关卡草稿非常直观运行时解析成std::vectorstd::vectorintconst char* mapRows[] { , , ######## , , P E , ############# ##############, };P是玩家出生点E是敌人#是实心地面。解析时逐个字符判断标记实体位置后把对应格子换成空气。这种数据格式的好处是关卡编辑不需要额外工具文本里直接改字符就能调整布局。完整工程里也可以换成 Tiled 导出的 CSV 格式但字符串方式的代码量最少也最容易定位问题。碰撞求解采用 X 轴和 Y 轴分离推进。先水平位移并处理水平碰撞再垂直位移并处理垂直碰撞。最忌讳的是一次性移动完 x 和 y再拿最终 AABB 和所有瓦片做穿透修正那样无法判断是哪个方向撞进来的角色会被系统强行推到一个莫名其妙的角落。void moveX(Entity e, const Level level) { e.x e.vx; int left int(e.x) / TILE_SIZE; int right int(e.x e.w - 0.001f) / TILE_SIZE; int top int(e.y) / TILE_SIZE; int bottom int(e.y e.h - 0.001f) / TILE_SIZE; if (e.vx 0) { for (int ty top; ty bottom; ty) { if (level.solid(right, ty)) { e.x float(right * TILE_SIZE - e.w); e.vx 0.0f; } } } else if (e.vx 0) { for (int ty top; ty bottom; ty) { if (level.solid(left, ty)) { e.x float((left 1) * TILE_SIZE); e.vx 0.0f; } } } }right和bottom减去0.001是为了避免实体边缘与格子边界完全重合时卡到下一格。左右碰撞只按实体覆盖的纵向格子范围检查一次只处理一个方向。垂直方向类似但要额外记录onGroundvoid moveY(Entity e, const Level level) { e.y e.vy; int left int(e.x) / TILE_SIZE; int right int(e.x e.w - 0.001f) / TILE_SIZE; int top int(e.y) / TILE_SIZE; int bottom int(e.y e.h - 0.001f) / TILE_SIZE; e.onGround false; if (e.vy 0) { for (int tx left; tx right; tx) { if (level.solid(tx, bottom)) { e.y float(bottom * TILE_SIZE - e.h); e.vy 0.0f; e.onGround true; } } } else if (e.vy 0) { for (int tx left; tx right; tx) { if (level.solid(tx, top)) { e.y float((top 1) * TILE_SIZE); e.vy 0.0f; } } } }移动顺序固定为moveX再moveY。向下落的角色会先完成水平移动再处理重力带来的下移。如果改成先moveY角色掉进地面后还执行水平移动会被地面边缘卡住。实体碰撞盒建议比瓦片略小。16x16 的格子玩家碰撞盒用 12x14居中偏上左右各留 2 像素底部留 0 像素顶部留 2 像素。这样走在砖块边缘时视觉上脚底踩在砖的界线上但碰撞盒还悬空一点玩家觉得“差一步也会掉下去”不会觉得被砖缝咬住。4.2 敌人与金币的源码组织超级玛丽的敌人用不着设计复杂行为树。一个 Goomba 只需要两个状态向当前方向移动碰到墙壁或走到平台边缘就转向。实现时给它一个与玩家类似的重力和速度更新但水平方向上不做玩家那样的加速度衰减保持恒定速度struct Enemy { float x, y, vx, vy; bool alive true; }; void updateEnemy(Enemy e, const Level level, float dt) { if (!e.alive) return; e.x e.vx * dt; // 前方一格没有地面则转向 int aheadX (e.vx 0) ? int(e.x e.w 1) / TILE_SIZE : int(e.x - 1) / TILE_SIZE; int belowY int(e.y e.h 1) / TILE_SIZE; if (!level.solid(aheadX, belowY)) { e.vx -e.vx; } }前方检测取的是脚底下方的格子而不是眼睛前方。这就是平台边缘检测敌人走到边缘脚底那块地没有了就回头。如果你只检测前方墙壁敌人会直直走下平台。金币的处理更简单每帧检查玩家 AABB 与金币 AABB 是否相交相交则置alive false并把计数加一。关卡里的实体统一放进一个std::vectorEntity加一个type字段区分玩家、敌人、金币、终点旗杆enum class EntityType { Player, Enemy, Coin, Flag }; struct Entity { EntityType type; bool alive; float x, y, w, h; float vx, vy; };update函数按type分发互不干扰。这种实体组件结构虽然原始但足够应付一整关的敌我交互等实体种类超过十种再拆组件系统也不晚现在引入复杂框架只会阻塞自己写代码。4.3 摄像机卷轴的锁定与贴地判定超级玛丽的横向卷轴有一个关键细节摄像机不以玩家屏幕中心为锚点而是留出右侧更宽的视野。常见做法是把摄像机目标锁定在玩家位置的前方 35% 处也就是玩家站在屏幕左侧三分之一附近时卷轴才开始移动跑到屏幕中央偏右位置角色就几乎看不到自己右侧了。这个比例不是美术调出来的它决定了玩家对前方视野的预判能力。float camX player.x - SCREEN_WIDTH * 0.35f; camX std::max(0.0f, std::min(camX, levelWidth - SCREEN_WIDTH));SCREEN_WIDTH * 0.35f是角色在画面中的水平锚点数值越大角色越靠屏幕左边右侧能看到的地图越远。平台跳跃普遍取 0.3 到 0.4 之间超过 0.5 会让玩家感觉到角色偏居一侧少了猎奇感。std::max和std::min把摄像机限制在地图边界内实现卡边效果。渲染时所有瓦片和实体的绘制坐标都要减去camX。SDL2 里SDL_RenderCopy接收的SDL_Rect是整数而camX是浮点由于浮点转整数会截断推荐先用round取整避免一帧画布平移一像素的原因导致角色边缘轻微抖动。5. 超级玛丽源码调试用窗口标题、热键和输入回放验证手感手感问题靠眼睛看很钝把状态参数实时烫到窗口标题上是最直接的调试方式。SDL2 的SDL_SetWindowTitle每次调用开销不高每一帧拼接字符串更新一次即可char title[128]; snprintf(title, sizeof(title), x%.1f vy%.2f state%d onGround%d, player.x, player.vy, (int)state, player.onGround); SDL_SetWindowTitle(window, title);这样角色跳跃时vy的变化过程、落地瞬间onGround是否变成 1、状态机是否切到Fall全都能实时看到。不需要打断点不需要加日志体感问题能被立刻定位到具体轴或具体参数。物理参数不建议靠改代码重新编译来调。把参数和按键绑定数值调整与玩法测试的切换时间几乎为零。常见做法是给数字键绑定静参数方向键上下来调整准备计算的值。比如用 1/2 调重力3/4 调跳跃初速每一帧把当前值显示在窗口标题上。调到满意后再把数值回填进代码。参数调完还有最后一步录制输入回放。把每一帧的left、right、jumpPressed按 tick 存成一个环形数组触发回放时不再读取键盘状态而是读取记录里的值struct InputFrame { bool left, right, jump; }; std::vectorInputFrame recording;回放和实时运行走同一个更新函数只是输入源不同。这能验证一个关键问题手感变差到底是你改了参数还是新增模块引入了碰撞回归。把回放里每一帧的vy和位置打印出来与修改前对比数值差异会明确指向出问题的部分。输入回放加上窗口标题里的 tick 序号能直接把坏手感定位到具体某一帧。本文还有配套的精品资源点击获取