ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Pygame实战:从零手写飞机大战,掌握游戏开发核心流程

Pygame实战:从零手写飞机大战,掌握游戏开发核心流程 简介一份基于Pygame实现经典“飞机大战”游戏的Python开发学习资源适合对Pygame库感兴趣、希望从零构建小型2D游戏项目的初学者和进阶者。内容围绕飞机大战的完整实现思路展开覆盖Pygame核心模块、窗口创建、精灵类封装、键盘控制、子弹射击、敌机生成、碰撞检测、音效播放和帧动画等关键环节并提供了从基础框架到完整游戏循环的代码示例与逻辑拆解。除了基础玩法还针对积分、道具、升级系统以及性能优化等拓展方向给出设计建议能够帮助读者掌握Pygame游戏开发的基本方法论。资源包大小约11.13MB方便直接下载学习和参考。目前已有192人学习对于想要用Python快速上手游戏开发的人群具有一定参考价值。1. 亲手用 Pygame 写一版飞机大战从零到能玩这套资源到底讲了什么很多刚学 Python 的人都会卡在同一个地方语法看了一堆却不知道能拿来做什么。Pygame 是少有的能让你在几小时内看到“活生生”产物的库这套《Pygame 实现飞机大战》恰恰就是奔着这个目标去的——它把窗口管理、精灵对象、事件响应、碰撞检测、音效播放这些游戏开发的骨架全拆开讲了一遍最终落成一款可运行的经典竖版射击游戏。适合两类人一是 Python 基础刚入门、想做一个完整项目练手的学习者二是已经会写业务代码、想快速了解 2D 游戏开发核心流程的转行者。不需要复杂引擎不依赖美术资源一套代码搭起来你就拥有了一个能跑、能玩、能自己加料的游戏原型。2. 搭建 Pygame 开发环境窗口、事件循环和第一张黑色画布2.1 安装与版本选择pip 之外还要关注 Python 版本匹配先说安装。绝大多数情况下一条命令就能搞定pip install pygame装完后建议立刻验证一下python -c import pygame; print(pygame.version.ver)能打印出版本号说明环境没问题。如果用的是 Anaconda也可以conda install pygame但我个人更推荐 pip因为 conda 源里的 pygame 版本往往滞后而新版本会修复一些 SDL 底层的问题。这里有一个常被忽略的点Pygame 2.x 和 1.9.x 的 API 略有差异。这套资源里的代码是兼容 2.x 的写法所以安装时尽量锁定 2.0 以上版本。如果你用的是 Python 3.10 以上的新版本旧版 pygame 可能直接无法导入报错信息往往是ModuleNotFoundError或者动态链接库加载失败。遇到这种情况升级 pygame 到最新版即可。2.2 初始化窗口的规范流程display 和 event 的分工Pygame 的初始化顺序是有一套约定的顺序错了就会出现一些莫名其妙的问题。咱们看资源里给出的基础代码import pygame pygame.init() screen pygame.display.set_mode((800, 600)) pygame.display.set_caption(飞机大战)pygame.init()会一次性初始化所有已导入的模块包括 display、font、mixer 等。严格来说可以拆开逐个调用但对于飞机大战这种规模的项目统一初始化就够了。set_mode((800, 600))创建了一个 800x600 的游戏窗口这个尺寸是竖版射击游戏的经典分辨率高度够、宽度适中后续计算飞机位置时比较直观。set_caption设置了窗口标题栏文字。有一点值得说透set_mode返回的screen对象其实是一个 Surface——Pygame 里所有可见的东西都是 Surface。窗口本身是 Surface加载的图片是 Surface文字渲染出来也是 Surface。理解这一点之后再去看后面的screen.blit()和pygame.display.flip()就不会糊涂了。窗口创建完成接下来就是主循环。一个干净的游戏循环骨架长这样clock pygame.time.Clock() FPS 60 running True while running: dt clock.tick(FPS) # 返回上一帧到这一帧的毫秒数 for event in pygame.event.get(): if event.type pygame.QUIT: running False # 在这里处理键盘、鼠标事件 # 更新游戏状态飞机位置、子弹坐标、碰撞检测等 # 绘制画面 screen.fill((0, 0, 0)) # 把精灵画到 screen 上 pygame.display.flip() pygame.quit()clock.tick(FPS)是控制帧率的核心参数 60 表示让循环尽量每秒跑 60 次。这里有个细节tick()返回的是距离上次调用经过的毫秒数也就是dt。飞机大战里的移动速度最好乘以dt/1000这样无论帧率高还是低飞机的实际速度都保持一致。资源正文里的移动方式是每帧固定移动 10 像素这在 60 FPS 下没问题但如果电脑性能差掉到 30 FPS飞机就会明显变慢。我一般会写成self.rect.y - 300 * dt / 1000用每秒像素数来定义速度而不是每帧像素数。pygame.display.flip()的作用是把绘制好的内容一次性呈现到屏幕上。还有一个pygame.display.update()可以做局部更新游戏元素少时看不出差别但 flip 更保险。3. 用 Sprite 封装飞机和敌机图片加载、Rect 定位与键盘控制3.1 Sprite 类到底解决了什么问题Spirte 是 Pygame 里最值得花时间理解的概念。它本质上是把一个游戏对象的图片、位置、更新逻辑和绘制逻辑打包成一个类让你不用手动维护一堆散乱的变量。资源里的 Plane 类是一个很好的示范class Plane(pygame.sprite.Sprite): def __init__(self, img_path): super().__init__() self.image pygame.image.load(img_path) self.rect self.image.get_rect() self.rect.center (400, 500) def update(self, events): for event in events: if event.type pygame.KEYDOWN and event.key pygame.K_UP: self.rect.y - 10 elif event.type pygame.KEYDOWN and event.key pygame.K_DOWN: self.rect.y 10self.image是飞机图片对应的 Surfaceself.rect是从图片尺寸自动生成的矩形区域。Pygame 内部所有位置计算都是通过 rect 完成的它的属性包括x、y、top、bottom、left、right、center等非常方便。pygame.image.load()支持 PNG、JPG 等常见格式但游戏素材最推荐 PNG——因为它支持透明通道。如果你用的是带白色背景的 JPG飞机画出来会顶着一个白色方块那体验就很出戏了。图片素材最好做一步预处理self.image pygame.image.load(img_path).convert_alpha()convert_alpha()会把图片转换成与屏幕相同的像素格式并保留透明通道。做了这一步blit 图片时的性能会明显提升尤其是素材比较大的时候。很多新手跳过这一步游戏元素一多就卡。3.2 键盘控制的两种实现事件驱动 vs 按键状态资源里用的是事件驱动方式——按下按键的瞬间触发一次移动。这在飞机大战这种需要持续移动的场景里有一个问题按住方向键不放系统键盘重复率Windows 里设置的重复延迟决定了飞机一次能走几步手感会很飘。更稳妥的做法是使用pygame.key.get_pressed()检测按键状态def update_with_pressed_keys(self, pressed_keys): speed 300 * dt / 1000 # 每秒移动 300 像素 if pressed_keys[pygame.K_LEFT]: self.rect.x - speed if pressed_keys[pygame.K_RIGHT]: self.rect.x speed if pressed_keys[pygame.K_UP]: self.rect.y - speed if pressed_keys[pygame.K_DOWN]: self.rect.y speed这个写法把移动绑定到“按住”状态而不是“按下”事件配合上面提到的 dt 速度换算手感会和商业游戏更接近。资源正文里的写法适合做“按一下动一格”的演示真要拿来做完整游戏建议改成按键状态检测。3.3 屏幕边界限制给飞机加上活动范围没有边界限制的话玩家按几下方向键飞机就直接飞出屏幕了。飞机大战通常会把玩家活动范围限制在屏幕下半部分用 rect 的clamp方法最省事screen_rect screen.get_rect() self.rect.clamp_ip(screen_rect)clamp_ip是原地修改把 rect 强制拉回屏幕范围内。如果想让飞机不能飞出最上面三分之一就手动加一个判断if self.rect.top 200: self.rect.top 200这类边界值建议定义成脚本开头的常量方便统一调整。4. 射击、敌机生成与 groupcollide 碰撞三个常用参数决定游戏手感4.1 子弹类和敌机类复用 Sprite用 Group 统一管理游戏里子弹、敌机都是 Sprite。设计子弹类时除了图片和位置还需要一个速度属性class Bullet(pygame.sprite.Sprite): def __init__(self, x, y): super().__init__() self.image pygame.Surface((4, 10)) self.image.fill((255, 255, 255)) self.rect self.image.get_rect(center(x, y)) self.speed 600 # 每秒钟向上移动 600 像素 def update(self, dt): self.rect.y - self.speed * dt / 1000 if self.rect.bottom 0: self.kill() # 超出屏幕顶部就销毁防止内存泄露子弹这里我用pygame.Surface直接创建了一个 4x10 白色矩形作为子弹图片好处是不依赖外部素材文件。实际项目里换成子弹 PNG 图片即可。self.kill()是 Sprite 类自带的方法调用后该 Sprite 会从它所属的所有 Group 中移除。敌机类的设计逻辑类似但移动方向相反而且还需要一个销毁条件class Enemy(pygame.sprite.Sprite): def __init__(self, img_path, x, y, speed): super().__init__() self.image pygame.image.load(img_path).convert_alpha() self.rect self.image.get_rect(center(x, y)) self.speed speed def update(self, dt): self.rect.y self.speed * dt / 1000 if self.rect.top 600: self.kill()Group的用处到这里就显现出来了。把所有子弹放进一个bullet_group所有敌机放进enemy_group更新和绘制都能一句话完成bullet_group.update(dt) enemy_group.update(dt) bullet_group.draw(screen) enemy_group.draw(screen)draw(screen)会把组里所有存活精灵的 image 自动 blit 到 screen 上省去手动遍历绘制的代码。4.2 自动发射子弹定时器不是唯一方案飞机大战的射击频率通常需要稳定控制。Pygame 里可以用pygame.time.set_timer()创建自定义事件每隔固定毫秒触发一次SHOOT_EVENT pygame.USEREVENT 1 pygame.time.set_timer(SHOOT_EVENT, 300) # 每 300 毫秒触发一次射击事件然后在主循环的事件处理里响应for event in pygame.event.get(): if event.type SHOOT_EVENT: bullet Bullet(player.rect.centerx, player.rect.top) bullet_group.add(bullet)pygame.USEREVENT是 Pygame 预留的自定义事件起点可以在它基础上加偏移量来区分不同自定义事件。300 毫秒意味着每秒约 3.3 发这是飞机大战的常规火力密度。如果把子弹速度调快、发射间隔调短游戏难度就会显著增加这两组参数是做难度曲线最直接的旋钮。4.3 groupcollide 参数解释四个参数每个都别搞反碰撞检测是飞机大战的血液。资源里给出的groupcollide是 Pygame 内置的精灵组碰撞检测函数参数含义值得仔细拆一遍collisions pygame.sprite.groupcollide( bullet_group, # 第一组精灵子弹 enemy_group, # 第二组精灵敌机 True, # dokill_1: 碰撞后是否销毁第一组里的精灵子弹 True # dokill_2: 碰撞后是否销毁第二组里的精灵敌机 )groupcollide的返回值是一个字典键是第一组中发生碰撞的精灵值是与该精灵碰撞的第二组精灵列表。刚才的调用里True, True表示子弹和敌机同归于尽这是最常见的配置。返回值可以用来加分或者播放爆炸音效for bullet, enemies in collisions.items(): score 10 * len(enemies) # 每消灭一个敌机加 10 分groupcollide默认使用rect碰撞检测也就是矩形相交判断。对于飞机大战来说足够用了但要注意如果飞机图片有大量透明区域矩形碰撞会让玩家感觉“明明没碰到却被判定为碰撞”。这是矩形碰撞的固有局限进阶方案是用 mask 碰撞检测——Pygame 提供了pygame.sprite.collide_mask()它基于像素掩膜做精确碰撞性能开销比 rect 大一些但飞机不多时完全跑得动。4.4 玩家飞机与敌机碰撞的处理边界资源里还有一组碰撞是玩家和敌机的collisions pygame.sprite.groupcollide(player_group, enemy_group, False, True) for c in collisions.values(): for e in c: e.kill()这里的第一个参数传入了False意味着玩家飞机不会被销毁。这在调试阶段是有意义的——先让玩家无敌专注测试碰撞逻辑是否触发。但做成完整游戏时这里应该改成玩家飞机被击毁、游戏结束或扣生命值。实际开发中我一般会把玩家生命值做成一个变量碰撞后递减生命归零才退出collisions pygame.sprite.groupcollide(player_group, enemy_group, False, True) if collisions: player.lives - 1 if player.lives 0: running False把玩家飞机放回屏幕中央并且给予短暂无敌时间是这里手感最关键的细节。5. 飞机大战避坑专场我把走过的弯路都摊开讲5.1 事件被消费两次飞机按一下跑两格现象写游戏循环时我在while running顶部用pygame.event.get()处理了一次事件后来在player_group.update()里又传入了一次pygame.event.get()结果飞机每按一次方向键就移动两倍距离。原因pygame.event.get()每调用一次就会把事件队列清空。同一个事件不可能被取两次。资源正文示例代码里也出现了这个问题——事件处理与精灵更新都各自调用了pygame.event.get()。我第一次跑起来时飞机像抽风一样排查了半天才发现事件队列被消费了两遍。解决事件只取一次然后把事件列表传给所有需要响应事件的精灵。修正后的写法是events pygame.event.get() for event in events: if event.type pygame.QUIT: running False player_group.update(events) enemy_group.update()这也是为什么 Plane 类的update方法需要接收events参数的原因——事件必须从外部传入而不是在 update 内部再去获取。5.2 图片加载路径有中文游戏直接闪退现象素材放在D:\游戏素材\player.png运行时抛出异常控制台报错信息指向pygame.error或文件不存在。原因Pygame 基于 SDLSDL 在处理中文路径时存在历史遗留问题。Windows 下对包含中文或特殊字符的路径支持不稳定某些版本直接无法打开文件。解决项目目录全部使用英文字母图片文件也统一命名为player.png、enemy.png、bullet.png。这不仅规避了 Pygame 的路径问题也避免了跨平台时编码不一致的麻烦。我后来养成的习惯是项目目录一旦建立路径里绝不出现中文。5.3 音频文件加载失败或有杂音尤其 MP3 格式现象代码里写了pygame.mixer.Sound(bgm.mp3)要么报错说无法加载要么播放时声音卡顿、有杂音。原因Pygame 的 mixer 对音频格式的兼容性有限。MP3 的编码格式千奇百怪尤其是某些网上下载的 MP3 采用高位采样率或特殊压缩参数SDL 的音频解码器不一定能正常处理。常见的兼容格式是 WAV但 WAV 文件体积大不适合做循环背景音乐。解决背景音乐用pygame.mixer.music模块加载 OGG 格式音效用pygame.mixer.Sound加载短 WAV 文件。OGG 兼具压缩率和兼容性是游戏开发的常见选择。初始化 mixer 时也建议手动指定参数pygame.mixer.init(frequency22050, size-16, channels2)frequency22050是 CD 音质的一半体积和音质比较平衡size-16表示 16 位有符号整数采样channels2保留双声道。如果目标平台是老电脑可以把 channels 改成 1 来降低 CPU 开销。5.4 碰撞检测把整个敌机组都删了现象写groupcollide(player_group, enemy_group, False, True)时把第一个参数误传成了True结果玩家飞机和敌机一碰玩家飞机直接消失游戏立即结束。原因dokill_1对应的是第一组精灵是否销毁。第一个参数是玩家组我本意只想销毁敌机结果把玩家飞机的销毁开关也打开了。解决每次写groupcollide前先默念一句“参数顺序第一组第二组第一组是否销毁第二组是否销毁”。写完后对照注释检查一遍再运行。尤其注意False, True与True, True的差别前者多用于角色与道具的碰撞后者多用于子弹与目标的碰撞。5.5 帧率没设上限游戏在独显和高刷屏上飚到几百 FPS现象游戏窗口里的飞机移动速度快到无法控制CPU 占用率异常高风扇狂转。原因如果没有clock.tick(FPS)while running循环会以操作系统允许的最大速度疯狂执行。在高刷新率显示器上循环可能每秒跑几百次每次循环移动 10 像素飞机当然就像瞬移一样。解决游戏循环开头创建clock pygame.time.Clock()每轮循环结束前调用clock.tick(60)限制帧率。这样运动速度是按帧算的不会因为硬件性能不同而差异巨大。之前提到的按秒定义速度再乘dt是终极解帧率波动时也能保持稳定的移动速度。6. 音效、帧动画和性能优化这几行代码让游戏从 Demo 变成作品先谈音效。Pygame 的音频分成两个体系pygame.mixer.Sound负责短音效pygame.mixer.music负责背景音乐。射击、爆炸、道具拾取属于前者循环播放的 BGM 属于后者。为碰撞加入音效只需在检测到碰撞时触发播放shoot_sound pygame.mixer.Sound(shoot.wav) explosion_sound pygame.mixer.Sound(explosion.wav) # 发射子弹时 shoot_sound.play() # groupcollide 检测到碰撞时 explosion_sound.play()Sound.play()每次调用都会重新播放重叠播放时会产生多个声部这在射击游戏中反而是加分项——连续射击时声音有层次感。背景音乐的循环播放则用pygame.mixer.music.load(bgm.ogg) pygame.mixer.music.play(-1) # -1 表示无限循环有一个容易踩的坑如果在游戏主循环内频繁创建Sound对象会导致内存持续增长。正确做法是在初始化阶段把所有音频文件加载好运行时只调用play()。再谈帧动画。实现爆炸效果最直接的方式是准备一组连续帧图片循环切换class Explosion(pygame.sprite.Sprite): def __init__(self, frames, x, y): super().__init__() self.frames frames # 一组 Surface self.current_frame 0 self.image frames[0] self.rect self.image.get_rect(center(x, y)) self.timer 0 def update(self, dt): self.timer dt if self.timer 50: # 每 50 毫秒切换一帧 self.current_frame 1 if self.current_frame len(self.frames): self.kill() else: self.image self.frames[self.current_frame] self.timer 0爆炸动画的精髓不是炫酷是短——控制在 200 到 300 毫秒内播完消失在玩家视野里节奏才紧凑。最后说性能。Pygame 性能最大的杀手是图片直贴。前面提到的convert_alpha()我再说一遍它让每张图片在 blit 前转换成屏幕像素格式避免实时转换。还有一个优化点是局部刷新如果游戏元素不多pygame.display.update(rect_list)可以只刷新变化过的区域而不是全屏刷新。但飞机大战的敌机遍布全屏局部刷新的意义不大老老实实flip()就好。真正影响帧率的往往是精灵数量——几百个弹幕精灵会让 Python 的碰撞检测成为瓶颈。这时候可以做一层粗排只对rect在屏幕范围内的精灵做碰撞检测超出范围直接跳过省下大量无效计算。这套资源从零到一覆盖了 Pygame 游戏开发的主干流程但真正让游戏“好玩”的部分——移动手感、碰撞反馈、动画节奏——都需要你在骨架之上反复调试参数。我当年跑通这份资源之后又把每个速度值、每个碰撞开关挨个乱改了一遍才慢慢理解了数值设计对游戏体验的影响。那之后我做任何游戏原型都会强制走一遍这套流程先搭窗口循环再封装精灵最后补碰撞和音效。希望你也能把这套东西跑出自己的手感来希望帮到你。本文还有配套的精品资源点击获取
返回列表