拓冰建站拓冰建站
首页 / 资讯中心 / 正文

用Python和pygame从零实现飞机大战:基础语法到游戏开发实践

简介一份基于Pygame库实现的“飞机大战”游戏开发示例面向正在学习Python游戏编程的入门与进阶开发者帮助理解面向对象、事件驱动和图形界面设计等核心概念。压缩包大小约1.66MB包含Python源码及必要素材整体精简适合快速上手与二次修改。已有394人学习下载。围绕飞机对象、子弹碰撞、敌机生成、音效播放和分数系统完整演示了Pygame窗口初始化、游戏循环、图像加载与屏幕绘制等关键流程同时涵盖矩形碰撞检测、mixer音效模块和游戏状态控制等实现细节。借助这份资源读者可以对照代码掌握Python游戏开发的基本框架并在此基础上扩展道具、关卡或敌方AI逐步打造属于自己的飞行射击作品。 学完Python基础语法之后最大的问题往往不是看不懂代码而是不知道拿这门语言干点什么。写爬虫吧还没学到requests做数据分析吧numpy还没弄明白。这时候最合适的过渡项目就是动手写一个飞机大战。这个项目用到的都是最基础的东西变量、循环、条件、函数、类但你又必须把它们组合起来解决真实问题整个写下来对语法的理解和写代码的信心都会上一个台阶。很多人觉得飞机大战太老土现在谁还玩这种像素游戏。但换个角度看它恰好是一个麻雀虽小五脏俱全的完整项目事件处理、游戏循环、碰撞检测、精灵管理、音效播放、界面绘制这些知识点往后做任何图形界面程序、小游戏甚至入门游戏引擎全都用得上。本文不打算只丢一堆代码让你复制粘贴而是把每一步为什么这么写、哪里容易踩坑一起讲清楚让你写完还能自己改出花来。1. 这个经典练手项目到底能练什么1.1 它是最小可用游戏的完整范本飞机大战的规则一句话就能说完玩家移动飞机发射子弹消灭敌机别让自己被撞到。规则简单不代表实现简单它背后覆盖了一个游戏程序最基本的几个模块事件响应键盘按下、松开、窗口关闭这是所有交互程序的根基状态更新飞机坐标变化、子弹移动、敌机生成本质是每帧重新计算所有对象的属性渲染输出把更新后的画面绘制到窗口上靠的是循环刷新资源管理图片、音效、字体怎么加载、怎么释放碰撞判定矩形与矩形的相交检测这是游戏逻辑的核心可以说几乎所有2D游戏都是在这个框架上加内容。你以后想做打砖块、贪吃蛇、俄罗斯方块底层逻辑和飞机大战是完全一样的只是换了个壳。把这个项目吃透等于掌握了2D小游戏的标准套路。1.2 为什么选pygame而不是别的方案刚入门的时候很多人会在pygame、tkinter、Pygame Zero之间犹豫。我在带新手的过程中发现选错了库会把大量精力浪费在跟游戏逻辑无关的事情上。tkinter是Python自带的GUI库做表单、做工具挺顺手但它并不是为实时游戏设计的。它的刷新机制、绘图性能、事件模式都偏桌面应用强行做飞机大战到后面弹幕多了会明显变卡而且代码写起来也别扭。Pygame Zero是pygame的进一步封装设计目标是给编程教学用代码确实简洁但对底层的暴露不够出了诡异问题很难排查等你项目跑到后面想加复杂功能反而不知道怎么下手。pygame站在中间的位置它封装了SDL库的底层细节你不用自己处理像素缓冲和硬件加速同时又保留了游戏开发的核心概念精灵、分组、碰撞检测这些机制都是公开的学完这一套将来切换到Unity或者Godot理解成本会低很多。所以我的选择一直是pygame这也是绝大多数Python游戏教程的默认选项。方案适合场景实时性能学习成本tkinter桌面工具、表单界面偏低低但思路偏GUIPygame Zero少儿编程、快速原型中等极低封装过度pygame2D小游戏、游戏入门较高适中概念完整2. 环境准备把Python和pygame一次配好2.1 Python安装里最容易被忽略的一步飞机大战项目用纯Python编写唯一的第三方依赖就是pygame所以环境搭建并不复杂但很多新手第一步就卡住了。在Windows上安装Python时安装向导第一页有一个复选框Add Python to PATH默认是不勾选的。这个勾如果不选后面在命令行里输入python会提示找不到命令然后就会陷入我明明装了Python为什么运行不了的困惑。我的建议是第一页就把这个勾选上如果忘了也可以去系统属性-环境变量手动把Python安装路径和Scripts目录加进去但新手直接用安装器重装一次更快。版本选择上建议下载官方最新的Python 3.x稳定版不要在32位和64位之间纠结现在的电脑基本都是64位默认选64位安装包即可。装完在命令行输入python --version能正常输出版本号这一步就算过了。我对新手还有一个额外建议装完Python后顺手把pip的源切换成国内镜像。这个操作不做也能装包但速度差距非常大。具体做法是在用户目录下创建一个pip.ini文件写入国内镜像源地址这样后面pip install就是秒下。这一步虽然跟飞机大战没有直接关系但能让后面的步骤顺畅很多。2.2 安装pygame与验证在命令行执行pip install pygame正常情况下会看到进度条装完以后可以用一行命令验证是否成功python -m pygame.examples.aliens运行之后如果弹出一个可以玩的外星人游戏窗口说明pygame已经完整可用。这一步非常推荐做因为它同时验证了Python环境、pip安装结果、图形窗口显示和pygame自身运行状态任何一个环节有问题都会在这里暴露出来。如果执行报错最常见的就两类一是pip命令找不到那大概率是PATH问题二是externally-managed-environment这样的提示这是新版Python在部分系统上对外部包管理做的限制解决办法是用虚拟环境或者给pip加--break-system-packages参数但新手我更推荐直接用python -m venv创建项目独立的虚拟环境一劳永逸。2.3 编辑器VSCode还是PyCharm编辑器这个话题经常引起争论我直接说结论写飞机大战这个体量的项目VSCode完全够用而且启动快、配置简单。在插件市场装上Python扩展和Pylance打开项目文件夹选好解释器就能获得完整的代码补全和错误提示。PyCharm社区版也是免费的对初学者更友好的一点是它把虚拟环境、解释器选择这些概念做成了图形界面基本不用记命令。缺点是启动比较慢项目多的时候内存占用也高。我的真实建议是哪个已经装了就先用哪个别在这个环节花太多时间选择困难。真正重要的是你开始写代码。3. 先把代码骨架立起来窗口、主循环和事件3.1 面向对象的整体设计很多教程一上来就堆了一大段顺序结构的代码窗口初始化、变量定义、while循环全揉在一起。短时间能跑但稍微一加功能代码就乱成一团。我更推荐从一开始就用面向对象的方式组织代码把整个游戏拆成几个类Game负责窗口创建、主循环、全局状态管理Player玩家飞机负责移动和射击Bullet子弹负责飞行和消失Enemy敌机负责出现、移动和撞击这样拆分的好处是每个类只操心自己的事互相之间通过方法调用和数据传递配合后期想加一个新敌方类型只要再写一个类往分组里一丢就行不需要改动主循环的逻辑。第一次接触的读者不用紧张我下面给的代码会一步步来你不需要一开始就理解全部先照着敲跑起来再看每个类的职责。3.2 游戏主循环为什么是灵魂飞机大战这类实时游戏本质上是一个无限循环每一帧重复做三件事处理输入、更新状态、绘制画面。这个循环就是整个程序的发动机。import pygame import sys class Game: def __init__(self): pygame.init() self.screen pygame.display.set_mode((480, 700)) pygame.display.set_caption(飞机大战) self.clock pygame.time.Clock() self.running True def handle_events(self): for event in pygame.event.get(): if event.type pygame.QUIT: self.running False def update(self): pass def draw(self): self.screen.fill((0, 0, 0)) pygame.display.flip() def run(self): while self.running: self.handle_events() self.update() self.draw() self.clock.tick(60) pygame.quit() sys.exit() if __name__ __main__: game Game() game.run()这段代码跑起来以后屏幕上会出现一个480乘700像素的黑色窗口然后一直保持直到你点关闭按钮。窗口大小480x700是有讲究的这是从早年经典飞机大战素材里继承下来的比例接近手机竖屏飞机从上往下飞有足够的纵向空间。你也可以改成自己喜欢的尺寸但后面所有坐标和素材尺寸都要跟着适配。pygame.time.Clock对象配合tick(60)作用是让循环每秒钟最多执行60次也就是60帧。没有这行控制程序会以电脑能跑多快就跑多快的速度空转画面动画速度就不固定在性能好的电脑上天上的子弹会快到看不见。加了这个限制同样的代码在不同电脑上运行效果才一致。可以试着把tick(60)改成tick(10)再运行你会发现窗口操作明显变得钝这就是帧率对游戏体验最直接的感受。4. 玩家飞机、子弹和敌机的核心玩法实现4.1 玩家飞机从键盘到屏幕坐标接下来创建玩家类。我建议在还没有素材图片的时候先用一个矩形色块代替飞机保证逻辑先跑通图片后面再换。这样做的好处是调试阶段不会因为找不到图片而卡住。class Player(pygame.sprite.Sprite): def __init__(self): super().__init__() self.image pygame.Surface((60, 70)) self.image.fill((0, 255, 0)) self.rect self.image.get_rect() self.rect.midbottom (240, 680) self.speed 6 def update(self, keys): if keys[pygame.K_LEFT] and self.rect.left 0: self.rect.x - self.speed if keys[pygame.K_RIGHT] and self.rect.right 480: self.rect.x self.speed if keys[pygame.K_UP] and self.rect.top 0: self.rect.y - self.speed if keys[pygame.K_DOWN] and self.rect.bottom 700: self.rect.y self.speedpygame.sprite.Sprite是pygame内置的精灵基类它要求子类必须有image和rect两个属性。rect是一个矩形对象记录了精灵的位置和尺寸pygame所有碰撞检测和绘制操作都是围绕这个矩形进行的。移动逻辑里的每一句判断都在防止飞机飞出屏幕按左键时先判断左边有没有超出边界如果left 0才允许移动否则按键无效。这种先判断再行动的写法比让飞机飞出去再拉回来要干净得多。如果把上下左右四个方向的边界判断都删掉你会看到飞机直接跑出屏幕再也找不回来这就是边界条件的重要性。在Game.update里需要传入当前按键状态def update(self): keys pygame.key.get_pressed() self.player.update(keys)这里用的是pygame.key.get_pressed()它返回当前所有按键的状态元组适合用于按住持续移动的场景。它和事件队列里KEYDOWN的区别在于事件方式是瞬间触发一次适合射击而按键状态是持续性的适合移动。4.2 子弹发射控制频率比控制速度更难子弹类的核心与玩家类似但它不需要响应键盘只需要向上飞行飞出屏幕就自我销毁。class Bullet(pygame.sprite.Sprite): def __init__(self, x, y): 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 def update(self): self.rect.y self.speed if self.rect.bottom 0: self.kill()注意speed是负值因为屏幕坐标系的y轴是向下增长的子弹要往上飞坐标就得减小。kill()是精灵组提供的方法作用是把这个精灵从所有包含它的精灵组里移除。子弹飞出屏幕后如果不移除它会一直存在于内存里每帧还在执行坐标更新游戏跑得越久子弹对象堆积越多内存就越来越大最终导致卡顿。这种对象生命周期管理的意识写大项目时会非常重要。子弹发射不能每帧都按一次否则一次按下会像机关枪一样疯狂喷射而且完全不受控制。通常的解决办法是记录上一次发射的时间每次发射前判断间隔是否已超过冷却时间def shoot(self): now pygame.time.get_ticks() if now - self.last_shot 200: self.last_shot now bullet Bullet(self.rect.centerx, self.rect.top) bullets.add(bullet)这里get_ticks()返回的是游戏启动以来的毫秒数用差值判断而非绝对时间好处是这个逻辑不受游戏是否卡顿影响实现起来最直观。200毫秒的间隔意味着每秒最多射出5发这是经过试玩后手感比较舒服的节奏。想加强火力把新子弹加进bullets分组然后在Game.update里调用bullets.update()其余交给分组自动处理。4.3 敌机生成用计时器代替等够时间敌机需要有规律地出现同时还不能全部挤在同一帧。最简单可靠的方式是设置一个事件定时器在初始化时注册ENEMY_SPAWN pygame.USEREVENT 1 pygame.time.set_timer(ENEMY_SPAWN, 1000)然后在事件循环里处理if event.type ENEMY_SPAWN: enemy Enemy() enemies.add(enemy)set_timer会每隔1000毫秒向事件队列推送一次自定义事件相当于游戏内的闹钟。用USEREVENT 1是为了自定义事件编号不跟pygame内置事件冲突。敌机类的实现同样简单class Enemy(pygame.sprite.Sprite): def __init__(self): super().__init__() self.image pygame.Surface((50, 50)) self.image.fill((255, 0, 0)) self.rect self.image.get_rect() self.rect.x random.randint(0, 480 - self.rect.width) self.rect.y -self.rect.height self.speed random.randint(3, 6) def update(self): self.rect.y self.speed if self.rect.top 700: self.kill()敌机从窗口上方出现初始y坐标是负数意思是它从屏幕外慢慢飞进来这样视觉效果更自然。横向位置用random.randint随机生成飞行速度也有随机波动这样敌机不会排列成整齐的队伍游戏更有变化。关于为什么用set_timer而不是在update里用get_ticks判断这里说句实话两种方式都可以但set_timer的代码更直白语义清晰适合新手快速上手。等以后想做更精细的波次管理比如第一波结束后增强敌机属性那时候再用get_ticks手动管理也不迟。4.4 碰撞检测与计分pygame精灵组提供了现成的碰撞检测方法处理子弹击中敌机这个场景只需一行代码hits pygame.sprite.groupcollide(enemies, bullets, True, True)groupcollide(enemies, bullets, True, True)的意思是对两个精灵组做矩形重叠检测后面两个True分别表示有碰撞时删除敌机、有碰撞时删除子弹。返回的字典里键是被删除的敌机值是撞到它的子弹列表。每检测到一次碰撞就加一次分for enemy in hits: score 10玩家被敌机撞到的检测更简单用spritecollide只销毁玩家自己if pygame.sprite.spritecollide(player, enemies, False): self.running FalseFalse的意思是碰撞时不要从enemies分组里删除敌机因为此时玩家都已经挂了敌机删不删没有意义保持默认逻辑少干扰。默认的碰撞检测是基于矩形区域的也就是说飞机和敌机的图片边缘是纵向尖角的形状也可能按矩形判定为碰撞偶尔会出现明明没碰到却判定撞了的情况。要不要更精确的碰撞看你的取舍这个我在第五章专门讲。计分文字用pygame自带的字体功能绘制font pygame.font.Font(None, 36) score_surface font.render(fScore: {score}, True, (255, 255, 255)) self.screen.blit(score_surface, (10, 10))到这里一个能玩的基础版飞机大战就完成了玩家可以移动可以射击敌机会从天而降击中加分撞到结束。整个项目加起来不到150行这就是第一个可交付的里程碑。5. 从能玩到好玩手感优化的几个大方向5.1 碰撞检测的精度取舍矩形碰撞快但不够精确。飞机两边的机翼位置实际是空的矩形判定却把空的位置也算进去了结果就会产生没碰到但爆炸了的冤枉感。换圆形碰撞检测可以改善。原理是把每个精灵缩小成一个圆心和半径然后计算两个圆心之间的距离距离小于半径之和就判定碰撞。pygame的Sprite类没有内置圆形碰撞但你可以写一个简单的方法封装。圆形碰撞的计算量比矩形大一些但在飞机大战这种对象数量不超过几十个的游戏里性能完全不是问题。在我实际测试中60帧运行时有50个对象在场矩形和圆形的性能差距根本感知不到。所以我建议如果你的素材图片形状比较尖比如飞机有细长的机翼就优先用圆形碰撞如果素材本身接近方形比如陨石、方块敌人矩形碰撞完全够用不要为了优化而优化。5.2 帧率波动的处理方法代码里tick(60)只是把帧率限制在60但万一某个帧计算量巨大比如同时出现了30个敌机实际帧率可能掉到40甚至更低。帧率掉下来之后物体移动速度是每帧移动多少像素的逻辑结果就是整个游戏变慢手感变得黏糊。解决办法有两种。第一种是把移动量按帧间隔时间放大或缩小pygame的Clock可以提供一个dt值上一次tick到现在的秒数物体每帧移动speed * dt这样即使帧率波动物体每秒移动的总距离是恒定的。第二种更简单粗暴提前做好性能预算避免在同一帧做大量重复计算比如敌机生成时做数量上限控制超过30个就暂停生成。对新手来说先做好上限控制就够了。等将来游戏复杂度上去了再引入dt也不迟。5.3 图片、音效资源怎么找先用色块把游戏逻辑跑通之后下一步就是换成真正的游戏素材。网上很多免费素材站提供像素风飞机素材搜plane sprites或者shootem up assets就能找到一大堆。注意看素材的授权协议个人练习用基本都没问题但要商用的话必须逐条核对。加载图片时有个非常实用的建议用pygame.image.load加载后立即调用convert_alpha()方法转换像素格式self.image pygame.image.load(player.png).convert_alpha()这一步能让图片的加载和绘制速度提升不少。如果不做转换pygame每帧绘制时需要做像素格式转换图片一多性能就会下降。音效同理pygame.mixer.Sound支持wav和ogg格式枪声、爆炸声、背景音乐都可以从免费音效站找。加载音效的代码放在初始化阶段不要在游戏主循环里反复加载否则每播放一次就做一次磁盘IO会造成明显的卡顿。5.4 加一点仪式感爆炸效果与结束界面基础玩法完成后你会发现游戏体验最突兀的地方是敌机被击中后直接消失玩家被撞击后黑屏退出整个过程没有任何反馈。加一个小爆炸效果就能让游戏观感提升一大截。简单的实现方式是在敌机被击中的位置生成一个特效对象播放几帧扩散的圆环或粒子然后自动销毁。复杂度不高但对手感的提升非常明显。玩家被撞后的结束界面也是必须的至少显示最终得分和按任意键重新开始的提示否则游戏结束后只能关掉窗口重新运行体验太生硬。6. 写完之后大概率会碰到的几个坑我提前帮你们排掉6.1 图片显示不出来的经典原因明明文件就在项目文件夹里pygame.image.load(player.png)却报错FileNotFoundError。这个坑我见得太多了十有八九是运行脚本时的工作目录问题。你需要知道的是程序运行时相对路径是相对于当前工作目录的不是相对于代码文件位置的。如果你用VSCode直接运行代码工作目录通常是打开的文件夹如果你在终端用python执行工作目录是当前所在的终端路径。排查方法很简单在代码里打印os.getcwd()看看当前工作目录到底在哪然后把图片路径改成绝对路径或者用sys.path[0]这种代码文件所在目录来构建路径。千万别用写死的绝对路径换台电脑就废了。6.2 游戏用久了越来越卡表情变慢动作原因前面提过子弹和敌机飞出屏幕后没有销毁对象越积越多每帧更新列表越来越长。排查方法是在运行中打印当前精灵组的长度看看数量是不是一直涨。解决方案就是把四个方向的出界判断都补上并且用kill()及时移除。还有一个隐蔽的卡顿来源是每个循环里都在加载图片或音效。我见过有人把pygame.image.load放在Player.update里的每次调用都重新从磁盘读文件帧率直接被拉到15。记住一个原则所有资源在初始化时加载一次之后只做引用绝不在循环里做IO操作。6.3 点关闭按钮没反应或者退出后终端卡住这个问题的根因通常是对事件循环的理解不够。pygame.QUIT事件是点窗口关闭按钮时产生的必须通过pygame.event.get()把它取出来处理程序才知道你要退出。如果事件循环里忘了判断QUIT窗口确实会关掉但程序主循环还在后台空转终端就卡住了。另外要注意pygame.quit()和sys.exit()的区别。前者只是卸载pygame模块并释放资源不会终止程序后者才是真正退出Python进程。正确的收尾写法是两行都调用先取消pygame的初始化再退出进程。顺序反了有可能出现窗口关不掉的诡异现象。6.4 打包成exe时找不到图片素材用pyinstaller -F game.py打包后发给别人对方双击运行第一帧就报错找不到图片。原因在于打包后程序被解压到一个临时目录原来的相对路径全失效了。这个问题在csdn和知乎上被反复提问解决思路是在代码里用sys._MEIPASS判断打包环境资源路径动态切换。如果只是想练手我建议先把打包放一放不要让它打断你写游戏主逻辑的节奏。等游戏功能完整了再回头研究打包那个阶段心态会好很多。最后再分享一个我自己的习惯每完成一个小功能就运行一次、玩一分钟。不要闷头写200行再集中调试那样一旦出问题定位成本会高到你怀疑人生。飞机大战虽然是个练手项目但用增量开发的方式走一遍你感受到的会不是一个能跑的程序而是一整套如何把需求拆小、再逐个击破的做事方法。这份经验才是这个老项目到今天依然值得一写的原因。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门