资讯动态

用pygame从零构建飞机大战:事件循环、精灵组与碰撞检测实战

发布时间:2026/10/17 19:44:14 来源:尧图企业网站定制
简介面向Python游戏开发初学者的《飞机大战》Pygame实现资源包以经典飞行射击游戏为项目载体完整演示从环境初始化到游戏可玩的全过程帮助读者理解面向对象编程、事件驱动机制和图形用户界面设计。压缩包大小约1.66MB便于快速下载与本地运行。已有395人学习下载适合正在学习Python游戏开发或准备课程设计的学生。资源围绕Pygame核心模块展开包括pygame.init()环境准备、set_mode()窗口创建、主循环中的事件处理与状态刷新以及创建飞机与子弹对象、用Rect区域判断碰撞、用mixer加载音效、显示分数等关键步骤。跟随代码逐行实现不仅能掌握Pygame常用API还能学会设计游戏循环、管理游戏状态最终独立完成一个包含敌我飞机、射击、得分与结束条件的完整飞行射击游戏。1. 飞机大战项目定位用 pygame 从零搭一架能上手的纵版射击游戏某开发者想学 pygame 又不想只看文档空转于是从一款经典飞机大战开始练手。做完之后最直观的感受是pygame 没想象中那么玄学核心就是「初始化窗口、循环处理事件、更新精灵、重绘画面」四件事而飞机大战恰好把每一件都落到具体代码里。这个项目就是一个完整的纵版射击游戏主角机用键盘左右移动、空格连射子弹、敌机从屏幕顶随机刷新下落、子弹与敌机碰撞计分、敌机撞到主角机则游戏结束。适合两类人一是刚接触 pygame、想跑通完整游戏闭环的初学者照着代码敲一遍就能理解游戏循环和精灵机制二是想快速搭射击类游戏原型的开发者直接拿精灵组和碰撞检测的写法改一版自己的玩法。代码量不大但结构是完整的不是玩具 Demo。2. 技术架构拆解事件循环、精灵组与碰撞检测的三层设计2.1 目录结构与模块划分拿到项目先看目录我习惯把 pygame 工程拆成「入口 场景 对象」三层这个飞机大战也是这么组织的aircraft_war/ ├── main.py # 入口初始化、主循环、场景切换 ├── settings.py # 常量屏幕尺寸、帧率、颜色、速度参数 ├── player.py # 主角机类 ├── bullet.py # 子弹类 ├── enemy.py # 敌机类 ├── game.py # 游戏场景逻辑生成、碰撞、计分 └── resources/ # 图片与音效资源目录main.py 只负责启动和主循环不掺业务逻辑game.py 是调度中心把主角机、子弹、敌机三类对象串起来settings.py 把散落的魔法数字收拢成常量后续调手感不用满文件翻。settings.py 里常见的参数如下参数名典型值作用SCREEN_WIDTH480窗口宽度SCREEN_HEIGHT700窗口高度FPS60主循环锁帧率PLAYER_SPEED6主角机横向移动速度BULLET_SPEED-12子弹飞行速度负值向上ENEMY_SPEED_RANGE(3, 7)敌机下落速度随机区间这样拆的好处是定位 bug 快飞机不动先查 player.py子弹不消失查 bullet.py敌机生成太密查 game.py 里的生成间隔。二次开发加新角色时照着 enemy.py 复制一个类改改参数就行。2.2 主循环帧率控制与事件分发pygame 游戏全部建立在主循环上这个项目的 main.py 核心循环长这样import pygame from settings import SCREEN_WIDTH, SCREEN_HEIGHT, FPS from game import Game def main(): pygame.init() screen pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) pygame.display.set_caption(飞机大战) clock pygame.time.Clock() game Game(screen) running True while running: # 1. 事件阶段处理用户输入和系统事件 for event in pygame.event.get(): if event.type pygame.QUIT: running False # 2. 更新阶段让所有精灵执行各自的 update 逻辑 game.update() # 3. 绘制阶段先清屏再画出所有对象 game.draw() pygame.display.flip() # 4. 锁帧控制循环速度避免 CPU 满载 clock.tick(FPS) pygame.quit() if __name__ __main__: main()四个阶段各有分工。事件阶段把鼠标点击、键盘按键从系统队列里取出来这里只处理了 QUIT键盘输入放到了精灵的 update 里统一读更新阶段让所有精灵按各自速度移动、按各自规则发射绘制阶段把所有对象画到后台缓冲再由 flip 整帧翻到前台。clock.tick(FPS) 把循环锁在每秒 60 轮如果不锁循环会以 CPU 能跑多快就跑多快风扇直接起飞游戏速度也会失真。2.3 精灵组为什么碰撞检测离不开 Grouppygame.sprite.Sprite 和 pygame.sprite.Group 是这个项目最值钱的设计。单个精灵只是「图片 位置矩形」的组合但放进 Group 之后update() 可以批量调用、draw() 可以批量绘制、碰撞检测可以一对多批量判定。game.py 里创建三个组self.all_sprites pygame.sprite.Group() self.bullets pygame.sprite.Group() self.enemies pygame.sprite.Group()all_sprites 管所有可见对象的统一绘制bullets 和 enemies 单独成组是为了做碰撞过滤。子弹只跟敌机撞敌机只跟主角机撞如果全塞进一个组里子弹跟主角机碰撞的误判会让你排查到怀疑人生。这个「全量绘制组 按角色拆分碰撞组」的模式是 pygame 项目里最实用的组织方式后面加道具、加 Boss 都是照这个套路再开新组。3. 核心模块实战主角机、子弹、敌机与计分链路3.1 主角机类键盘控制与边界约束player.py 里的主角机是整个游戏的交互核心。继承 pygame.sprite.Sprite初始化时创建 Surface 作为飞机外形rect 记录位置update 里实时读键盘状态class Player(pygame.sprite.Sprite): def __init__(self, screen_width, screen_height): super().__init__() self.image pygame.Surface((50, 60)) self.image.fill((0, 200, 255)) # 先用色块代替图片素材 self.rect self.image.get_rect() self.rect.centerx screen_width // 2 self.rect.bottom screen_height - 30 self.speed 6 self.screen_width screen_width def update(self): keys pygame.key.get_pressed() if keys[pygame.K_LEFT] or keys[pygame.K_a]: self.rect.x - self.speed if keys[pygame.K_RIGHT] or keys[pygame.K_d]: self.rect.x self.speed # 边界约束不让飞机飞出画面 if self.rect.left 0: self.rect.left 0 if self.rect.right self.screen_width: self.rect.right self.screen_width这里有两个关键点。第一是 pygame.key.get_pressed() 返回当前所有按键的实时布尔状态适合做「按住持续移动」如果改用事件里的 KEYDOWN 逐次处理会出现按一下只挪一下的情况手感差很多。第二是边界约束放在 update 末尾用 rect.left 和 rect.right 直接钳制比判断 centerx 更直观——rect 的左右边界和图片视觉边界完全对齐不会出现飞机半截飞出屏幕的穿帮画面。speed 设为 6 意味着每帧最多移动 6 像素60 帧每秒就是 360 像素每秒横穿 480 宽的屏幕大约 1.3 秒这个手感在射击游戏里算适中的。3.2 子弹类对象生命周期与越界回收子弹是这个项目里最容易踩性能坑的地方。每按一次空格就 new 一个 Bullet如果不做回收玩十分钟可能积攒几千个飞出屏幕的子弹对象驻留内存。这个项目用「越界自毁」来管理生命周期class Bullet(pygame.sprite.Sprite): def __init__(self, x, y, screen_height): super().__init__() self.image pygame.Surface((6, 14)) self.image.fill((255, 255, 0)) self.rect self.image.get_rect() self.rect.centerx x self.rect.bottom y self.speed -12 # 负值向上飞行 self.screen_height screen_height def update(self): self.rect.y self.speed # 核心飞出屏幕顶部就销毁自己 if self.rect.bottom 0: self.kill()自毁阈值用 rect.bottom 0 而不是 rect.y 0是因为子弹图片有 14 像素高度只要图片最低点越出屏幕顶就可以安全销毁不会出现「看着还在屏幕里却被提前移除」的断裂感。speed 取负值表示向上方向本质就是速度的正负号改一行就能复用这个类做敌方向下发射的子弹。发射子弹的触发逻辑放在 game.py 里检测到空格按下时从主角机当前位置生成一颗子弹并加入两个组。3.3 敌机生成与碰撞检测groupcollide 的返回值敌机类核心是随机生成参数合格做法是在init里用 random 决定出生横坐标和下落速度import random class Enemy(pygame.sprite.Sprite): def __init__(self, screen_width, screen_height): super().__init__() self.image pygame.Surface((40, 30)) self.image.fill((220, 60, 60)) self.rect self.image.get_rect() self.rect.x random.randint(0, screen_width - self.rect.width) self.rect.y -self.rect.height self.speed random.randint(3, 7) self.screen_height screen_height def update(self): self.rect.y self.speed if self.rect.top self.screen_height: # 飞出屏幕底部 self.kill()碰撞检测在 game.py 里用 pygame.sprite 的内建函数一次搞定不需要手写矩形相交判断# 子弹撞敌机子弹和敌机都销毁每击毁一架加 10 分 hits pygame.sprite.groupcollide(self.bullets, self.enemies, True, True) for enemies_hit in hits.values(): self.score 10 * len(enemies_hit) # 敌机撞主角机直接判负 crashed pygame.sprite.spritecollide(self.player, self.enemies, True) if crashed: self.game_over()groupcollide 返回字典键是第一个组里发生碰撞的精灵值是与该精灵碰撞的第二个组精灵列表。第四个参数 True 表示碰撞后从组里销毁敌机第三个参数销毁子弹。spritecollide 判断的是矩形相交对飞机这种不规则造型会有「看起来没撞上却被判定撞了」的误差优化方案是换成 mask 像素级碰撞我在避坑章单独讲。4. 运行与调试环境配置、启动命令与资源替换4.1 环境准备Python 版本与 pygame 安装跑这个项目先把环境备好。pygame 对 Python 3.8 以上的兼容性都很稳建议直接用虚拟环境装避免污染系统 Python# 创建并激活虚拟环境Windows 用 venv\Scripts\activate python -m venv venv source venv/bin/activate # 安装 pygame pip install pygame # 验证安装是否成功 python -c import pygame; print(pygame.version.ver)能打印出版本号说明 pygame 已就位。常见问题是 pip install 显示成功但 import 报 ModuleNotFoundError多数原因是机器上装了多个 Python 版本pip 和 python 指向了不同的解释器。验证命令里直接用 python -c 而不是 pip show就是为了确认「当前这个解释器能不能 import 到 pygame」这一步能帮你避开大部分环境玄学。4.2 启动方式与基本操作环境就绪后进入项目目录直接跑python main.py启动后会弹出 480x700 的窗口方向键或 WASD 控制左右移动空格发射子弹。判断运行是否正常看三点窗口标题是不是「飞机大战」、飞机能不能跟随按键流畅移动、子弹击毁敌机后分数是否增加。这个项目没有做动态难度曲线敌机生成频率是固定的想调难度去 settings.py 改生成间隔和敌机速度区间即可不需要改逻辑代码。提示窗口弹出一秒内就崩溃九成是图片或字体资源路径写错先检查 resources 目录下的文件是否存在、文件名是否区分大小写。4.3 资源替换从色块到图片素材项目默认用色块代替飞机图片跑通后再换正式素材。替换的合格做法是修改每个精灵的init把 Surface 填色改成 pygame.image.loadself.image pygame.image.load(resources/player.png).convert_alpha() self.rect self.image.get_rect()convert_alpha() 很关键它把图片转成带透明通道的格式让 PNG 的透明背景在游戏里真正透明。如果不调这个转换很多系统上 PNG 的透明区域会渲染成黑色块这是 pygame 新手最容易翻车的位置之一。图片尺寸不统一没关系get_rect() 会根据实际尺寸生成矩形只要别让图片比窗口还大就行。音效资源同理pygame.mixer 加载 WAV 或 OGG 格式最稳MP3 在部分环境会出现解码延迟建议统一转格式再放进去。5. 避坑记录pygame 开发中的高频问题与排查方案5.1 黑屏或窗口无响应现象窗口弹出来了但全黑鼠标转圈看起来像卡死。原因主循环里漏掉了 pygame.display.flip()。pygame 的绘制是双缓冲机制所有 draw 操作画在后台缓冲上不调 flip 画面永远切不到前台显示。另一种可能是在循环里用了 time.sleep 阻塞把事件处理也堵住了。解决确认循环末尾一定有 flip() 或 update()需要控制节奏时用 clock.tick(FPS) 而不是 sleep。每次写完绘制逻辑先跑一遍确认画面刷新再继续加逻辑别攒一堆代码再调试。5.2 游戏越玩越卡现象开局很流畅玩几分钟后帧率明显下降敌机移动像慢动作。原因子弹和敌机飞出屏幕后没有调用 kill()。kill() 会把精灵从所有 Group 里移除不调的话对象虽然看不见了但仍在 Group 里每帧执行 update 和碰撞检测对象越积越多碰撞检测耗时随对象数量线性上涨。解决给每类精灵的 update 末尾加越界判断飞出屏幕就 self.kill()。排查时可以临时打印 len(self.enemies)如果这个值随时间单调上涨说明回收逻辑没写全。5.3 碰撞检测不准确现象子弹明显没碰到敌机却被判定命中或者飞机擦着敌机边缘过去了却没触发游戏结束。原因默认碰撞基于 rect 矩形相交对非矩形图片有天然误差。飞机图片如果是三角形或带镂空造型矩形面积远大于视觉面积就会出现「空爆」。解决改用 pygame.sprite.collide_mask 做像素级碰撞按图片的非透明像素计算真实相交。使用前给精灵设置 mask再通过 collided 参数传入# 在精灵 __init__ 里生成 mask self.mask pygame.mask.from_surface(self.image) # 碰撞时指定 collided 参数 hits pygame.sprite.groupcollide( self.bullets, self.enemies, True, True, collidedpygame.sprite.collide_mask )mask 碰撞计算量比 rect 大几十个对象毫无压力但别在几百个对象上全部用 mask性能会明显下降。实践做法是玩家子弹对敌机用 mask子弹对子弹这类高频小对象用 rect。5.4 按键反馈黏滞现象按一下方向键飞机连续移动两次或者松开按键后还在动。原因事件队列里同时收到 KEYDOWN 和系统按键重复产生的多个事件。移动逻辑如果写在 KEYDOWN 分支里按住键就会触发多次感觉按键「黏」住了。解决移动逻辑从事件分支挪出来改用 pygame.key.get_pressed() 在 update 阶段读取实时状态。这个 API 每帧反映当前键盘物理状态天然免疫按键重复。注意 get_pressed 不区分「按下瞬间」和「持续按住」涉及「按一下只触发一次」的逻辑切武器、换弹仍然要回到 KEYDOWN 事件里判断。5.5 中文文字显示乱码现象用 pygame.font.Font 渲染中文标题画面上全是方框。原因pygame 默认字体不包含中文字形直接渲染中文必然乱码。解决加载系统中文字体文件或者把字体文件放进 resources 目录font pygame.font.Font(resources/fonts/simhei.ttf, 24) text_surface font.render(游戏结束, True, (255, 255, 255))Windows 常见字体路径是 C:/Windows/Fonts/simhei.ttfmacOS 在 /System/Library/Fonts/PingFang.ttc。跨平台项目最稳的做法是随资源目录带一份开源中文字体避免依赖系统环境。字体加载失败会直接抛异常所以启动时先验证字体路径再进入主循环。6. 二次开发指南道具系统与 Boss 战的改造路线玩通之后这个项目的改造空间比想象中大。给三条具体的改造路线按难度从低到高排。第一步加道具掉落。在敌机被击毁时用随机数决定是否生成道具精灵道具类复用 Enemy 的「从上往下落」逻辑只是碰撞对象从子弹换成主角机效果可以是三发子弹、护盾或加速。这一段的核心是「同一个下落逻辑不同的碰撞行为」改造时把 Enemy 被击毁的处理抽成一个独立方法在 groupcollide 返回后调用不要散落在 game.py 各段。道具的碰撞检测单独开一个 powerups 组与 enemies 组区分开避免道具也被子弹打掉。第二步加 Boss 战。在 game.py 维护一个 boss 对象到达分数阈值后切换场景状态。Boss 移动用正弦函数控制横向摆动比直线来回移动的视觉效果好很多。这里有个血泪经验Boss 血条用两个嵌套矩形绘制内层宽度按剩余血量百分比缩放玩家对血条变化有直觉感知比显示数字更有反馈。Boss 发射的子弹要记得加进碰撞检测组我第一次做弹幕时忘了把 Boss 子弹 add 进 bullets 组结果玩家被「看不见的子弹」打死排查了很久才发现是漏了这一步。第三步加音效和粒子特效。爆炸特效用多个小圆形精灵组成的粒子组实现粒子在 update 里逐渐缩小、降低透明度缩到零就 kill和子弹回收是同一个套路。音效统一用 WAV 或 OGG 格式加载后设置音量别让音效和背景音乐抢音量pygame.mixer 默认 8 个声道同时播放多个音效不够用就调 mixer.set_num_channels。做完这些改造我记得最深刻的一次是加 Boss 弹幕时忘了把 Boss 子弹加进碰撞组玩家被隐形子弹打死查了两小时才发现是 add 的位置写错了。从那以后我每次加新对象都强制走一遍「创建 → 加组 → 定义碰撞 → 验证死亡回收」四步流程顺序只要乱一步游戏就会出各种奇怪问题。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑