简介一份基于Pygame实现的Python飞机大战游戏开发资源面向希望通过小型游戏项目巩固面向对象编程、事件驱动机制和图形界面设计的初学者。资源详细展示了Pygame环境初始化、窗口创建、主循环结构、键盘输入处理、画布刷新等基础流程并给出飞机移动、子弹发射、碰撞检测、音效播放、分数统计等关键功能的实现思路同时涉及生命值归零和敌机清空等结束判定提示可扩展道具、关卡和简单AI增强可玩性。通过整体代码逻辑与分段示例读者能理解类与对象、pygame.Rect矩形碰撞判断在实际游戏中的应用也能体会事件驱动、状态更新与渲染刷新构成的典型主循环运作方式并顺利迁移到自己的飞行射击项目中。压缩包为zip格式包体约1.66MB整体轻量平台目前暂未收录具体文件清单但不影响学习和动手复现。目前已有395人学习浏览适合高校Python课程设计、Pygame入门实践或希望快速搭建飞行射击游戏原型的开发者。1. 飞机大战项目定位用pygame复刻经典弹幕玩法的完整样本你是不是也有过这样的阶段——学了Python基础语法看了几篇面向对象教程但一到写项目就不知道从哪下手飞机大战就是那个最适合拿来开刀的项目它有循环、有事件、有碰撞、有资源加载一个小游戏把pygame的核心机制几乎全踩了一遍。这个项目用纯Python实现核心依赖只有pygame一个库玩家通过键盘控制战机躲避从屏幕上方不断涌出的敌机击落敌机得分被击中则扣血血条归零即游戏结束。适合想通过完整游戏实战夯实pygame开发的初学者也适合需要快速拿一个可跑的demo做二次开发模板的熟手。它不依赖任何游戏引擎所有代码摊开都在一个可以直接读懂的逻辑闭环里。2. 项目结构与技术选型主循环、精灵组与事件系统怎么组织写pygame项目最怕一上来就把所有逻辑塞进一个main.py几百行代码堆在一起改一个功能牵扯全身。这个飞机大战项目在结构上做了一个比较清爽的分层虽然体量不大但目录划分遵循了「入口、配置、资源、逻辑」分离的常见做法。2.1 整体目录结构assets、config、main模块的分层拿到项目后第一件事是看目录它长这样plane_war/ ├── assets/ │ ├── images/ │ │ ├── player.png │ │ ├── enemy.png │ │ └── bullet.png │ └── sounds/ │ ├── shoot.wav │ └── explosion.wav ├── config.py ├── main.py ├── plane_sprites.py └── requirements.txtassets目录专门放图片和音效资源这样不会和代码混在一起换皮肤时只动images文件夹。config.py把屏幕尺寸、帧率、颜色、初始血量、敌机生成速度这些参数集中管理而不是散落在各个模块里。plane_sprites.py定义了所有游戏对象的类——玩家飞机、敌机、子弹它们都继承自pygame.sprite.Sprite。main.py只负责初始化游戏并启动主循环。这样的分层带来的直接好处是想调整游戏难度只需要改config.py里的数值想新增一个敌人类型直接在plane_sprites.py里新增一个类并注册到主循环中完全不用关心图片从哪加载、窗口怎么创建。我第一次拿到这个项目时只用了十分钟就定位到了敌机生成频率的参数位置这种配置与逻辑分离的设计对二次开发者来说非常省心。2.2 技术选型为什么用pygame而不是PyQt或cocos很多初学者会问做个飞机大战为什么不用PyQt或者cocosPyQt虽然也能做界面但它的核心是桌面应用组件做游戏时的帧循环、精灵管理、碰撞检测都需要自己从零实现且性能远不如专业游戏库。cocos和unity则引入了引擎概念学习曲线陡增还要处理跨平台打包。pygame的优势在于它把所有游戏基础设施都准备好了显示窗口、加载图片、播放声音、处理键盘输入、绘制图形都不用额外造轮子。这个项目选pygame还有一个现实原因依赖极轻。requirements.txt里只有一行pygame2.0.0不像某些游戏框架动辄几十个依赖包pygame在Windows、macOS、Linux上都有官方预编译包pip install直接装不会出现编译失败这类劝退问题。对于教学和快速验证想法来说pygame是最折中的选择——它让你不用处理底层图形API又能完全掌控游戏循环。2.3 依赖文件与运行环境的细节说明requirements.txt虽然只有一行但版本号需要稍微注意。pygame从2.0版本开始重构了API而且对Python 3.11的支持更稳定。如果你用的是Python 3.10以下的老版本装pygame 1.9.x也能跑但部分API会有细微差异比如显示模式初始化、音频驱动的默认值。我一般建议直接装2.0以上版本这个项目里用到的pygame.sprite.Group、pygame.key.get_pressed()等接口在2.x下表现更稳定。另外一个容易忽略的点是项目根目录下没有看到单独的utils模块而是把工具函数直接写进了plane_sprites.py。这种写法在小型游戏里是合理的但如果你打算后续扩展成较大的项目建议把资源加载路径封装成一个独立的asset_manager模块避免每个类都自己拼接相对路径。3. 核心模块拆解一个子弹从发射到命中的完整链路这一章是项目的核心价值所在。飞机大战的全部逻辑其实可以浓缩成一句话游戏循环不断地处理输入、更新对象位置、检查碰撞、绘制画面。但怎么把这些步骤优雅地组织起来就是代码功力了。3.1 主入口main.py游戏初始化与主循环main.py是项目的启动入口它的结构非常标准先看代码import pygame import config from plane_sprites import Player, Enemy, Bullet def main(): pygame.init() # 设置窗体尺寸和标题 screen pygame.display.set_mode((config.SCREEN_WIDTH, config.SCREEN_HEIGHT)) pygame.display.set_caption(飞机大战) clock pygame.time.Clock() # 创建精灵组 all_sprites pygame.sprite.Group() enemies_group pygame.sprite.Group() bullets_group pygame.sprite.Group() player Player() all_sprites.add(player) running True while running: # 控制帧率防止CPU占用过高 clock.tick(config.FPS) for event in pygame.event.get(): if event.type pygame.QUIT: running False # 更新所有精灵状态 all_sprites.update() bullets_group.update() enemies_group.update() # 绘制背景和所有精灵 screen.fill(config.BG_COLOR) all_sprites.draw(screen) bullets_group.draw(screen) enemies_group.draw(screen) pygame.display.flip() pygame.quit() if __name__ __main__: main()这里的核心是while循环。游戏循环的第一件事是clock.tick(config.FPS)它的作用是让每帧间隔时间固定。假设FPS设为60那么tick方法会保证每秒最多执行60次循环这样游戏速度在不同性能的机器上保持一致。如果不加这一行游戏在高端电脑上会跑得飞快敌机移动速度看起来像是瞬移这就是很多新手说游戏速度忽快忽慢的根本原因。事件处理部分非常简洁——只监听了QUIT事件也就是点击窗口关闭按钮。实际游戏中玩家的上下左右按键动作并不靠event遍历来捕获而是用pygame.key.get_pressed()在精灵内部获取键盘状态这种区分很重要event用于离散动作像点击关闭、按下开火键而get_pressed适合处理持续按住的方向移动。你会发现这里把精灵分成了三个Groupall_sprites负责玩家绘制和更新bullets_group管理和绘制子弹enemies_group管理和绘制敌机。这样分开的原因在碰撞检测时就会体现出来——你只需要让bullet精灵和enemy精灵所在的组做碰撞检查不需要把玩家也卷进去。3.2 玩家飞机精灵设计移动边界与键盘响应接下来看plane_sprites.py中的玩家类import pygame import config class Player(pygame.sprite.Sprite): def __init__(self): super().__init__() # 加载图片并缩小到合适尺寸 self.image pygame.image.load(assets/images/player.png) self.image pygame.transform.scale(self.image, (60, 80)) self.rect self.image.get_rect() self.rect.midbottom (config.SCREEN_WIDTH // 2, config.SCREEN_HEIGHT - 20) self.speed config.PLAYER_SPEED self.hp config.PLAYER_HP def update(self): # 获取当前按下的键盘键位 keys pygame.key.get_pressed() if keys[pygame.K_a] or keys[pygame.K_LEFT]: self.rect.x - self.speed if keys[pygame.K_d] or keys[pygame.K_RIGHT]: self.rect.x self.speed if keys[pygame.K_w] or keys[pygame.K_UP]: self.rect.y - self.speed if keys[pygame.K_s] or keys[pygame.K_DOWN]: self.rect.y self.speed # 水平边界限制 if self.rect.left 0: self.rect.left 0 if self.rect.right config.SCREEN_WIDTH: self.rect.right config.SCREEN_WIDTH # 垂直边界限制 if self.rect.top 0: self.rect.top 0 if self.rect.bottom config.SCREEN_HEIGHT: self.rect.bottom config.SCREEN_HEIGHT每个精灵都是一个pygame.sprite.Sprite子类必须重写update方法这个方法会被精灵组的update()自动调用。注意update方法没有返回值它只修改自身的rect属性。rect是pygame管理矩形区域的类保存了精灵的位置和尺寸所有移动本质上都是改变rect的x和y坐标。移动控制用的是键盘状态检测而不是事件驱动这里有个性能层面的考量事件队列在游戏运行时会持续堆积如果用event类型判断按键那么一个按住动作可能会产生多个KEYDOWN事件导致移动速度不均匀。get_pressed()返回的是一个布尔元组表示当前每一帧中哪些键处于按下状态这样在60FPS下每帧移动speed个像素速度就非常平滑。speed和hp都是直接从config读取的这样改难度时不需要碰逻辑代码。边界判断用了四组if而不是elif因为玩家可能同时按了上和左如果只用if-elif斜向移动会被吞掉一个方向。这个细节值得留意。3.3 子弹与敌机碰撞Group碰撞检测的内在逻辑敌方子弹与飞机的碰撞是飞机大战中最高频的逻辑看下子弹和敌机的设计class Bullet(pygame.sprite.Sprite): def __init__(self, x, y): super().__init__() self.image pygame.image.load(assets/images/bullet.png) self.image pygame.transform.scale(self.image, (10, 20)) self.rect self.image.get_rect() self.rect.centerx x self.rect.bottom y self.speed config.BULLET_SPEED def update(self): self.rect.y - self.speed # 超出屏幕后移除子弹 if self.rect.bottom 0: self.kill() class Enemy(pygame.sprite.Sprite): def __init__(self): super().__init__() self.image pygame.image.load(assets/images/enemy.png) self.image pygame.transform.scale(self.image, (50, 60)) self.rect self.image.get_rect() self.rect.x random.randint(0, config.SCREEN_WIDTH - self.rect.width) self.rect.y random.randint(-100, -50) self.speed random.randint(config.ENEMY_SPEED_MIN, config.ENEMY_SPEED_MAX) def update(self): self.rect.y self.speed if self.rect.top config.SCREEN_HEIGHT: self.kill()子弹从飞机机头位置生成每帧向上移动敌机从屏幕上方随机的负坐标区域生成每帧向下移动。击中检测放在main.py的主循环里用pygame.sprite.groupcollidehits pygame.sprite.groupcollide(bullets_group, enemies_group, True, True) for hit_enemy in hits.values(): score 100groupcollide是pygame内置的碰撞检测方法它会遍历第一个Group中的所有精灵检查每个精灵的rect是否与第二个Group中的任一精灵的rect发生重叠。参数中的第三个True表示碰撞后自动移除子弹第四个True表示自动移除敌机也就是一碰就消失的标准弹幕效果。返回的hits是一个字典键是子弹精灵值是被击中的敌机精灵列表。通过遍历字典的values来累计分数。这种碰撞检测的精度是矩形包围盒对于飞机大战这种俯视射击游戏完全够用。但要注意pygame的rect碰撞是基于矩形重叠如果子弹速度过快可能一帧内穿越敌机没有发生重叠这就是所谓的「穿透」问题。常见解法是把子弹设计成足够长或者把移动拆分成子步进但在这个项目里只要子弹速度不超过敌机宽度的FPS倍就不会出问题。config中BULLET_SPEED默认是15FPS是60敌机宽度50所以理论上能稳定检测。3.4 游戏状态切换与计分逻辑游戏进行中还需要处理玩家血量、得分和结束判定。这部分常常会被新手写成一坨全局变量但这个项目做了一个比较好的状态管理class GameState: def __init__(self): self.score 0 self.hp config.PLAYER_HP self.running True self.over False def update_score(self, points): self.score points def damage(self, value): self.hp - value if self.hp 0: self.over True把游戏状态封装成一个独立的GameState类而不是把score和hp散落在main函数里这样后续加暂停、加菜单、加多关卡都只需要扩展这个类。计分规则的逻辑也在这里集中管理修改得分系数时只需要改update_score的调用点。主循环中的判断逻辑是if player.rect.colliderect(enemy.rect): state.damage(10) enemy.kill()这里直接使用两个精灵的rect碰撞方法因为玩家和敌机不需要像子弹和敌机那样批量检测单独判断性能更好。注意血量扣除后判断了state.over如果为True则跳出主循环。这种状态的转移是显式的比用自定义event来通知更可控。4. 运行与调试从克隆到第一局通关的完整记录这部分写给想立刻跑起来的读者。按照下面的步骤操作五分钟内就能看到游戏窗口弹出来。4.1 环境准备与依赖安装项目要跑起来的前提是Python环境推荐使用Python 3.8以上的版本太老或太新的Python版本都可能导致pygame的wheel包不兼容。另一个人在Windows上遇到过Python 3.13还没发布pygame对应轮子时安装失败的情况所以如果卡在安装pygame直接把Python换到3.10或3.11。第一步先创建一个虚拟环境推荐避免污染全局Pythonpython -m venv venv # Windows激活 venv\Scripts\activate # mac/Linux激活 source venv/bin/activate然后安装依赖pip install -r requirements.txt如果你的网络环境比较慢可以使用国内镜像源例如清华的PyPI镜像。但要注意镜像源只影响下载速度不会改变包的API。安装完成后检查一下python -m pygame.examples.aliens如果这个示例游戏能正常弹窗说明pygame的环境和显示驱动都没问题。这一步非常重要很多初学者项目本身写对了但运行环境有问题然后误以为是代码bug。4.2 启动游戏与传入参数调优项目设计时把配置都集中在config.py这意味着运行游戏完全不需要命令参数直接启动即可python main.py以下是config.py中的几个关键可调参数参数名默认值作用SCREEN_WIDTH480窗口宽度调大后需要重新设计坐标边界SCREEN_HEIGHT700窗口高度FPS60帧率影响游戏速度和流畅度PLAYER_SPEED5玩家飞机每帧移动的像素数BULLET_SPEED15子弹每秒上升的像素数ENEMY_SPEED_MIN2敌机最小下落速度ENEMY_SPEED_MAX6敌机最大下落速度PLAYER_HP5玩家初始生命值调优的方法是如果觉得游戏难度太高把ENEMY_SPEED_MIN降到1把PLAYER_HP加到8如果觉得画面太卡把分辨率调小成(400, 600)。改完config.py后不需要改任何代码重新运行main.py即可生效。这种参数与逻辑分离的设计对于快速试验不同手感非常有用甚至可以把这些参数配置为命令行参数比如python main.py --speed 6但项目没有内置argparse有需要的读者可以自行扩展。4.3 常见运行报错与log排查运行阶段最容易遇到的三类报错这里提前说明。第一类是pygame.error: Unable to open file这类问题95%以上是工作目录不对。当你从项目根目录以外的路径运行main.py时代码中相对路径assets/images/player.png会拼接成一个错误的路径。解决办法是在项目根目录下运行或者在代码中把资源路径改成基于__file__的绝对路径import os BASE_DIR os.path.dirname(os.path.abspath(__file__)) IMAGE_PATH os.path.join(BASE_DIR, assets, images, player.png)第二类是没有声音但程序正常跑这往往是pygame在初始化混音器时选择了错误的音频驱动。可以尝试在代码开头加入pygame.mixer.pre_init(44100, -16, 2, 512)在pygame.init()之前调用能让音频初始化更稳定。第三类是运行时出现TypeError: pygame.event.Event object is not subscriptable这通常是因为你尝试用event[key]来获取按键但应该用event.key。新老版本pygame的差异坑这个在后面避坑章节里会详细说。5. 避坑指南pygame开发中五个最容易翻车的地方这一章写的是我在实际跑这个项目以及做二次开发时踩过的坑。每一条都是真实会遇到的而且报错信息往往非常误导人。5.1 坑一图片加载不报错但窗口全黑现象main.py正常启动窗口弹出来了但画面完全黑色没有任何报错甚至player精灵也没显示。原因资源加载使用了相对路径而启动命令是在其他目录下执行的。虽然pygame在加载失败时会抛出pygame.error但如果你在做初始化时图片路径写错了比如assets/images/player.png写成了assets/images/player.PNG由于Windows文件系统不区分大小写可能不会报错但加载到的是一个空surface。另一种情况是你在pygame.init()里没有初始化显示模块虽然大多数时候pygame会自动处理但某些环境下会导致图片surface没有正确创建。解决先打印image对象确认是否为空再检查路径大小写和目录层级。最稳妥的做法是使用os.path拼接绝对路径并把资源加载封装成一个独立的函数。我在验证这个项目时直接把所有资源路径切换为基于当前文件目录的绝对路径问题立刻消失。5.2 坑二FPS没有限制导致CPU占用100%现象游戏运行后风扇狂转任务管理器一看Python进程CPU占用率接近100%而且游戏速度时快时慢。原因主循环里没有调用clock.tick(FPS)。没有帧率限制时while循环以每秒几千次的速度疯狂执行每帧都去绘制和更新不仅CPU耗尽而且所有精灵移动速度变成「极速模式」游戏完全没法玩。这是一个非常经典的pygame新手错误因为不报错只表现为性能问题。解决在循环开始处添加clock.tick(config.FPS)。如果这样做了仍然CPU占用高检查是否在循环中重复加载图片、重复转换surface比如在update里调用pygame.image.load()就是灾难级错误。图片和声音资源必须在初始化时加载一次之后只复用。5.3 坑三groupcollide碰撞检测漏判子弹现象子弹明明飞向敌机但有时能击中有时直接穿过而且穿过去的子弹和敌机都一切正常。原因子弹速度过快导致前后两帧之间子弹与敌机的矩形没有任何时刻相交。假设子弹每帧移动30像素敌机宽度只有30像素那么子弹在某一帧刚好在敌机上方下一帧已经跑到敌机下方两帧之间没有重叠帧碰撞检测就漏过去了。这不是pygame的bug而是离散碰撞检测的固有缺陷。解决控制子弹速度在合理范围内原则是子弹每帧位移小于敌机的最小尺寸。如果必须高速子弹可以把子弹的image拉长比如做一个宽度8但长度30的长条子弹图片让矩形前后帧都有覆盖或者手动做连续碰撞检测用上一帧位置到当前位置做线段与矩形相交判断。对飞机大战来说最简单的方法是调整BULLET_SPEED不超过15我实测在FPS60下这个速度不会出问题。5.4 坑四子弹击穿敌机后kill()引发字典遍历错误现象运行一段时间后程序抛出RuntimeError: dictionary changed size during iteration崩溃位置在groupcollide之后的for循环里。原因groupcollide返回的字典hits会在成员移除时动态变化。当你在循环中处理多个子弹时某个子弹触发kill()后hits字典内部也同步移除了这个键但你的for循环还在遍历原始键视图导致迭代异常。这属于并发修改更准确地说是迭代中修改的经典错误。解决不直接遍历hits字典而是调用hits pygame.sprite.groupcollide(...)之后把hits.items()强制转成list再进行遍历for bullet, enemies in list(hits.items()): score 100 * len(enemies)这样迭代的是原始快照不会被运行时修改影响。另一个更好的做法是封装一个fight_collide函数专门负责碰撞后的事件处理避免在主循环里写太多递归逻辑。5.5 坑五中文字体显示成方块现象使用pygame.font.SysFont(simhei, 24)设置中文字体但屏幕上的文字全部变成「口口口」。原因pygame的SysFont依赖于系统字体名称不同操作系统对中文字体的命名不一样。在Windows下是simheimacOS下是pychinese或STHeitiLinux下可能是wqy-microheiSysFont找不到指定字体时回退到默认字体默认字体不包含中文字符集于是显示为方块。解决将字体文件放到assets/fonts目录下然后使用pygame.font.Font(assets/fonts/simhei.ttf, 24)加载这样完全绕开系统字体依赖。如果没有现成ttf文件也可以下载一个开源中文字体放进去这是游戏项目中很常见的做法。我另一个项目里就是靠这个办法解决了Linux服务器上字体方块问题之后再也没在字体上翻过车。6. 二次开发思路把方块飞机大战扩展成弹幕射击原型这个项目留了很舒服的扩展位下面几个方向是我认为性价比最高的改造路径。6.1 增加武器强化与道具掉落现在玩家每次只能单发子弹升级到弹幕游戏最简单的方式是给玩家加「子弹等级」。在Player类里增加一个bullet_level属性每次射出发射三发子弹中间一发直行左右两发稍微偏移角度。实现方式是在Bullet类里增加speed_x参数在初始化时给不同子弹不同的横向速度。另外可以用pygame.sprite.Group来管理道具敌机被击落后有一定概率掉落一个「能量块」精灵玩家触碰到能量块后提升子弹等级。这只需要复用现有的rect碰撞检测逻辑扩展门槛很低。6.2 引入状态机管理菜单、游戏、结束当前游戏没有暂停和菜单主循环在running跳出后直接退出。我一般会建立一个GameState枚举管理MENU、PLAYING、PAUSED、GAMEOVER四个状态。主循环中不同状态绘制不同界面例如GAMEOVER时绘制分数和重新开始按钮点击按钮重置GameState对象并清空所有精灵组。这个状态机的核心改动在main.py把while running内部的逻辑拆成三个函数handle_events、update、draw每个函数内部按state分支执行。这种改造带来的最大价值是不需要重复启动pygame.init和加载资源整个生命周期内窗口只初始化一次。6.3 音效与背景音乐的接入项目assets/sounds目录里已经预留了shoot.wav和explosion.wav但主循环里没有接播放逻辑。接入非常简单在Player类中emit子弹的位置调用pygame.mixer.Sound.play(shoot_sound)在碰撞检测命中时播放explosion_sound。注意pygame的mixer最多同时播放多个声音如果射击频率太高需要控制每次射击之间的最小间隔。背景音乐可以使用pygame.mixer.music.load然后music.play(-1)循环播放。这里有个优化点不要把Sound对象放在循环内反复创建应该在模块顶层加载一次。6.4 扩展双人对战模式的大致思路如果想做成双人同屏对战需要新增一个Player2类使用不同的按键控制例如WASD控制玩家一方向键控制玩家二。两个玩家共用一台电脑每个人的子弹只对自己之外的精灵生效这就需要在碰撞检测时把两个玩家的子弹分到不同的Group。最难的点是敌机既要撞击玩家也要被子弹消灭但玩家的子弹不能攻击另一个玩家。其实pygame的groupcollide本身支持传入不同的组只要把玩家二和玩家一放到不同的Group即可。不过这个扩展需要改动的地方较多适合已经把这个项目完全吃透后动手。最后说一点自己的习惯每次在这个项目上做改动我都会先跑一遍原有的测试流程——启动游戏、射击一个敌人、确认分数增加、被撞一次、确认血量减少、死亡后重开。这个小循环五步不到十秒却能把百分之九十以上的回归问题拦在门外。后来不管改哪个游戏项目我都强制自己先列一个手动冒烟用例清单再动手。希望你也能从这种朴素的验证方法里受益把飞机大战跑起来再改成自己的版本。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?