
简介基于Python与Pygame库实现的超级马里奥游戏项目完整复刻了经典横版闯关玩法包含马里奥、敌人、平台、道具、得分系统及关卡设计等核心要素适合具备一定Python语法基础、希望掌握游戏开发实战流程的学习者。压缩包共91个文件以29个.py源码文件为主干另有24个.pyc编译文件、18个.ogg游戏音效、9张.png角色/场景图片、5个.wav音频及少量配置与文档整体大小仅9.52MB源码、组件、游戏状态、素材资源等模块划分清晰。该项目已被3855人浏览学习具有不错的参考价值。通过阅读并修改源码可系统学习Pygame的事件监听、精灵渲染、碰撞检测、地图二维数组设计、面向对象类结构如Player、Enemy、Platform以及音频资源管理同时还能了解游戏状态切换和关卡扩展思路对从基础语法过渡到完整小游戏开发非常有帮助。1. 项目拆解为什么用Python写马里奥以及这件事值不值得做先亮个观点用Python写超级马里奥听起来像个入门练手项目但你要是真把它写完、写好它对工程能力的锻炼绝对被严重低估了。很多人一听“Python游戏”就想到turtle画个圆、pygame贴个方块觉得和“真正的游戏开发”不沾边。但马里奥这个品类恰好是2D平台跳跃游戏的经典样本——它不是靠美术堆出来的而是靠物理、碰撞、状态机、相机系统这些底层逻辑撑起来的。你把这些搞明白了换到Unity、Godot或者直接用C写也就是换了个渲染层的写法核心思路完全通用。这事的本质是用Python还原一个经典横版过关游戏的核心玩法玩家控制马里奥在由砖块、管道、平台组成的关卡里跑跳踩敌人、吃金币、顶砖块最后到达旗杆过关。真要较真SMBSuper Mario Bros的每一处手感都有讲究马里奥起跳时的高度和滞空时间、踩怪后的反弹高度、砖块被顶时的震动反馈、敌人从水管探头的节奏……这些就是整个项目的魂。这个项目适合谁三种人刚学完Python基础语法、想找个绕不开面向对象和游戏循环的项目来练手的人对游戏开发感兴趣、想搞懂2D平台游戏底层机制的人以及已经工作、想用一个小型但完整的项目来复盘架构设计的人。如果你是零基础刚看完变量和循环的那种建议先去把类class和列表list用熟再回来啃这个倒不是说门槛高而是面向对象没感觉的时候写这类代码会比较痛苦。另外一个被忽略但很实际的价值是这个项目是实习和简历里极其安全的一块敲门砖。它小、完整、能跑、能演示、能讲解比十个只写了半截的爬虫脚本有说服力得多。2. 核心设计思路Game Loop、状态机和对象模型2.1 游戏循环是灵魂别的都是配件任何游戏不管多复杂最底层都是同一个骨架游戏循环Game Loop。它的逻辑非常朴素——不断重复三件事读输入、更新状态、画画面。人类眼睛看到连续变化的画面会产生“动态”的错觉就是靠这个循环转得足够快Pygame默认的Clock.tick(60)就是把每秒循环次数锁在60次对应60FPS。这里有个很关键的细节很多人第一版都会忽略画面更新和物理更新必须分开。画面只是一层皮马里奥的坐标、速度、跳跃状态、碰撞结果这些才是里子。你每帧先根据键盘输入改变马里奥的速度和坐标再检测碰撞并修正位置最后才把修正后的坐标画到屏幕上。顺序反了或者混在一起就会出现“明明没按跳跃键人却飘了”“被砖块卡住疯狂抖动”这类灵异现象。在Python里写这个循环最直接的方案是clock pygame.time.Clock() running True while running: dt clock.tick(60) / 1000.0 # 转换为秒 for event in pygame.event.get(): if event.type pygame.QUIT: running False handle_input() # 1. 读输入 update(dt) # 2. 更新状态 render() # 3. 画画面 pygame.display.flip()注意那个dt。不同电脑性能不一样屏幕刷新率也不一样如果你直接把速度按“每帧移动几像素”来写画面在高刷屏上就会变成快进。dtdelta time就是把这个差异抹平的移动距离 速度 × 时间而不是 移动距离 速度 × 帧数。我见过太多人忽略这个结果游戏在自己的电脑上正常一换电脑就变成重力加速度异常。这个坑记住一句话永远不会出错的习惯是所有涉及速度和加速度的更新都乘上dt。2.2 马里奥的物理模型别小看这次跳跃2D平台跳跃游戏的手感八成取决于跳跃物理模型。如果你只是把马里奥的y坐标往下加那不叫跳跃叫“瞬移”。标准做法是把跳跃拆成加速度、速度、位置三个层次重力加速度G每帧对垂直速度施加一个向下的增量让马里奥下落时越来越快。跳跃初速度J按下跳跃键时一次性给垂直速度一个负值屏幕坐标系y向下所以向上是负。最大速度Vmax限制水平移动和下落速度防止数值失控。我调试了相当久才定下来一组比较接近原版手感的朋友聚会参数写在这里供你参考单位像素/秒基于60FPSGRAVITY 1500.0 # 重力加速度下落时越来越快 JUMP_SPEED -880.0 # 起跳瞬间的垂直速度负值向上 MOVE_SPEED 320.0 # 水平移动速度 MAX_FALL_SPEED 1200.0 # 下落极限速度 SKIP_HOLD_TIME 0.08 # 跳跃键按住缓冲提升手感这里有个经典机制值得专门说可变跳跃高度Variable Jump Height。玩过马里奥的人都有体感轻点一下跳跃键跳得矮长按跳跃键跳得高。实现方式不复杂——在跳跃键仍然按住期间重力加速度降低为原来的一半左右一旦松开重力恢复原值。这样按得越久上升过程被“削”得越少跳得就越高。这是平台跳跃游戏调节手感的最核心手段之一没有它跳跃就是一根没有灵魂的弹簧。另一个更隐蔽但任天堂粉丝会狂喜的细节叫跳跃滞后Coyote Time。玩家在走出平台边缘之后的大约100毫秒内仍然可以跳跃体验上就是“明明感觉已经踏空了却还能跳出来”。这个机制的原理是玩家认知和反应存在延迟如果严格按碰撞检测结果来判定能否起跳玩家会频繁觉得“我明明按了跳跃却没反应”。做游戏所谓的手感很多时候就是靠这种反直觉的“物理宽容”做出来的。2.3 碰撞检测从矩形碰撞到像素级修正马里奥和砖块、地面、管道的互动全部依赖碰撞检测。主流的入门方案是矩形碰撞检测每个物体给一个矩形范围pygame.Rect每帧检查两个矩形是否重叠。这个API在pygame里是现成的——rect1.colliderect(rect2)但直接用老代码会遇到一个经典毛病碰撞后物体卡进墙里无法自拔。原因在于当物体移动速度较快时单帧内它可能“穿过”了墙。矩形碰撞检测只能告诉你“当前帧是否重叠”不能告诉你“上一帧在哪里”。如果移动步长大于墙的厚度就会发生穿墙。解决思路通常是两个方向一是把移动和碰撞做成“先移动再校验、一旦碰撞就回退到贴边位置”的分步处理二是引入“预测性碰撞”即根据速度预测下一帧位置提前检测是否会撞墙。我实际采用的做法是分轴处理——水平移动和垂直移动分开做碰撞def move_and_collide(entity, world, dx, dy): # 水平移动 entity.rect.x dx for tile in world.collide_tiles(entity.rect): if dx 0: entity.rect.right tile.left elif dx 0: entity.rect.left tile.right entity.velocity.x 0 # 垂直移动 entity.rect.y dy for tile in world.collide_tiles(entity.rect): if dy 0: entity.rect.bottom tile.top entity.on_ground True elif dy 0: entity.rect.top tile.bottom entity.velocity.y 0这个方案的妙处在于水平和垂直的碰撞互不干扰不会出现“斜着撞墙时角色被莫名其妙弹飞”的情况。同时entity.on_ground状态完全由垂直碰撞结果赋值——只有脚下踩到东西才为True这个布尔值直接决定马里奥能不能起跳。要是想让头顶撞砖块时有一个“顶一下”的反馈可以再加一个简易像素检测砖块被撞击后绘制时偏移几个像素再回弹配合粒子效果砖块碎片四散观感就会很接近原版。不过关于逐像素碰撞我的建议是先用矩形方案把整个游戏跑通再考虑要不要升级——矩形方案简单、高效、调试成本低新手上路绝对够了。3. 实操落地搭出可复现的完整项目3.1 环境准备与素材选择的坑开始写代码之前先说环境。Python版本建议3.9以上太低的话pygame的新版本装不上。安装依赖就一行pip install pygame如果下载速度慢或者经常超时用国内镜像源可以省掉大量痛苦pip install pygame -i https://pypi.tuna.tsinghua.edu.cn/simple素材方面推荐两个路子。一是用现成的免费素材包网上搜“free pixel art platformer tileset”能找到大量CC0授权的资源注意看清授权条款个人学习无所谓但如果做出来想发到网上去最好用CC0或MIT协议的素材。二是自己画——用Aseprite或者免费的LibreSprite16x16或32x32像素的图块就够了。说实话自己画的素材虽然精度一般但用自己素材做出来的项目那种成就感是完全不一样的。再就是音频。马里奥的音效如果直接拿来用会有版权问题建议用Bfxr、ChipTone这类工具生成8-bit风格的音效一小时就能搞定全套——跳跃、踩怪、吃金币、死亡、过关每种一个音效氛围感立刻拉满。3.2 地图与关卡用文本文件搭积木不需要图形化编辑器我用一种极其简单但有效的方案做关卡用文本字符表示地图。每一行代表地图的一行每个字符代表一种图块或物体# 其中 X砖块, 地面, o金币, E敌人, | 水管, P玩家出生点 ------------------------------------------------------------ ------------------------------------------------------------ -----------X---------X-------------------------------------- -----------X-o-o-o-X--------------------------------------- --------X--X---------X------E------------------------------- || P || |写一个解析函数逐字符扫描并生成对应的游戏对象def load_level(path): with open(path, r, encodingutf-8) as f: rows f.read().splitlines() tiles [] player_start (100, 400) for y, row in enumerate(rows): for x, ch in enumerate(row): if ch X: tiles.append(Tile(TileType.BRICK, x*TILE_SIZE, y*TILE_SIZE)) elif ch : tiles.append(Tile(TileType.GROUND, x*TILE_SIZE, y*TILE_SIZE)) elif ch o: coins.append(Coin(x*TILE_SIZE, y*TILE_SIZE)) elif ch E: enemies.append(Goomba(x*TILE_SIZE, y*TILE_SIZE)) elif ch P: player_start (x*TILE_SIZE, y*TILE_SIZE) return tiles, coins, enemies, player_start为什么推荐这个方案因为调试太方便了。你想改关卡结构直接打开txt改字符就行不用开着编辑器拖拽半天两个人协作时没装任何工具也能看懂“这个地表啥样”。这极其符合“先跑通再美化”的工程原则。3.3 实体的类设计与对象管理马里奥、敌人、砖块、金币、粒子效果这些都是游戏实体Entity。每个实体都需要继承一个基类基类里放最通用的属性class Entity: def __init__(self, x, y, width, height): self.rect pygame.Rect(x, y, width, height) self.velocity pygame.math.Vector2(0, 0) self.on_ground False self.alive True def update(self, dt): self.rect.x self.velocity.x * dt self.rect.y self.velocity.y * dt def draw(self, screen, camera): screen.blit(self.image, camera.apply(self.rect))马里奥是Player类继承Entity额外维护状态机站立、跑动、跳跃、下落、死亡。每种状态对应不同的动画帧和物理参数。这里有个值得好好学习的点用状态机控制角色比写满if-else逻辑清晰一万倍。比如class Player(Entity): def __init__(self, x, y): super().__init__(x, y, 32, 32) self.state PlayerState.IDLE self.facing 1 # 1右, -1左 def update(self, dt): self.handle_input() self.apply_physics(dt) self.check_state_transition()状态机的好处是状态之间的转换规则非常清晰——只有在on_ground且未按下跳跃键时才能从FALL切换到IDLE只有从IDLE或RUN时按下跳跃键才会进入JUMP。游戏逻辑复杂起来之后这种结构化设计能救命。3.4 相机跟随让马里奥在横向地图里“动起来”超级马里奥的地图比屏幕宽得多所以必须实现一个“相机”的概念屏幕上显示的内容只是世界的一个窗口相机跟随玩家移动。相机本身不是实体只是一个偏移量。原理说起来一句话绘制时所有物体的屏幕坐标 世界坐标 - 相机偏移量。class Camera: def __init__(self, width, height): self.offset pygame.math.Vector2(0, 0) self.width width self.height height def update(self, target): target_x target.rect.centerx - self.width // 2 target_y target.rect.centery - self.height // 2 # 限制相机不超出地图边界 target_x max(0, min(target_x, self.width - self.width)) # 简化示意 self.offset.x target_x self.offset.y target_y def apply(self, rect): return rect.move(-self.offset.x, -self.offset.y)这里有一个原版SMB的经典设计相机不会同时进行水平和垂直追随。玩家在地面奔跑时相机只在水平方向移动垂直方向是锁定的当玩家跳上高处平台时相机再垂直跟着上移。这个设计能避免玩家在跳跃时画面跟着上下乱晃造成眩晕和失焦。咱们实现时可以给相机加一个“只在玩家y坐标超过某个阈值时才垂直追随”的逻辑。3.5 敌人AI与踩怪判定让板栗仔活过来板栗仔Goomba是马里奥最经典的敌人。它的AI简单得可怕向左走到边缘或撞墙就转向碰到马里奥则视情况判定——从上方踩到板栗仔卒马里奥反弹起跳从侧面碰到马里奥卒进入死亡动画。踩怪判定非常考验细节。你不能简单地“两个矩形重叠就判定踩死”因为玩家可能是侧身蹭过去的。惯例做法是当马里奥的垂直速度大于0下落且马里奥的矩形底部距离板栗仔矩形顶部很近比如10像素以内时判定为有效踩踏。def check_enemy_collision(player, enemy): if player.rect.colliderect(enemy.rect): # 马里奥在下落且脚底在敌人上边界附近 if player.velocity.y 0 and player.rect.bottom - enemy.rect.top 12: enemy.alive False player.velocity.y JUMP_SPEED * 0.6 # 踩怪反弹 return stomp else: player.die() return hurt return None这段代码里player.velocity.y JUMP_SPEED * 0.6极其重要——踩到敌人后如果不给一个向上的反弹力马里奥会直接继续下落砸到第二个敌人身上体验很差。这个反弹高度也需要仔细调太高了玩家觉得飘太低了觉得黏我最终定在跳跃初速度的60%左右手感比较接近原作。4. 常见问题与排查技巧实录搞这种项目踩坑是必然的有些坑你躲都躲不掉。我把调试过程中遇到的高频问题整理成表方便你遇到问题时直接对照现象根本原因解决办法玩家穿墙或掉出地图移动和碰撞不同步单帧位移超过图块尺寸分轴碰撞先移动再修正限制最大下落速度角色在墙边疯狂抖动碰撞修正后位置仍处于重叠状态死循环碰撞修正时强制rect.right tile.left并清零速度避免用rect.inflate()放大碰撞盒跳跃按快了没反应跳跃判定只发生在固定的物理帧内加入跳跃缓冲jump buffer记录按键按下的最近时间200ms内落地即起跳从平台边缘走出去后无法起跳起跳条件只判定on_ground但已经离地加入土狼时间coyote time离地后保留约100ms的跳跃资格游戏在不同电脑上速度不一样物理更新没有使用delta time所有速度相关更新乘以dt并把刷新率锁在60FPS贴图边缘有白边素材使用了PNG透明通道但没做预乘alphaimage pygame.image.load(path).convert_alpha()切割图集时注意扩大1像素的裁剪区域音频播放卡顿掉帧每帧重新加载音频对象初始化时一次性加载所有音效播放时用pygame.mixer.Sound.play()不要反复加载文件内存占用持续增长每帧创建新的Surface或Rect对象没有释放精灵对象尽量复用粒子系统及时回收死亡粒子再补充一个独有的排查技巧用调试覆盖层暴露物理状态。开发时我在屏幕上画了马里奥的rect边框实时显示速度向量和当前状态名。这比打印日志直观太多一眼就能看出是重力没生效、跳跃初速度太小还是碰撞回退了。游戏开发里调试用的可视化永远比文本输出高效。另外有个经验值得特别说——把一个变量抽出来做成参数反复试。比如重力加速度我一开始写死在update方法里每次想调整都要改代码重启效率极低。后来把所有物理参数都集中到一个PhysicsConfig类里并且在开发模式里加了键盘快捷键调参上下键改重力左右键改跳跃速度手感是用键盘一点点“试”出来的不是靠眼睛猜出来的。建议你也这么做省下的时间将以小时计。5. 优化与扩展从“能玩”到“好玩”第一版跑通之后你会发现游戏“能玩但不够好玩”。这句话恰恰是编程进阶的分水岭功能实现完毕不等于项目完成整个系统是否具备优化和扩展的余地才是工程能力的最佳体现。几个低成本高收益的优化方向我排个序第一是粒子系统。踩扁敌人时爆出一团像素碎屑、顶砖块时掉落的碎砖片、金币闪烁时的光芒这些用二三十行参数化粒子类就能实现但对观感的提升是翻天覆地的。粒子类只需要维护位置、速度、生命周期、颜色和尺寸五个属性每帧做一次更新和绘制即可。第二是动态音效与背景音乐循环。用极小容量的8-bit音乐文件做循环再配合简单音效游戏代入感会立刻上一个台阶。但需要注意音效混音时的音量平衡背景音乐要压低音量音效要高一个台阶否则容易听不清关键反馈。第三是增加关卡变体。比如加入移动平台、旋转的障碍、升降梯。这些机制每次新增都对应一个新的实体类和一个新的碰撞处理逻辑等于给你一张永不重样的练习题。第四个方向也是进阶玩家最值得做的记录并回放。把每一帧的输入序列存下来就能实现“死亡后自动回放生前5秒操作”的“死亡回放”功能这需要引入“输入缓存”数据结构以及时间戳管理。它不会直接让游戏更好玩但会让系统的抽象层级上一个台阶实战价值极高。最后如果对自己写出来的成品很满意可以打包成独立的exe文件分享给朋友玩。打包工具用PyInstaller核心命令就一句pyinstaller --onefile --windowed --add-data assets;assets main.py注意事项就一个素材路径要用相对路径并跟随程序目录动态获取。用sys._MEIPASS这个PyInstaller内置变量解决资源路径问题不然打出来的exe运行时会报“找不到图片素材”。我个人写完这个项目最大的体会是游戏开发其实是一门关于约束的艺术——资源有限性能有限玩家耐心更有限。Python虽然通常被认为“不适合做游戏”但恰恰因为它的迭代速度极快反而成了学习游戏机制和物理模型的绝佳土壤。真到要发商业游戏的时候同样的设计思路平移给C或Rust就是两天的事。跳坑吧每一个坑里都有东西可挖。本文还有配套的精品资源点击获取