简介:Python飞机大战小游戏源码包,面向渴望通过实战巩固基础的Python初学者,以及需要课程配套案例的师生。它用一个可运行的经典弹幕游戏串联变量、数据类型、条件语句、循环、函数等核心知识点,省去繁琐环境配置即可边玩边学。包内共92个文件,涵盖77张PNG图片素材、9张GIF动图、WAV与MP3音效,另有主程序、数据库及备份压缩包,整体仅11.14MB,代码与素材分离,便于理解游戏资源和逻辑结构。已有1595人参与学习,足以印证其入门价值。这份资源提供从界面绘制、音效播放、碰撞检测到计分更新的完整参考实现,读者可直接体验游戏,或逐行研读源码理清Pygame事件循环与对象管理,还能替换贴图、音效和关卡参数,将学到的语法融会贯通,是比较理想的Python编程进阶练手项目。
1. 飞机大战不只是练手:一份能跑通全流程的 Python 教学样本
很多人把飞机大战归类为“新手玩具”,实际上它踩中了 Python 游戏开发里最核心的几个技术点:事件循环、碰撞检测、精灵管理和资源加载。这份源码里最值得看的不是那架飞机怎么画,而是它的代码组织方式——主程序 FeiJiDaZhan.py 之外还带了 music 目录、图片资源和 git 版本控制文件,说明它不是随手写的脚本,而是有意识地按项目结构去组织的。
对初学者来说,这个资源的价值在于能边玩边改,变量、条件、循环、函数这些概念都能在真实代码里找到对应位置;对已经有几年经验的开发者,它反而更像一个“反例样本”,你可以拿它来讲清楚什么样的代码需要重构、什么时候该引入对象池、哪些性能问题会在游戏场景里被放大。这篇博文就按“拆源码 → 跑起来 → 改逻辑 → 做课程的思路来推进,最后收在几招能直接用上的调试和打包技巧上。
2. 拆解 FeiJiDaZhan.py:pygame 窗口、事件循环与精灵框架
2.1 程序的起点:窗口初始化不是一行 print
打开 FeiJiDaZhan.py,第一眼看到的通常是 pygame.init() 加一个屏幕尺寸常量。很多教程把这两行当成固定仪式,但实际开发里这里藏着两个值得注意的参数问题。第一个是 pygame.init() 会一次性初始化所有 pygame 模块,包括 mixer、font、joystick 这些你未必用到的部分,加载时间会长一些,若你对启动速度敏感,可以改成只初始化需要的模块。
import pygame # 只初始化游戏核心模块,避免 mixer、font 等模块的无效加载 pygame.display.init() pygame.font.init() # 也可以先初始化混音器,用于加载背景音乐 pygame.mixer.init(frequency=44100, size=-16, channels=2, buffer=512) SCREEN_WIDTH = 480 SCREEN_HEIGHT = 700 screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) pygame.display.set_caption("飞机大战 FeiJiDaZhan")这段代码里frequency和buffer是两个常被忽略的参数:frequency=44100是 CD 音质标准,能兼容绝大多数音频素材;buffer=512控制音频缓冲大小,数值越小声音延迟越低,但对老机器或大体积音乐文件容易爆音。跑飞机大战这类素材较小的游戏,512 是比较合理的中间值。如果你换了更大的背景音乐,发现开枪音效有哒哒哒的爆裂声,先把 buffer 调到 1024,再考虑压缩音频文件。
2.2 事件循环:while True 背后的三件事
游戏循环是这份源码里最值得逐行读的部分。它不是简单的“每隔多少帧刷新画面”,而是一个包含事件处理、逻辑更新、画面渲染三件事的循环体。很多初学者在这三个步骤的顺序上犯错误,比如先渲染再处理按键,导致操作比画面慢一帧,体感就是“飞机总慢半拍”。
while True: # 1. 事件处理:从事件队列里取出这一帧的输入 for event in pygame.event.get(): if event.type == pygame.QUIT: pygame.quit() exit() elif event.type == pygame.KEYDOWN: if event.key == pygame.K_SPACE: hero_fire() # 2. 逻辑更新:移动飞机、更新子弹坐标、检测碰撞 keys = pygame.key.get_pressed() if keys[pygame.K_LEFT]: hero_rect.x -= hero_speed update_bullets() update_enemies() # 3. 画面渲染:先画背景,再画角色,最后翻页 screen.blit(bg_image, (0, 0)) screen.blit(hero_image, hero_rect) pygame.display.flip() clock.tick(60)这里有几个关键点需要解释:pygame.event.get()取出的是一帧内发生的所有事件,它是队列式的,如果这帧循环不做处理,事件就丢了;pygame.key.get_pressed()返回的是当前键盘状态,适合持续移动,但它们混用时要小心——按住方向键和连按空格在这个逻辑里没有冲突,是因为 fire 动作在 KEYDOWN 分支里,而移动在轮询状态里。clock.tick(60)锁帧率,把循环稳定在 60 FPS,不加这行游戏跑多快完全取决于 CPU,飞机直接变成瞬移。
2.3 精灵与精灵组:把飞机、子弹、敌机装进同一个集合
源码里如果直接看到pygame.sprite.Sprite子类,那说明作者已经用了比较规范的面向对象写法;如果只是字典加列表,你可以在阅读时把它当作重构练习。精灵类的核心优势不只是封装,而是它能挂到Group上做批量操作:group.update()调所有精灵的 update,pygame.sprite.groupcollide()一次性处理两组的碰撞。
class Bullet(pygame.sprite.Sprite): def __init__(self, x, y): super().__init__() self.image = pygame.Surface((4, 10)) self.image.fill((255, 255, 0)) self.rect = self.image.get_rect() self.rect.midbottom = (x, y) self.speed = -12 # 负值代表向上 def update(self): self.rect.y += self.speed if self.rect.bottom < 0: # 飞出屏幕就回收 self.kill()这个类里self.rect是精灵碰撞和定位的核心属性,pygame 内置的碰撞检测全依赖它。self.image先用Surface((4, 10))生成了一个纯色矩形,方便调试时肉眼确认子弹的移动轨迹。self.kill()是精灵组提供的生命周期方法,调用后子弹会从它所属的 Group 里移除,这是做对象回收最直接的方式。我一般会在这个阶段特别给学生看一个东西:物品飞出屏幕后如果不 kill,精灵组里的对象只会越来越多,FPS 会断崖式下跌,内存占用肉眼可见地涨。
2.4 资源素材的组织方式:.gitattributes 与 media 目录的用意
这个资源包带了.gitattributes,里面通常是文本属性的声明,例如统一换行符、标记二进制文件,防止音乐和图片在版本控制里被误改成乱码。在配合 Course 场景使用时,我会建议把资源目录固定成这样的结构,尤其是多个学员协作的课程项目:
| 目录/文件 | 职责 | 说明 |
|---|---|---|
| FeiJiDaZhan.py | 主入口 | 单一执行文件,含主循环 |
| music/ | 音频资源 | wav、ogg、mp3 分开放 |
| images/ | 贴图素材 | 飞机、敌机、背景 PNG |
| fonts/ | 字体文件 | 用于显示分数等 UI 文本 |
| .gitattributes | 版本控制属性 | 锁定素材文件的换行符和二进制识别规则 |
这个表格不是为了凑版面。实际开发里,.gitattributes如果缺失,在 Windows 和 macOS 双人协作时很容易出现素材文件被 Git 判定为改动的现象,因为两个系统对文本换行的处理不同。把图片和音频声明为-text就能避免这个问题,这也解释了为什么资源包里会有这样一个平时不会多看一眼的文件。
3. 子弹、敌机与碰撞检测:游戏循环里的状态管理
3.1 三种碰撞检测的取舍
飞机大战类的碰撞检测没有想象中复杂,但一旦敌机换成不规则形状,判定精度会直接影响手感。源码里最常用的是rect.colliderect()或者spritecollide(),它们背后是矩形相交检测,速度快,但矩形区域往往比飞机实际图形大一圈,玩家会感觉“没撞上也判定撞上了”。
import pygame def check_collision(hero_group, enemy_group): # 返回一个字典 {被撞的敌机: [撞到它的子弹列表]} hit_dict = pygame.sprite.groupcollide( hero_group, enemy_group, True, # 英雄是否消失,Ture 表示同归于尽 False # 敌机不消失,方便处理爆炸动画 ) for enemy, bullets in hit_dict.items(): # 这里写分数累加或爆炸效果触发的逻辑 enemy.show_explosion() return hit_dictgroupcollide()的四个参数是group1, group2, dokill1, dokill2。第一个布尔参数控制英雄被撞后是否从组里移除,第二个控制敌机是否移除。很多人把两个都设成 True,炸完整个屏幕上什么都没了;我建议敌机侧设 False,这样爆炸动画还能在场景里展示几帧,视觉反馈更完整。如果你想要更强的判定精度,可以把collided参数改成pygame.sprite.collide_mask,它会按精灵的mask属性逐像素检测,适合子弹小、敌机不规则的场景,代价是每帧 CPU 开销会明显上升。
3.1.1 像素级碰撞的代价
飞机大战这种同屏几十个对象的小游戏,逐像素检测其实也扛得住,但要做一次pygame.mask.from_surface()预计算,而不是每帧都从 Surface 生成 mask。
class Enemy(pygame.sprite.Sprite): def __init__(self, image_path): super().__init__() self.image = pygame.image.load(image_path) self.rect = self.image.get_rect() # 预先生成像素掩码,碰撞检测时直接用 self.mask = pygame.mask.from_surface(self.image)from_surface()创建的是每个像素点的透明通道掩码,透明区域自动算作无碰撞。这个做法适合游戏中少数特殊对象用,比如 boss 的机体遮挡角度很大、与玩家擦身飞过时,矩形碰撞会让人非常沮丧。但给所有子弹都生成 mask 就属于过度设计,纯色子弹用矩形检测足够。
3.2 对象池与频繁创建销毁
飞机大战里子弹的生成速度极高,如果用Bullet()实例化然后 shoot 到 list 里,每次生成都有对象创建开销,回收时又要del,看起来没问题,但跑到子弹密集的关卡时,Python 的内存分配会让帧率产生肉眼可见的抖动。
对象池是这里最常见也最容易被讲复杂的优化方案,其实实现起来并不难。核心思路是在游戏开始时就预分配一批子弹对象,之后“发射”只是把对象的状态重置并激活,而不是新建。
class BulletPool: def __init__(self, size=100): self._pool = [Bullet(0, 0) for _ in range(size)] self._active = [] def get_bullet(self, x, y): if self._pool: bullet = self._pool.pop() bullet.rect.midbottom = (x, y) bullet.alive = True self._active.append(bullet) return bullet return None # 池不够用时可以选择扩容或放弃发射把对象池逻辑写进游戏后要注意一个问题:sprite.Group的add()和remove()本身就是高频操作,对象池与Group配合时,get 和 reuse 的顺序必须严格配对,否则一个子弹飞出屏幕后没有回收到池子里,久而久之池就空了。调试时可以在池的 get 方法里加一条计数器日志,确认回收率在 95% 以上。
3.3 分数、生命值与状态机
飞机大战的分数系统看起来只是“加数字”,但它牵扯到游戏状态切换。源码中一般会出现game_state这类全局变量,值可能是"menu"、"playing"、"gameover"。问题在于初学者会把状态判断散落得到处都是,我在这个项目里见过二十多个 if 里重复判断同一个状态变量。
比较好的做法是把状态判断收敛到主循环的分支函数里,而不是在多个函数里反复查状态。代码结构可以这样组织:
def game_mode(): for event in pygame.event.get(): if event.type == pygame.KEYDOWN and event.key == pygame.K_RETURN: return "playing" return "menu"这是一个非常朴素的有限状态机雏形,状态之间的迁移路径都是显式的。加暂停功能时你会立刻体会到这种写法的价值——只需要新增一个"pause"状态,再在循环开头加分支跳转,其他代码完全不用动。有好几年经验的开发者拿这个项目练手,我建议直接把它重构成类化的状态栈,而不是继续用字符串变量,因为状态多起来之后,字符串比较会让代码越来越难读。
4. 音乐与资源加载:把散落素材组织成可维护项目
4.1 音乐循环与音效分离
带 music 目录的飞机大战源码,通常混淆了背景音乐和音效的使用方式。背景音乐 BGM 和开枪、爆炸等音效在技术上是有本质区别的,BGM 用pygame.mixer.music模块,音效用pygame.mixer.Sound。混用的后果是开枪音效播放时,背景音乐会突然卡顿。
import pygame def init_audio(): pygame.mixer.music.load("music/bg.ogg") pygame.mixer.music.set_volume(0.6) pygame.mixer.music.play(loops=-1) # 循环播放 fire_sound = pygame.mixer.Sound("music/fire.wav") explosion_sound = pygame.mixer.Sound("music/explosion.wav")pygame.mixer.music是独立的音频通道,而Sound对象会占用 mixer 的普通通道。loops=-1表示无限循环,适合背景音乐。set_volume(0.6)的参数范围是 0.0 到 1.0,超过 1.0 会导致混音削波,声音变成刺耳的破音。有几个值得注意的点:wav 格式未经压缩、加载快、适合音效;ogg 压缩率高、适合音乐本身较大的场景。原来代码里如果背景音乐循环时有“咔嗒”声,多数不是代码问题,而是音频文件本身没有做无缝循环设计,曲尾和曲首之间有静音段。
4.1.1 音频格式的选型表
不用把素材全部转成同一种格式,按用途区分是最合适的。实际编码时,加载前最好先检查文件是否存在,否则打包后换一台机器运行,路径问题会直接让游戏在启动时崩溃。
| 用途 | 推荐格式 | 原因 | 注意事项 |
|---|---|---|---|
| 背景音乐 | OGG | 体积小、循环无缝、pygame 原生支持 | 避免使用过高码率,128kbps 足够 |
| 开枪/爆炸音效 | WAV | 即按即放,无需解码延迟 | 采样率统一 44100Hz,否则可能变调 |
| 菜单点击 | WAV | 时长极短,解码成本越低越好 | 16bit 单声道能进一步减小体积 |
4.2 游戏循环中的音频卡顿排查
飞机大战的音频卡顿几乎都来自两个地方:一是加载音频时在循环里读了磁盘文件,二是mixer.Sound.play()被高频调用导致通道数爆满。第二个情况更隐蔽,当同一帧发射三发子弹,每一发都调用play(),系统会去抢空闲通道。pygame.mixer.set_num_channels(16)可以调整总通道数,但更稳妥的是对高频音效做自带的通道槽位控制。
fire_sound_played = False def play_fire(): global fire_sound_played # 控制音效播放频率,防止连射时音轨重叠 if not fire_sound_played: fire_sound.play() fire_sound_played = True pygame.time.set_timer(pygame.USEREVENT + 1, 100)set_timer在这里充当了一个“解禁计时器”,每 100 毫秒允许播放一次音效。连射时音效不会糊成一团,体感上也仍然有连续开火的效果。这个思路在处理爆炸音效时同样适用,尤其是敌机成群被消灭时,十几个爆炸音效叠在一起非常吵。
4.3 素材路径与打包后的资源目录
源码包里的飞机图片、音乐文件如果直接用相对路径加载,比如"music/bg.ogg",在开发时没有问题,一旦用 PyInstaller 打包成 exe 或者其他平台的可执行文件,路径就失效了,因为可执行文件的工作目录是系统按启动位置决定的,而不是源码文件夹。
import sys import os def resource_path(relative_path): """兼容 PyInstaller 打包后的资源目录解析""" base_path = getattr(sys, "_MEIPASS", os.path.abspath(".")) return os.path.join(base_path, relative_path) bg_music = resource_path("music/bg.ogg")这里的_MEIPASS是 PyInstaller 在打包模式下注入的临时解压目录,普通运行环境下它不存在,就会回退到当前路径。这个函数几乎是所有带资源的 Python 游戏打包时必写的桥接代码,没有它,游戏在自己电脑上跑得很好,发给别人双击就报“找不到文件”。调试这类路径问题时可以先用os.path.exists()打点,确认实际加载路径是不是自己预期的那一个。
5. 从课程作业到课堂项目:改造、调试与发布技巧
5.1 两个检查点:先验证声音是否被混音器占用,再检查对象数量
把这份源码当课程项目用时,不建议一开始就让学员照抄代码,而是先跑通原版,再动手改造。跑通后立刻检查两件事:按住空格键持续开火,观察 FPS 是否下跌;连续击杀多个敌机,观察爆炸音效是否出现爆音。这两个检查点是理解后面性能优化的抓手。调试时可以在主循环里加一个临时的对象计数打印,把子弹和敌机的数量输出到终端,能直观看出对象池和 kill 机制是否正常工作。
5.2 一张表帮你把“能玩”变成“能教”
课程项目的评分标准应该比“能跑”更细,这里是我常用的拆解方式,可以直接拿去做课堂验收表或自测清单:
| 检查维度 | 具体信号 | 常见问题 |
|---|---|---|
| 代码结构 | 是否用了类管理飞机/子弹/敌机 | 全部写在全局变量里,后续加功能非常难 |
| 事件处理 | 按键响应是否即时、无粘连 | 依赖 sleep 控制速度导致操作卡顿 |
| 碰撞反馈 | 爆炸效果与计分是否同步 | 敌机消失但分数未加 |
| 资源加载 | 是否支持打包后运行 | 直接用相对路径,打包即崩 |
| 性能基线 | 同屏 50 个对象时帧率稳定 60 | 对象池没做或 kill 条件不严格 |
5.3 一条最值得尝试的改造路线
如果你不想只替换贴图和音乐参数,可以试着把固定波次的敌机刷新改成基于累积帧的波次生成器,这会让游戏难度曲线更平滑。常见做法是用变量记录已生成的波次,每波结束后缩短下一波的出现间隔,这样“难度递进”就成了一个可以用数字调节的参数,而不是在多处代码里各改各的。改造完跑 10 分钟,记录击杀数、死亡位置、帧率曲线,这才是把一个源码下载包真正变成自己能力的过程。
本文还有配套的精品资源,点击获取