Python 3.7与Pygame实战:从零复刻经典打飞机游戏

发布时间:2026/7/14 13:23:46
Python 3.7与Pygame实战:从零复刻经典打飞机游戏 1. 项目概述从零到一用Python复刻经典“打飞机”最近在整理旧项目时翻出了一个几年前用Python写的“打飞机”小游戏。这个项目虽然不大但麻雀虽小五脏俱全涵盖了游戏开发中从图形渲染、事件处理到碰撞检测、状态管理等一系列核心概念。更重要的是我把它完整地适配到了Python 3.7环境确保在当下主流的Python版本上也能顺畅运行。对于想入门游戏开发或者想找一个综合性练手项目的朋友来说这个案例再合适不过了。这个“打飞机”游戏本质上是一个2D的纵向卷轴射击游戏。玩家控制一架飞机在屏幕底部移动发射子弹消灭从屏幕上方不断出现的敌机同时躲避敌机的撞击和子弹。听起来简单但实现起来你需要处理图像加载与显示、键盘事件响应、游戏主循环逻辑、精灵Sprite的状态更新与绘制、矩形碰撞检测以及分数、生命值等游戏状态的管理。整个过程就像是在用代码搭建一个微型的、可交互的动画世界。我选择Python来实现主要是看中它的简洁和丰富的库生态。对于这类2D小游戏Pygame库是不二之选。它封装了SDLSimple DirectMedia Layer提供了对图像、声音、事件输入等多媒体功能的便捷访问让我们可以专注于游戏逻辑本身而不是底层图形接口的细节。这个项目将带你走完全流程从环境搭建、素材准备到核心类的设计、主循环的构建再到最后的调试与优化。我会附上详细的代码解释和运行截图确保你能看懂每一步并能亲手复现出来。2. 开发环境与核心工具链搭建2.1 Python 3.7的安装与配置要点既然项目标题和热词都强调了Python 3.7我们就从这里开始。Python 3.7是一个长期支持版本在稳定性和库兼容性上表现很好。虽然现在已有更新的版本但3.7的生态依然非常成熟对于学习和小型项目开发完全足够。安装建议直接从Python官网下载对应你操作系统的安装包。在安装时务必勾选“Add Python 3.7 to PATH”这个选项这能让你在命令行中直接使用python和pip命令省去后续手动配置环境变量的麻烦。安装完成后打开命令行Windows上是CMD或PowerShellmacOS/Linux上是Terminal输入python --version如果显示Python 3.7.x就说明安装成功了。虚拟环境的重要性我强烈建议为这个项目创建一个独立的虚拟环境。这能确保项目依赖的库比如Pygame不会干扰到你系统上其他Python项目。创建方法很简单在你选定的项目目录下运行python -m venv venv。这会在当前目录创建一个名为venv的文件夹里面包含了一个独立的Python解释器和pip。激活虚拟环境后所有通过pip install安装的包都会装在这个隔离的环境里。Windows激活venv\Scripts\activatemacOS/Linux激活source venv/bin/activate激活后命令行提示符前通常会显示(venv)表示你正在虚拟环境中工作。2.2 Pygame库的安装与版本选择游戏的核心引擎是Pygame。安装它只需要一行命令pip install pygame。在Python 3.7环境下pip会自动安装与之兼容的最新稳定版Pygame如2.0版本。这里需要澄清一个热词中的误区“python3.7需要flask版本号”。Flask是一个Web框架和我们的桌面游戏开发毫无关系这可能是关键词联想上的偏差。我们的核心依赖只有Pygame。为什么是Pygame因为它足够轻量且功能聚焦。对于“打飞机”这种2D精灵游戏Pygame提供的功能恰到好处pygame.display管理窗口、pygame.image加载图片、pygame.event处理键盘鼠标事件、pygame.time.Clock控制帧率、pygame.Rect处理矩形区域和碰撞、pygame.sprite模块提供精灵和精灵组的抽象。你不需要像用大型游戏引擎那样学习复杂的编辑器直接写代码就能控制一切这对理解游戏运行原理非常有帮助。验证安装安装完成后可以运行一个简单的测试脚本确认Pygame工作正常import pygame pygame.init() screen pygame.display.set_mode((400, 300)) pygame.display.set_caption(Pygame Test) running True while running: for event in pygame.event.get(): if event.type pygame.QUIT: running False screen.fill((255, 255, 255)) pygame.display.flip() pygame.quit()如果弹出一个白色的窗口并且能正常关闭说明环境搭建成功。2.3 项目结构与素材资源准备在写代码之前先规划好项目目录结构。一个清晰的结构能让开发过程更顺畅。plane_game/ ├── assets/ # 存放所有素材 │ ├── images/ # 图片玩家飞机、敌机、子弹、背景、爆炸效果 │ └── sounds/ # 音效射击音效、爆炸音效、背景音乐 ├── src/ # 源代码 │ ├── main.py # 游戏主入口包含主循环 │ ├── player.py # 玩家飞机类 │ ├── enemy.py # 敌机类 │ ├── bullet.py # 子弹类 │ └── game_config.py # 游戏配置屏幕大小、颜色、速度等常量 └── requirements.txt # 项目依赖列表素材准备与处理这是让游戏“像样”的关键一步。你可以在一些免费的素材网站如OpenGameArt找到像素风格的飞机、子弹图片。或者用绘图工具简单画几个色块也没问题重点是区分开玩家、敌机和子弹。素材处理有几个注意事项格式与透明建议使用PNG格式它支持透明度。将飞机、子弹图片的背景设为透明这样在游戏中它们就不会带着难看的白色方框。尺寸统一提前规划好各类精灵的显示尺寸。比如玩家飞机可能是50x50像素敌机是40x40像素子弹是10x30像素。保持比例协调。音效与音乐准备一些简短的WAV或OGG格式音效用于射击和爆炸。背景音乐可以选择一段循环播放的MIDI或MP3。注意音效文件不宜过大。将处理好的图片和音效文件放入assets目录下对应的文件夹。在代码中我们将使用pygame.image.load()和pygame.mixer.Sound()来加载它们。3. 游戏核心架构与类设计解析3.1 游戏主循环一切逻辑的发动机游戏的核心是一个无限循环即“游戏主循环”。每一帧它都按顺序做以下几件事处理事件比如按键、更新所有游戏对象的状态位置、生命值等、绘制当前帧的画面、控制循环速度以稳定帧率。在Pygame中这个模式非常清晰。# 在 main.py 中 def main(): pygame.init() screen pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) pygame.display.set_caption(Python打飞机) clock pygame.time.Clock() all_sprites pygame.sprite.Group() # 用于管理所有需要绘制的精灵 enemies pygame.sprite.Group() # 专门管理敌机方便碰撞检测 player_bullets pygame.sprite.Group() # 管理玩家子弹 player Player() # 创建玩家实例 all_sprites.add(player) running True while running: # 1. 处理事件 for event in pygame.event.get(): if event.type pygame.QUIT: running False elif event.type pygame.KEYDOWN: if event.key pygame.K_SPACE: player.shoot(player_bullets, all_sprites) # 2. 更新游戏状态 all_sprites.update() # 这里可以添加敌机生成逻辑、碰撞检测逻辑等 # 3. 绘制 screen.fill(BLACK) # 用黑色清屏模拟星空背景 all_sprites.draw(screen) # 这里可以绘制UI如分数、生命值 pygame.display.flip() # 4. 控制帧率 clock.tick(60) # 每秒60帧 pygame.quit()关键点解析pygame.event.get(): 获取当前帧的所有事件按键、鼠标、退出等必须每帧处理否则系统会认为程序无响应。sprite.update(): 调用精灵组中每个精灵的update方法这是精灵自主更新状态如移动的地方。sprite.draw(screen): 将精灵组中所有精灵绘制到指定的screen表面。这要求每个精灵都有image和rect属性。clock.tick(60): 这是控制游戏速度的灵魂。它告诉循环“等待一下确保从上一次调用tick到现在时间刚好过去了1/60秒”。这能将游戏帧率稳定在60FPS避免在不同性能的电脑上速度差异过大。3.2 玩家飞机类响应与控制的核心玩家类Player继承自pygame.sprite.Sprite。作为精灵它必须拥有image显示什么和rect在哪里显示属性并可以实现update方法。# 在 player.py 中 import pygame from game_config import * class Player(pygame.sprite.Sprite): def __init__(self): super().__init__() # 加载图片并转换格式提升绘制效率 self.image pygame.image.load(assets/images/player.png).convert_alpha() # 设置初始位置屏幕底部中央 self.rect self.image.get_rect(center(SCREEN_WIDTH // 2, SCREEN_HEIGHT - 50)) self.speed 5 self.last_shot_time 0 # 用于控制射击间隔 self.shoot_cooldown 250 # 射击冷却时间毫秒 def update(self): # 获取当前所有按键状态实现持续移动 keys pygame.key.get_pressed() if keys[pygame.K_LEFT] and self.rect.left 0: self.rect.x - self.speed if keys[pygame.K_RIGHT] and self.rect.right SCREEN_WIDTH: 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 SCREEN_HEIGHT: self.rect.y self.speed def shoot(self, bullet_group, all_sprites_group): current_time pygame.time.get_ticks() # 检查是否过了冷却时间 if current_time - self.last_shot_time self.shoot_cooldown: bullet Bullet(self.rect.centerx, self.rect.top) bullet_group.add(bullet) all_sprites_group.add(bullet) self.last_shot_time current_time # 可以在这里播放射击音效 # shoot_sound.play()设计要点与避坑指南移动控制的选择这里使用了pygame.key.get_pressed()来检测按键状态而不是在事件循环里处理KEYDOWN。因为get_pressed()返回的是当前帧所有按键的布尔值状态能实现“按住持续移动”的效果更适合动作游戏。而事件KEYDOWN只在按键按下的那一帧触发一次。边界检测在update中移动后立即检查rect的边界left,right,top,bottom是否超出屏幕范围这是防止飞机“飞走”的关键。射击冷却如果不加限制玩家可以在一帧内发射无数子弹。我们通过记录上次射击时间last_shot_time和设置冷却时间shoot_cooldown单位毫秒来实现节流。pygame.time.get_ticks()获取游戏开始后的毫秒数是计时的标准方法。图片格式优化convert_alpha()方法在加载带透明通道的PNG图片后调用它能将图片转换为与当前显示模式最匹配的格式能显著提升后续blit绘制操作的速度。3.3 敌机与子弹类游戏内容的填充者敌机Enemy和子弹Bullet同样是精灵它们的行为模式更简单沿着固定方向移动移出屏幕后自动销毁以释放资源。# 在 enemy.py 中 import pygame import random from game_config import * class Enemy(pygame.sprite.Sprite): def __init__(self): super().__init__() self.image pygame.image.load(assets/images/enemy.png).convert_alpha() # 敌机从屏幕顶部随机位置出现 self.rect self.image.get_rect(center(random.randint(20, SCREEN_WIDTH-20), -20)) self.speed random.randint(2, 5) # 随机速度增加变化 def update(self): self.rect.y self.speed # 如果敌机完全移出屏幕底部则销毁它 if self.rect.top SCREEN_HEIGHT: self.kill() # 在 bullet.py 中 class Bullet(pygame.sprite.Sprite): def __init__(self, x, y): super().__init__() self.image pygame.image.load(assets/images/bullet.png).convert_alpha() self.rect self.image.get_rect(center(x, y)) self.speed -7 # 负数表示向上飞玩家子弹 def update(self): self.rect.y self.speed # 如果子弹移出屏幕顶部则销毁 if self.rect.bottom 0: self.kill()敌机生成策略敌机不会一次性全部创建。我们通常在游戏主循环中每隔一定时间比如每秒生成一个新的敌机实例并添加到敌机组和总精灵组中。这可以通过pygame.time.get_ticks()计时或者更优雅地使用pygame.USEREVENT事件来实现。# 在main.py的主循环前设置一个定时事件 ENEMY_SPAWN pygame.USEREVENT 1 pygame.time.set_timer(ENEMY_SPAWN, 1000) # 每1000毫秒1秒触发一次 # 在主循环的事件处理部分 for event in pygame.event.get(): ... elif event.type ENEMY_SPAWN: new_enemy Enemy() all_sprites.add(new_enemy) enemies.add(new_enemy)使用事件来生成敌机比在主循环里用if判断时间更清晰也更容易管理多种定时任务。4. 核心游戏逻辑的实现与调优4.1 碰撞检测游戏交互的灵魂没有碰撞检测子弹和敌机就只是互不干扰的动画。Pygame提供了几种碰撞检测方法对于“打飞机”这种矩形精灵我们使用pygame.sprite.groupcollide()和pygame.sprite.spritecollide()最为高效。子弹与敌机的碰撞这是得分的关键。我们检查玩家子弹组player_bullets和敌机组enemies之间的碰撞。# 在主循环的更新状态部分添加碰撞检测 hits pygame.sprite.groupcollide(player_bullets, enemies, True, True) for hit in hits: # hit是一个字典但在这里我们只关心发生了碰撞 score 10 # 击中一架敌机得10分 # 可以在这里播放敌机爆炸音效和动画pygame.sprite.groupcollide(group1, group2, dokill1, dokill2)函数非常强大。它检测group1和group2中所有精灵的碰撞。dokill1和dokill2参数是布尔值如果为True则发生碰撞的精灵会自动从对应的组中移除即被kill()。这正好符合我们的需求子弹和敌机相撞后两者都应该消失。函数返回一个字典记录了哪些精灵发生了碰撞以及和谁碰撞对于简单的计分我们只需要知道发生了碰撞即可。玩家与敌机的碰撞这是判断玩家是否被击中的逻辑。# 检测玩家是否与任何敌机相撞 if pygame.sprite.spritecollide(player, enemies, True): player_lives - 1 # 播放玩家被击中音效或动画 if player_lives 0: running False # 游戏结束pygame.sprite.spritecollide(sprite, group, dokill)检测一个精灵与一个精灵组中所有精灵的碰撞。dokill参数为True时发生碰撞的敌机会被自动移除。这里我们让玩家扣血并判断游戏是否结束。注意碰撞检测的优化。如果精灵数量非常多每帧进行两两检测会非常耗时。Pygame的精灵组碰撞检测内部已经做了一定优化。但对于更复杂的游戏可以考虑空间分割算法如四叉树。在我们的“打飞机”游戏中精灵数量有限几十个直接使用内置方法完全足够。4.2 游戏状态管理与UI绘制一个完整的游戏需要有状态反馈。我们需要在屏幕上显示玩家的分数、剩余生命值可能还有游戏结束的提示。分数与生命值变量在主循环外定义score和player_lives变量并在碰撞检测逻辑中更新它们。UI绘制Pygame使用pygame.font.Font对象来渲染文字。我们需要先创建一个字体对象然后用它来将文本字符串渲染成一个图像表面Surface最后将这个表面绘制到屏幕上。# 在main.py开头定义字体和颜色 font pygame.font.SysFont(None, 36) # 使用系统默认字体大小36 WHITE (255, 255, 255) # 在主循环的绘制部分清屏和绘制精灵之后 score_text font.render(fScore: {score}, True, WHITE) lives_text font.render(fLives: {player_lives}, True, WHITE) screen.blit(score_text, (10, 10)) screen.blit(lives_text, (SCREEN_WIDTH - 100, 10)) # 游戏结束判断 if player_lives 0: game_over_text font.render(GAME OVER, True, (255, 0, 0)) screen.blit(game_over_text, (SCREEN_WIDTH//2 - 70, SCREEN_HEIGHT//2 - 18)) # 可以在这里停止游戏更新只保留绘制和事件处理等待玩家退出或重启font.render(text, antialias, color)中antialias参数设为True可以让文字边缘更平滑。screen.blit(surface, position)将文字表面绘制到屏幕的指定位置。游戏状态管理进阶目前我们的游戏只有一个“进行中”的状态。更完善的架构可以引入一个简单的状态机比如有START_MENU、PLAYING、GAME_OVER、PAUSED等状态。主循环根据当前状态来决定执行哪一部分的逻辑和绘制。这能让代码结构更清晰也便于扩展功能比如开始菜单、暂停功能。4.3 音效与背景音乐的集成声音是游戏体验的重要组成部分。Pygame的pygame.mixer模块处理声音。# 在初始化部分加载音效和音乐 pygame.mixer.init() shoot_sound pygame.mixer.Sound(assets/sounds/shoot.wav) explosion_sound pygame.mixer.Sound(assets/sounds/explosion.wav) # 设置音效音量 shoot_sound.set_volume(0.3) explosion_sound.set_volume(0.5) # 加载并循环播放背景音乐 pygame.mixer.music.load(assets/sounds/bgm.mp3) pygame.mixer.music.set_volume(0.2) # 背景音乐音量调低些 pygame.mixer.music.play(-1) # -1表示无限循环 # 在需要播放音效的地方如玩家射击时 # shoot_sound.play()注意事项音效格式pygame.mixer.Sound适合加载短促的音效WAV, OGG。pygame.mixer.music模块更适合处理长的背景音乐MP3, OGG, MIDI。音量控制务必在播放前设置好音量特别是背景音乐避免突然的巨大声响。set_volume()的参数范围是0.0到1.0。资源管理音效对象可以重复调用play()。背景音乐使用play(loops)loops-1为循环播放loops0默认播放一次。错误处理如果声音文件加载失败程序可能会崩溃。在生产环境中可以考虑用try-except包裹加载代码并提供静默失败或使用默认音效的备选方案。5. 项目调试、打包与扩展思考5.1 调试技巧与常见问题排查开发过程中肯定会遇到各种“飞机不动了”、“子弹消失了”、“碰撞没反应”的问题。掌握一些调试技巧能事半功倍。使用Print大法在怀疑的代码位置如update方法、碰撞检测后打印关键变量的值。这是最直接的方法。例如在Player.update()里打印self.rect.x, self.rect.y看移动是否生效。绘制调试信息在屏幕上实时绘制精灵的矩形框可以直观看到碰撞区域是否准确。for sprite in all_sprites: pygame.draw.rect(screen, (0, 255, 0), sprite.rect, 1) # 用绿色线框绘制矩形帧率监控在循环里打印clock.get_fps()确保游戏运行在预期的帧率附近。如果帧率异常低可能是某处有性能瓶颈比如在循环内加载图片。精灵是否被正确kill确保在碰撞检测或移出屏幕后调用了精灵的kill()方法。kill()会将精灵从其所在的所有精灵组中移除Pygame就不会再更新和绘制它相当于销毁。事件处理阻塞确保主循环中pygame.event.get()被持续调用。如果遗漏操作系统会认为程序“卡死”了。常见问题速查表问题现象可能原因排查方向窗口打开后立即关闭主循环没有正确运行或立即退出检查while running循环条件检查事件处理中是否误触发退出图片显示为黑色方块图片路径错误或格式问题检查文件路径使用绝对路径或确保相对路径正确尝试convert()而非convert_alpha()按键无反应事件处理逻辑错误或按键检测代码位置不对确认使用pygame.key.get_pressed()在update中或正确处理了KEYDOWN事件碰撞检测无效精灵的rect属性未正确更新或碰撞组不对打印碰撞双方的rect坐标确认检查groupcollide使用的精灵组是否正确游戏速度过快/过慢未使用Clock.tick()或参数设置不当添加clock pygame.time.Clock()并在循环末尾调用clock.tick(60)音效不播放文件路径错误、格式不支持或音量设为0检查文件路径尝试WAV格式确认调用了play()且音量05.2 项目打包与分享代码写好了怎么分享给没有Python环境的朋友玩呢我们需要将项目打包成一个独立的可执行文件.exe, .app等。这里推荐使用PyInstaller。安装PyInstaller在项目虚拟环境中运行pip install pyinstaller。处理资源文件PyInstaller默认只打包.py文件。我们的图片、声音等资源需要额外处理。一个可靠的方法是在代码中修改资源加载路径使其在打包后也能找到文件。可以使用sys._MEIPASS属性PyInstaller创建的临时文件夹路径。import sys import os def resource_path(relative_path): 获取资源的绝对路径。在开发环境和PyInstaller打包后都能工作 try: # PyInstaller创建的临时文件夹路径 base_path sys._MEIPASS except Exception: # 正常开发环境下的路径 base_path os.path.abspath(.) return os.path.join(base_path, relative_path) # 加载图片时 self.image pygame.image.load(resource_path(assets/images/player.png)).convert_alpha()执行打包命令在项目根目录下打开命令行运行pyinstaller --onefile --windowed --add-data assets;assets src/main.py--onefile: 打包成单个exe文件。--windowed: 运行时不显示控制台窗口对于游戏是好的。--add-data assets;assets: 将本地的assets文件夹复制到打包后的程序内部分号前是源路径分号后是目标路径在打包程序中。src/main.py: 你的主程序入口。打包完成后在dist文件夹里就能找到可执行文件。你可以把这个文件单独发给别人运行。第一次打包可能会遇到各种依赖问题需要耐心根据错误信息调整命令或代码。5.3 功能扩展与优化方向一个基础的“打飞机”完成后你可以从很多方向去扩展它让它更像一个完整的游戏增加敌机种类创建不同的敌机类如FastEnemy,TankEnemy拥有不同的速度、生命值、外观和得分。在主循环的生成逻辑中随机产生不同种类的敌机。添加道具系统让敌机被击落后有概率掉落道具如火力增强、生命恢复、护盾。创建一个PowerUp精灵类玩家碰撞后触发效果。这需要引入更多的游戏状态管理。实现关卡与难度递增随着分数增加提高敌机生成频率、速度或者引入新的敌机类型。可以定义一个level变量分数每达到一定阈值level加1并调整游戏参数。加入粒子特效敌机爆炸时不只是消失可以产生一些小的火花粒子飞溅。这需要实现一个简单的粒子系统管理大量短生命周期的、有运动轨迹的小精灵。优化代码结构将游戏状态分数、生命、关卡封装到一个GameState类中。将精灵管理、碰撞检测、UI绘制等逻辑从主循环中抽离出来放到独立的函数或类方法里让主循环保持简洁。添加开始菜单与暂停功能如前所述引入状态机。在开始菜单状态绘制按钮并检测鼠标点击。在游戏状态按ESC键可以切换到暂停状态。这个项目就像一块游戏开发的敲门砖。通过实现它你不仅学会了Pygame的基本使用更重要的是理解了游戏循环、状态更新、事件驱动、资源管理等通用概念。这些知识是相通的未来无论是学习更复杂的2D框架还是转向3D游戏开发都会是一份宝贵的经验。动手去改去加新功能遇到问题就查文档、搜索、调试这才是提升最快的方式。