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

Pygame实战:拆解Python版魂斗罗,掌握2D游戏开发核心逻辑

简介经典小游戏Python版魂斗罗完整程序包面向想通过实战学习Pygame游戏开发的Python初学者也适合希望复刻童年经典的游戏爱好者。压缩包共247个文件以228个png素材图片为主配合9个py源码、8个pyc编译文件以及md说明和jpeg预览图整体大小2.67MB。主程序Contra.py采用WASD控制移动、J键射击、P键退出涵盖角色、子弹、敌人、关卡等模块便于逐段研读游戏循环、碰撞检测和动画绘制逻辑。已有1809人学习下载通过阅读源码与替换图片素材可快速掌握2D游戏项目的基本组织方式还能在此基础上扩展关卡与角色是理解Python游戏开发流程的一份轻量参考。1. 解压即玩的Python版魂斗罗值得拆开看的远不止情怀这是最近在折腾的一份Python版魂斗罗完整程序包主程序Contra.py用Pygame重写了红白机经典射击游戏。压缩包里十几个PNG素材、一张地图JPG和一份README解压后一条python Contra.py就能跑起来。对只想怀旧的人来说这是个能玩的游戏但对写代码的人来说它其实是一份非常典型的2D动作射击游戏最小可运行样本游戏主循环、精灵碰撞、动画帧切换、事件驱动输入这些在Pygame游戏开发里最常被问到的点这份代码都覆盖了。适合正在学Python、想搞明白Pygame游戏循环的朋友也适合想找个完整项目做二次开发的老手。2. Contra.py拆解主程序结构与Pygame游戏循环2.1 程序包里都有什么先看压缩包内容。map01.jpeg是关卡地图背景pygame.image.load加上transform.scale就能铺成640x480窗口的背景6.png到10.png这一串是角色动画帧站立、奔跑、跳跃各占几张boom1.png是爆炸特效README.md写明运行方式和键位。典型的Pygame 2D游戏资源布局图片要么做精灵图集要么逐帧文件。这里用的是逐帧PNG好处是加载代码简单坏处是文件数量多美术修改时得一张张替换。文件类型在游戏中的用途Contra.pyPython源码主入口游戏循环与逻辑map01.jpeg背景图像关卡地图渲染到背景层6.png / 7.png / 8.png / 9.png / 10.png角色动画帧站立、奔跑、跳跃等姿态boom1.png特效图像子弹命中或敌人死亡时的爆炸帧README.md文档运行说明、键位说明2.2 Pygame初始化固定窗口与帧率设定主程序打开后的第一段通常是Pygame初始化和窗口创建。常见做法是把游戏常量集中定义后续所有逻辑都引用这些常量避免魔法数字散落在代码里。比如窗口尺寸定为640x480这是4:3比例跟原版魂斗罗的屏幕比例一致贴图也不会被拉伸变形。import pygame WIDTH, HEIGHT 640, 480 FPS 60 TITLE Contra - Python Remake pygame.init() screen pygame.display.set_mode((WIDTH, HEIGHT)) pygame.display.set_caption(TITLE) clock pygame.time.Clock()set_mode返回的Surface是最终呈现到屏幕的画布所有图像Blit到screen上才算渲染完成。Clock对象用来做帧率限制clock.tick(FPS)放在主循环末尾保证游戏在60帧上下运行不会在性能好的机器上跑得飞快、物理逻辑错乱。2.3 主循环三件事输入、更新、绘制Pygame程序的核心是永不退出的while循环每一帧做三件事从事件队列里读输入、更新游戏内对象状态、把画面画出来。事件队列用pygame.event.get()拉取注意处理pygame.QUIT事件否则点窗口关闭按钮游戏也不会退出只能靠P键或者强制结束进程。running True while running: for event in pygame.event.get(): if event.type pygame.QUIT: running False keys pygame.key.get_pressed() player.handle_input(keys) player.update() bullets.update() enemies.update() screen.blit(background, (0, 0)) enemies.draw(screen) bullets.draw(screen) player.draw(screen) pygame.display.flip() clock.tick(FPS)逻辑说明player.handle_input(keys)在每帧内读取键盘状态这里用get_pressed()而不是事件里的KEYDOWN是因为移动操作需要在按住按键的每一帧都生效而KEYDOWN只在按下那一刻触发一次。update()负责位移、动画帧切换、碰撞检测的数据变化。绘制阶段先画背景再画游戏对象因为Pygame没有Z轴概念后画的会盖在先画的上面所以背景必须第一个Blit。pygame.display.flip()把后台缓冲一次性交换到屏幕这是双缓冲机制防止画面闪屏。clock.tick(FPS)参数是目标帧率上限。若机器跑不满会尽量逼近若性能过剩则主动休眠等待降低CPU占用。把FPS从60改成30也能跑但动作射击游戏对帧率敏感30帧下跳跃和射击的手感差很多原版魂斗罗是60帧标准。3. 玩家角色与动画帧把按键变成屏幕上的移动3.1 Sprite精灵类为什么继承pygame.sprite.Sprite主角在Pygame里一般封装成Player类继承pygame.sprite.Sprite。Sprite基类自带image和rect两个属性前者是角色当前帧的图像Surface后者是图像在屏幕上的位置矩形。所有碰撞检测、绘制、更新都围绕这两个属性进行这是Pygame精灵系统的约定自己造轮子反而会跟spritecollide等内置函数脱节。class Player(pygame.sprite.Sprite): def __init__(self, x, y): super().__init__() self.animations { stand: [pygame.image.load(f) for f in [6.png, 7.png]], run: [pygame.image.load(f) for f in [8.png, 9.png, 10.png]], } self.state stand self.frame_index 0 self.anim_timer 0 self.image self.animations[self.state][0] self.rect self.image.get_rect() self.rect.topleft (x, y) self.speed 3 self.jump_velocity 0初始化时把所有需要的图像加载进来按状态分组存到字典里。frame_index和anim_timer配合控制动画帧切换频率speed是水平移动速度单位是像素/帧jump_velocity是竖直方向速度跳跃物理全靠它。3.2 WASD键位与跳跃物理控制布局在README里有说明A向左、D向右、W跳跃、S趴下、J射击、P退出。按键绑定直接写在handle_input里按住的状态用pygame.key.get_pressed()检测。跳跃是动作射击里最容易写出飘忽手感的部分简化做法是给jump_velocity一个负的初速度每帧叠加重力常量落到地面后归零。GRAVITY 0.5 JUMP_SPEED -12 GROUND_Y 400 def handle_input(self, keys): if keys[pygame.K_a]: self.rect.x - self.speed if keys[pygame.K_d]: self.rect.x self.speed if keys[pygame.K_w] and self.on_ground: self.jump_velocity JUMP_SPEED self.on_ground False if keys[pygame.K_s]: self.state duck def update(self): self.jump_velocity GRAVITY self.rect.y self.jump_velocity if self.rect.bottom GROUND_Y: self.rect.bottom GROUND_Y self.jump_velocity 0 self.on_ground True参数说明GRAVITY0.5意味着每帧下落速度增加0.5像素/帧每秒约30像素/秒的下落加速。JUMP_SPEED-12是起跳瞬间的初速度。这两个数的比例决定了跳跃高度和滞空时间想让跳得更高就加大JUMP_SPEED绝对值想让下落更快就加大GRAVITY。原版魂斗罗跳跃手感偏硬落地很快这里的参数已经比较接近。GROUND_Y是地面高度用rect.bottom判断角色脚底是否触地避免把整个矩形算进去产生半身陷地的错觉。S键的逻辑留了个接口按下时切到duck状态松开时需要在外部把状态还原回stand否则角色会一直保持趴下的动画帧。P键这类一次性指令更适合用KEYDOWN事件监听因为按住不放只应触发一次退出动作跟持续移动的按键处理逻辑不同。3.3 动画帧切换固定间隔换帧动画原理很简单每隔N毫秒把frame_index加一然后从当前状态的帧列表里取图像。N决定动画播放速度N太小动画闪得太快N太大看起来像幻灯片。用时间戳控制的好处是跟FPS解耦——FPS变了动画速度不变。帧计数器也能做到但掉帧时会失真。ANIM_INTERVAL 100 # 毫秒 def update_animation(self, now): if now - self.anim_timer ANIM_INTERVAL: self.anim_timer now frames self.animations[self.state] self.frame_index (self.frame_index 1) % len(frames) self.image frames[self.frame_index]这里now由外部传入通常是pygame.time.get_ticks()的返回值单位是毫秒。取模% len(frames)保证索引不会越界并且能循环播放。状态切换时应该把frame_index归零否则从奔跑切到站立会从中间帧开始视觉上会跳一下。跳跃过程通常不切帧直接固定用跳跃那一帧等落地再回到站立或奔跑这是老式动作游戏的习惯做法。下面把这几个关键参数汇总一下后面调手感时直接改这里。参数值作用GRAVITY0.5重力加速度像素/帧JUMP_SPEED-12起跳初速度负值向上GROUND_Y400地面高度触地判定ANIM_INTERVAL100动画帧切换间隔毫秒speed3水平移动速度像素/帧4. 子弹、敌机与碰撞检测动作射击的核心循环4.1 Bullet类速度和生命周期管理子弹是动作射击游戏最频繁创建和销毁的对象。每个Bullet保存速度向量和生存时间超出屏幕范围就kill()掉防止对象无限制堆积。Pygame的Sprite支持kill()调用后该对象从所有Group中移除下一轮draw就不会再画它这是精灵系统管理对象生命周期的标准手段。class Bullet(pygame.sprite.Sprite): def __init__(self, x, y, direction): super().__init__() self.image pygame.Surface((8, 4)) self.image.fill((255, 255, 255)) self.rect self.image.get_rect() self.rect.center (x, y) self.speed_x 8 * direction self.lifetime 90 def update(self): self.rect.x self.speed_x self.lifetime - 1 if self.lifetime 0: self.kill()方向参数direction只有1或-1乘以8就是向右或向左的飞行速度。这里用lifetime限制存活90帧大约1.5秒。相比只判断屏幕边界生命周期管理更稳——角色站在画面中间向左右射击子弹即使飞不出屏幕也会按时消失游戏中不会积压无用的Sprite实例。子弹图像就用一个8x4像素的白色矩形实际项目中可以替换成带拖尾效果的PNG素材碰撞判定范围跟着rect走替换素材不会影响逻辑。4.2 碰撞检测矩形碰撞和mask碰撞到底用哪个碰撞检测是动作射击游戏性能与手感的平衡点。Pygame内置spritecollide默认用矩形碰撞检测两个Sprite的rect是否相交速度快但精度低子弹擦过敌人头部偏上的像素区域也会判为命中。改进方案是传入pygame.sprite.collide_mask利用图像的非透明像素做精确的像素级碰撞。hits pygame.sprite.spritecollide(player, enemies, False) if hits: player.hurt() hits pygame.sprite.spritecollide(bullet, enemies, False, pygame.sprite.collide_mask) for enemy in hits: bullet.kill() enemy.hurt()第一行只传三个参数是矩形碰撞推荐写法False表示命中后不自动删除敌人由玩家掉血逻辑自己处理。第二行collide_mask是关键差异mask碰撞基于每张图像像素的alpha通道只有两个图像实际不透明的区域重叠才判定命中更适合子弹这种细长物体跟不规则敌人的碰撞判定。代价是mask碰撞比矩形碰撞慢一个数量级同屏子弹和敌人数量不多时可以放心用如果做成弹幕游戏就得牺牲精度换取性能。碰撞方式速度精度适用场景默认矩形碰撞快低含透明像素拾取道具、Boss近战判定collide_mask慢高按像素判定子弹命中不规则敌人collide_circle中等中等按最大内切圆圆形爆炸范围4.3 Enemy类与死亡特效boom1.png的角色敌人逻辑可以先写个基础版本水平巡逻、朝玩家方向移动、被子弹命中后切换成爆炸帧然后移除。boom1.png这个爆炸动画的接入点在hurt()后的状态切换里死亡时播放一两帧爆炸再kill()给玩家明确的击打反馈这是动作游戏的基本体验要求。class Enemy(pygame.sprite.Sprite): def __init__(self, x, y): super().__init__() self.image pygame.image.load(8.png) self.rect self.image.get_rect() self.rect.topleft (x, y) self.hp 2 self.dead False def hurt(self): self.hp - 1 if self.hp 0: self.dead True self.image pygame.image.load(boom1.png) self.death_timer 15 def update(self): if self.dead: self.death_timer - 1 if self.death_timer 0: self.kill() else: self.rect.x - 1 # 向左巡逻hp2意味着要命中两枪才能击杀具体数值由关卡设计决定把血量写在属性里而不是硬编码在hurt()函数中方便后续做不同难度的敌人变体。死亡后15帧约0.25秒内保留爆炸帧在画面上之后再从Group移除避免爆炸画面一闪而过看不清。想做出敌人掉落道具的效果可以在kill()之前生成一个新的Pickup Sprite加入道具Group这是这类游戏扩展时的常见做法。5. 卡帧排查与FPS优化让60帧跑得稳再打包成exe5.1 图像加载性能convert()与colorkey的取舍拖动角色时感觉画面一顿一顿先查图像加载。Pygame的pygame.image.load()返回的Surface像素格式跟显示Surface不一致每次Blit时都要做像素格式转换这是卡顿的主要来源。解决办法是加载后立刻调用convert()或convert_alpha()把图像转换成屏幕的同格式。JPEG背景用convert()带透明通道的PNG用convert_alpha()。bg pygame.image.load(map01.jpeg).convert() player_img pygame.image.load(8.png).convert_alpha()convert()之后的Surface内存格式与显示屏幕完全一致Blit时不再需要实时转换综合性能提升在游戏中非常明显。透明PNG用convert_alpha()保留alpha通道如果用convert()透明区域会变成黑色或白色块。没有透明需求的背景图不要调用convert_alpha()它的处理速度比convert()稍慢。5.2 控制台窗口与打包PyInstaller的基础用法程序调试完毕想发给别人试玩总不能要求每个人都装Python和Pygame。用PyInstaller打包成单个exe注意--windowed参数隐藏命令行窗口否则运行时会多弹一个黑色控制台体验很差。pip install pyinstaller pyinstaller --onefile --windowed --name Contra Contra.py--onefile把所有依赖打包进单一exe--windowed是Windows下不显示控制台的关键参数。打包后把map01.jpeg和所有PNG放到exe同目录程序才能正常加载资源。也可以把这些图片文件加进打包脚本的datas参数但依赖路径处理会更绕通常直接外置资源文件更省事。5.3 运行时路径兼容sys._MEIPASS的常见坑打包后最常见报错是找不到图片文件。PyInstaller的onefile模式会把程序解压到临时目录当前工作目录未必是exe所在目录。处理办法是在代码里加路径兼容逻辑开发时用项目目录打包后用exe旁目录或临时解压目录。import os import sys def resource_path(relative): base_path getattr(sys, _MEIPASS, os.path.dirname(os.path.abspath(__file__))) return os.path.join(base_path, relative)getattr(sys, _MEIPASS, 默认值)是打包判断的标准写法PyInstaller运行时会向sys模块注入_MEIPASS属性指向解压临时目录开发环境下这个属性不存在就回退到脚本所在目录。所有pygame.image.load()路径都过一遍resource_path()资源和代码路径才不会在打包后分崩离析。本文还有配套的精品资源点击获取
分享:

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

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