简介:这是一份基于Python与Pygame实现的飞机大战小游戏完整项目源码,主要面向期末大作业、课程设计等场景,也可作为Pygame入门学习的参考项目。代码中包含详细注释,结构清晰,对新手友好,能帮助理解游戏循环、精灵类、碰撞检测、音效播放等核心概念。项目从玩家操控、敌机生成、子弹发射到碰撞爆炸、得分计分,模块划分明确,方便读者按需阅读和二次开发。资源包共60个文件,涵盖5个Python源码文件、39张游戏贴图与界面图片、12个WAV音效及1个背景音乐OGG文件,同时附带字体文件、文本说明和Markdown文档,整体仅1.65MB,部署简单,下载后即可运行体验。目前已有168人学习下载,适合需要快速完成高分作业或希望系统学习Pygame游戏开发的同学参考借鉴,项目功能完善、界面美观、操作简单,具有较高的实际应用与学习价值。
1. 为什么“飞机大战”总是 Python 期末大作业的高分标答
飞机大战是 Python 小游戏里的常青树,期末交这个题目的人不少,但高分真不多。原因在于这道题看起来简单,实际要完整跑通,必须在 1000 行左右的规模里把 Python 的类、模块、事件循环、碰撞检测、资源加载全部串一遍。老师打开一份名为python和pygame飞机大战小游戏项目源码(高分期末大作业).zip的作业时,第一眼不会去看你特效多炫,而是先看代码是不是一坨。真正的高分标答不是“能玩”,而是“能讲”:每个文件知道为什么存在,每个参数知道为什么是这个值。下面这套从环境、运行、拆模块到打包演示的完整写法,就是照着这个标准来的。
2. 先搭 Python + pygame 环境,让飞机大战窗口先跑起来
2.1 装 Python 和 pygame:先绕过 failed to build 'pygame' 的坑
如果你还没有可用的 Python 环境,先把官方安装包装好,安装时勾选Add Python to PATH,这是所有 python 安装教程里的第一步。装好后建议立即给项目建虚拟环境:python -m venv venv,Windows 激活命令是venv\Scripts\activate,macOS 和 Linux 是source venv/bin/activate。虚拟环境的价值在于答辩换电脑时,只要pip freeze > requirements.txt就能把依赖完整带走,不会出现“在我电脑上能跑”的尴尬。
然后安装 pygame。如果你的 Python 小版本比较新,很容易在命令行看到error: failed to build 'pygame' when getting requirements to build wheel。这个报错表示 pip 找不到匹配的 wheel,于是尝试从源码编译。老电脑缺编译器时,这个过程会卡很久,最终还报错。我一般用两条命令规避:
python -m pip install --upgrade pip wheel setuptools python -m pip install --only-binary=:all: pygame第一条先把 pip 和构建工具刷新到最新,第二条里面的--only-binary=:all:告诉 pip:所有包都只允许使用二进制发行版,不要尝试源码构建。这样如果平台确实没有 pygame 的 wheel,pip 会立即给出“找不到匹配版本”的明确结论,而不是闷头编译到半夜。
如果强制二进制后没的装,直接装社区维护的 pygame-ce:
python -m pip install pygame-cepygame-ce 在代码里依然写import pygame,API 与官方几乎完全兼容,很多新 Python 版本的预编译包反而比官方出得更快。老师问起来,你可以说“用社区维护版解决 Python 版本兼容问题”,这本身就是一个加分项。
| 命令 | 作用 | 什么时候用 |
|---|---|---|
pip install pygame | 安装官方 pygame | Python 版本较老、wheel 已发布时 |
pip install --only-binary=:all: pygame | 强制只用二进制,不编译源码 | 想快速确认环境到底缺什么 |
pip install pygame-ce | 安装社区维护版 | 官方 wheel 缺失或构建失败时 |
2.2 窗口、Surface 与事件循环:让飞机大战窗口不闪退
pygame 游戏最核心的其实是窗口循环。先把窗口跑起来,后面所有精灵、碰撞、计分才有地方放。这里是最小的可运行骨架:
import sys import pygame def main(): pygame.init() screen = pygame.display.set_mode((960, 640)) pygame.display.set_caption("飞机大战") clock = pygame.time.Clock() while True: for event in pygame.event.get(): if event.type == pygame.QUIT: pygame.quit() sys.exit() screen.fill((12, 20, 40)) pygame.display.flip() clock.tick(60) if __name__ == "__main__": main()pygame.init()会初始化显示、音频、字体等子系统。如果项目里要加载图片、音效和中文字体,全量初始化最省心。set_mode的元组(960, 640)决定窗口大小,也决定了后面所有blit的坐标上限。fill先铺满背景,flip再把缓冲区一次性推到窗口,这个过程叫双缓冲,pygame 的 GUI 绘制都是先在内存 Surface 上完成,最后统一上屏。
clock.tick(60)让主循环每秒最多跑 60 次。去掉这一行,游戏会以几百帧运行,碰撞检测偶尔会出现“子弹穿过敌机”的跳跃现象,CPU 占用也会直接拉满。常见误用是把screen.fill写在循环外,结果画面全是拖影;或者拿到event之后不做pygame.quit()和sys.exit(),导致关不掉窗口。只要保持“取事件、更新逻辑、绘制、翻页、延时”在同一循环里,窗口就不会闪退。
2.3 把资源目录约定好,避免演示时换路径就崩
很多大作业源码里到处都是"images/player.png"这类相对路径,在自己电脑上双击没问题,拷到教室电脑上从其他目录启动就崩。相对路径的参考点是“当前工作目录”,不是你源码所在目录。最稳的做法是单独建一个paths.py:
from pathlib import Path BASE_DIR = Path(__file__).resolve().parent RESOURCE_DIR = BASE_DIR / "resources" IMAGE_DIR = RESOURCE_DIR / "images" SOUND_DIR = RESOURCE_DIR / "sounds" FONT_DIR = RESOURCE_DIR / "font"Path(__file__).resolve().parent根据源码文件位置推算目录,和从哪个目录启动无关。这样项目里就能统一写from paths import IMAGE_DIR,换机器、换路径都不会崩。资源目录不存在时,尽早报错也比加载图片时抛看不懂的 traceback 好:在paths.py最后加一句assert RESOURCE_DIR.exists(), "缺少 resources 目录"足够。
3. 让飞机大战真正“打”起来:精灵、碰撞与计分
3.1 用 Sprite 类把玩家、子弹、敌机编成三个组
pygame 的sprite.Sprite是专门为“可移动绘制对象”设计的基类。继承它之后,类里必须有两个属性:image和rect。Group.draw()全靠这两个属性把图片画到屏幕上,少一个都会报错。玩家类的核心写法如下:
import pygame class Player(pygame.sprite.Sprite): def __init__(self, x, y): super().__init__() self.image = pygame.image.load("resources/images/player.png").convert_alpha() self.rect = self.image.get_rect(center=(x, y)) self.speed = 6 def update(self, keys): if keys[pygame.K_LEFT]: self.rect.x -= self.speed if keys[pygame.K_RIGHT]: self.rect.x += self.speed self.rect.x = max(0, min(self.rect.x, 960 - self.rect.width))convert_alpha()把 PNG 的透明通道转成 pygame 内存格式,这样飞机图片的透明背景不会变成黑块。rect是每帧移动的基础,修改rect.x和rect.y就是移动图片。边界判断写成max(0, min(...))比写两个if的意图更明确:先取右边界最小值,再取左边界最大值,最终保证玩家不出屏幕。
玩家、子弹、敌机应该放在三个不同的Group里。Group.update(*args)会把参数分发给组内每个精灵的update方法,例如player_group.update(keys)传键盘状态,bullets_group.update()不传参。这样主循环里不需要手写for循环,代码更省。
3.2 碰撞检测:groupcollide 的参数和返回值
游戏“打得起来”的关键碰撞。最常用的写法是groupcollide:
hits = pygame.sprite.groupcollide(enemies, bullets, True, True) for enemy in hits: score += 10 explosion_sound.play()groupcollide检查第一组里的每个精灵是否与第二组精灵重叠,返回一个字典:键是“被撞中的第一组成员”,值是一个列表,包含所有撞到它的第二组成员。后面两个True分别对应dokill_a和dokill_b:第一个 True 让被撞敌机从组里删除,第二个 True 让子弹同时删除。一枪一个,符合飞机大战的基础规则。
| 参数 | 默认值 | 作用 |
|---|---|---|
dokill_a | False | 碰撞后是否立即删除第一组精灵 |
dokill_b | False | 碰撞后是否立即删除第二组精灵 |
collided | None | 传None表示矩形碰撞;传pygame.sprite.collide_mask做像素级碰撞 |
矩形碰撞的问题是:PNG 图片透明边缘依然算在rect里,经常出现“子弹离敌机还有 5 像素就判定命中”的违和感。想要精确,把参数改成pygame.sprite.groupcollide(enemies, bullets, True, True, pygame.sprite.collide_mask)。像素级碰撞根据图片非透明区域判断,观感真实,代价是 CPU 消耗变大。飞机大战同屏对象的数量一般不会超过几十个,这个开销可以忽略。
3.3 用时间戳控制敌机生成,别用 time.sleep
敌机生成的正确方式是基于毫秒时间戳做间隔判断:
now = pygame.time.get_ticks() if now - last_enemy_time >= ENEMY_SPAWN_INTERVAL: enemies.add(Enemy()) last_enemy_time = nowget_ticks()返回pygame.init()后经过的毫秒数,适合做冷却、倒计时和连发间隔。它不会暂停程序,不影响键盘响应。反过来,time.sleep(0.1)虽然也能生成间隔效果,但会阻塞整个主循环,轻则按键迟钝,重则音效断断续续。想加难度,可以让生成间隔随关卡动态变化,二.段写法:
enemy_interval = max(200, 800 - level * 80)每过一关,间隔少 80 毫秒,但不会低于 200 毫秒,避免后期敌机堆成墙。
3.4 计分与中文字体:pygame 字体渲染的隐藏条件
计分界面要显示“得分”而不是“Score”,需要处理中文字体。pygame 的Font加载字体文件时,文件不存在不报错,等到render才抛异常;系统英文字体渲染中文会变成方块。正确的做法是准备一个中文 .ttf 字体文件:
font = pygame.font.Font("resources/font/msyh.ttf", 28) score_surface = font.render(f"得分: {score}", True, (255, 255, 255)) screen.blit(score_surface, (20, 20))第二个参数True表示开启抗锯齿,文字边缘更平滑。font对象创建一次放循环外,不要每帧都重新加载字体文件——虽然一个字体文件加载几十次不至于崩溃,但答辩时老师如果盯着你的代码问性能优化,这就是第一个能答的点。
4. 面向期末大作业的飞机大战源码结构:拆成老师愿意翻的模块
4.1 一个模块只放一个对象:单文件的第一次重构
很多同学交上来的“项目源码”只有一个main.py,一千多行,老师翻到一半就失去耐心。高分大作业的源码,一定要让人能在 30 秒内找到入口和核心文件。常见的目录结构如下:
plane_war/ ├─ main.py ├─ settings.py ├─ paths.py ├─ sprites/ │ ├─ __init__.py │ ├─ player.py │ ├─ enemy.py │ └─ bullet.py ├─ ui.py └─ resources/ ├─ images/ ├─ sounds/ └─ font/main.py只放启动、主循环和状态切换;sprites/目录里每个文件负责一种精灵;ui.py负责血条、得分和菜单绘制。这个结构本身就是答辩提纲。sprites/下必须有一个__init__.py,Python 3 里它让目录变成一个包,之后才能写from sprites.player import Player。资源目录不需要__init__.py,它不是被导入的模块。
4.2 settings.py 集中管理参数,避免“魔法数字”
同一份源码里如果960、6、800散落在多个文件,你会发现自己调一次手感要改十几个位置。把所有可变参数集中到一个settings.py里,是成本最低的架构优化:
# settings.py SCREEN_WIDTH = 960 SCREEN_HEIGHT = 640 FPS = 60 PLAYER_SPEED = 6 BULLET_SPEED = -12 ENEMY_SPAWN_INTERVAL = 800 ENEMY_SPEED = 3| 参数 | 建议值 | 作用 |
|---|---|---|
SCREEN_WIDTH | 960 | 窗口宽度,也是边界判断的右边界 |
SCREEN_HEIGHT | 640 | 窗口高度 |
FPS | 60 | 主循环目标帧率 |
PLAYER_SPEED | 6 | 玩家每帧移动像素数 |
BULLET_SPEED | -12 | 子弹 Y 方向速度,负数表示向上 |
ENEMY_SPAWN_INTERVAL | 800 | 敌机生成间隔毫秒 |
ENEMY_SPEED | 3 | 敌机下落速度 |
在业务文件里用import settings,然后写settings.PLAYER_SPEED,比from settings import *更清晰。修改游戏难度只需要打开settings.py一个文件,老师拷到其他电脑上,也不用满项目找 960 是哪里的魔法数字。
4.3 用 state 状态机串起菜单、游戏中、结束
没有状态机的飞机大战只有一个“游戏进行中”分支,加个主菜单就得套一堆 flag。最轻量的状态玩法是用一个字符串变量:
state = "menu" while running: if state == "menu": draw_menu(screen) if start_key_pressed: state = "playing" elif state == "playing": update_game() draw_game(screen) if player.life <= 0: state = "game_over" elif state == "game_over": draw_game_over(screen) if retry_key_pressed: reset_game() state = "playing"state让主循环按分支执行,每个分支的代码量都很小。以后想加暂停,加一个state == "pause"分支就行。期末大作业里最常见的“菜单 → 游戏 → 结束 → 重开”闭环,用这种方式写,老师追问时你能清楚说出当前状态和数据归属。
4.4 兼容源码运行和 exe 运行的资源路径封装
写完游戏后,你一定会打包成 exe 发给老师。打包后资源文件会被解压到临时目录,源码里写死的相对路径全部失效。提前封装一个resource_path函数能同时兼容两种运行方式:
import sys from pathlib import Path BASE_DIR = Path(__file__).resolve().parent def resource_path(relative_path: str) -> Path: base = getattr(sys, "_MEIPASS", BASE_DIR) return Path(base) / relative_path源码运行时sys._MEIPASS不存在,getattr返回默认值BASE_DIR,资源路径和平时一样。PyInstaller 打包后,sys._MEIPASS指向临时解压目录,资源路径自动切换。两个模式用同一个函数,代码里不需要再写任何if判断。
5. 给飞机大战打包并录演示:答辩前最值得花时间的两个步骤
5.1 用 PyInstaller 打包,注意 --add-data 这个分号
源码跑通后,别急着把.py文件压进 zip。先用 PyInstaller 打包成单个 exe 试运行一遍。命令如下:
python -m PyInstaller --onefile --windowed --name plane_war --add-data "resources;resources" main.py--onefile把所有内容合成一个 exe,运行时自动解压到临时目录;--windowed隐藏黑色控制台,否则玩家会看到一个命令行窗口;--name设置输出文件名;--add-data把资源目录映射进临时目录,前面是源码里的资源路径,后面是打包后目标目录名。Windows 上必须用分号分隔,macOS 和 Linux 用冒号,写错会打包成功但运行时找不到图片和字体。
| PyInstaller 参数 | 作用 |
|---|---|
--onefile | 打包成单个可执行文件 |
--windowed | 不显示控制台窗口 |
--name | 设置产物文件名 |
--add-data | 附带资源文件,格式是“源路径;目标路径” |
打包后的 exe 一定要用resource_path里的逻辑加载资源。调试阶段可以临时去掉--windowed,让 print 输出显示在控制台里,方便确认resource_path返回的临时路径是否真实存在。
5.2 录两种演示片段,给老师留两个提问钩子
期末大作业的视频不需要长,1 分钟就够了。第一段录正常难度,从主菜单开始打一波敌机;第二段录参数调整对比,打开settings.py,把ENEMY_SPAWN_INTERVAL从 800 改成 300,保存后重新运行,老师能直观看到敌机密度变化。录屏时声音调低但不要静音,开火和爆炸音效能掩盖画面抖动上的小瑕疵;录屏帧率设 60,导出时用 30 帧,文件小且流畅。
如果老师让你现场演示,不要只打开游戏。先把项目目录和settings.py放在屏幕可见位置,再从命令行启动游戏,主动展示代码和进程的对应关系。如果老师问“这是你写的吗”,别急着辩解,直接改一行PLAYER_SPEED,保存重启让飞机速度明显变快,这个动作比说十句“是我自己写的”都有说服力。README 第一行放这条 PyInstaller 打包命令,老师按着做一遍能复现,遇到--add-data分号问题还能顺着你留下的注释排错。
本文还有配套的精品资源,点击获取