1. 项目概述:从零到一,用Python复刻经典“打飞机”
最近在整理旧项目时,翻出了一个几年前用Python写的“打飞机”小游戏。这个项目虽然不大,但麻雀虽小五脏俱全,涵盖了游戏开发中从图形渲染、事件处理到碰撞检测、状态管理等一系列核心概念。更重要的是,我把它完整地适配到了Python 3.7环境,确保在当下主流的Python版本上也能顺畅运行。对于想入门游戏开发,或者想找一个综合性练手项目的朋友来说,这个案例再合适不过了。
这个“打飞机”游戏,本质上是一个2D的纵向卷轴射击游戏。玩家控制一架飞机在屏幕底部移动,发射子弹消灭从屏幕上方不断出现的敌机,同时躲避敌机的撞击和子弹。听起来简单,但实现起来,你需要处理图像加载与显示、键盘事件响应、游戏主循环逻辑、精灵(Sprite)的状态更新与绘制、矩形碰撞检测,以及分数、生命值等游戏状态的管理。整个过程,就像是在用代码搭建一个微型的、可交互的动画世界。
我选择Python来实现,主要是看中它的简洁和丰富的库生态。对于这类2D小游戏,Pygame库是不二之选。它封装了SDL(Simple 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或PowerShell,macOS/Linux上是Terminal),输入python --version,如果显示Python 3.7.x,就说明安装成功了。
虚拟环境的重要性:我强烈建议为这个项目创建一个独立的虚拟环境。这能确保项目依赖的库(比如Pygame)不会干扰到你系统上其他Python项目。创建方法很简单:在你选定的项目目录下,运行python -m venv venv。这会在当前目录创建一个名为venv的文件夹,里面包含了一个独立的Python解释器和pip。激活虚拟环境后,所有通过pip install安装的包都会装在这个隔离的环境里。
- Windows激活:
venv\Scripts\activate - macOS/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(f'Score: {score}', True, WHITE) lives_text = font.render(f'Lives: {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为循环播放,loops=0(默认)播放一次。 - 错误处理:如果声音文件加载失败,程序可能会崩溃。在生产环境中,可以考虑用
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()且音量>0 |
5.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游戏开发,都会是一份宝贵的经验。动手去改,去加新功能,遇到问题就查文档、搜索、调试,这才是提升最快的方式。