简介:基于Python实现的Flappy Bird游戏课程设计项目包,面向学习Pygame游戏开发、需要完成课程设计或入门游戏编程的读者。项目包含完整可运行源码与设计报告,详细说明了游戏初始化、管道与地板移动、按键响应、小鸟运动模拟及碰撞检测等核心模块的实现思路,并配有模块划分和界面资源,便于直接运行、修改和二次开发。压缩包共96个文件,涵盖52张png图片素材、18个py源码文件、10个wav与10个ogg音效、2个ico图标、docx设计报告及说明文档等,整体仅1.56MB,结构清晰。该资源已有378人学习使用,能帮助读者快速掌握Pygame游戏循环、事件处理和碰撞检测等关键技能,适合作为Python游戏开发练习和课程设计参考。
1. 用Python还原Flappy Bird,其实是在学一套完整游戏框架
看到“基于Python制作flappybird游戏”这个标题,第一反应可能是“pygame写个几百行的小项目”。实际做下来会发现,它真正训练的是对游戏循环、状态机、碰撞检测和物理模拟的整体掌控,难点不在Flappy Bird规则本身,而在“怎么让代码表达出手感”。如果只是把鸟当作一张上下移动的图片,程序一分钟就能跑通,但玩起来完全不对味。下面这套方案以Python 3.10+和pygame 2.x为基准,从零搭一个真正能玩、能调、能打包的版本,讨论最稳妥的落地路径,也是入门2D游戏开发最顺的一条线。
2. 基于Python做Flappy Bird:引擎选型与最小运行框架
2.1 为什么选择pygame,而不是tkinter或arcade
做Flappy Bird,第一件事是决定图形库。tkinter适合表单和简单点击,绘制大量贴图还想维持60帧更新会非常吃力;arcade把精灵系统和物理封装得高级,但依赖安装和陌生API会抬高门槛。pygame生态成熟、资料多,把窗口创建、事件队列、图像渲染、碰撞检测全部封装成现成接口,尤其适合这个项目。
下面表格里列了四个常见选择,方便按场景取舍:
| 方案 | 渲染方式 | 碰撞检测 | 学习成本 | 适合程度 |
|---|---|---|---|---|
| tkinter | Canvas绘制 | 坐标手动判断 | 低但上限低 | 不适合高频刷新 |
| pygame | Surface blit | 自带Rect碰撞 | 中 | 最合适 |
| arcade | 现代OpenGL封装 | 自带精灵碰撞 | 中高 | 略重 |
| pyglet | OpenGL窗口 | 手动或封装 | 高 | 不推荐起步 |
Flappy Bird的核心是“高帧率下的位置更新+矩形碰撞”。pygame的Rect对象提供colliderect方法,鸟和管道都用矩形近似,判断效率高,代码也更直白。选pygame不是因为它最华丽,而是因为它把2D小游戏最常用的能力放到了离用户最近的位置。
安装建议走虚拟环境,避免污染系统Python。Windows、Linux和macOS通用:
python -m venv venv venv\Scripts\activate # Windows环境 source venv/bin/activate # macOS或Linux环境 pip install pygame --index-url https://pypi.tuna.tsinghua.edu.cn/simple这里--index-url是配置国内源地址,下载速度会明显更快。如果读者在vscode里打开终端,务必确认当前解释器切换到了venv下的Python,否则会出现“明明装好了却import不到”的情况,这是新手最容易踩的环境错位坑。
提示:安装完成后执行
python -c "import pygame; print(pygame.__version__)",输出版本号说明pygame可用。
2.2 Flappy Bird典型游戏循环:事件、更新、渲染顺序
pygame程序骨架是“事件输入—逻辑更新—画面重绘”的重复。Flappy Bird的循环与这个骨架完全一致,但比普通演示多一个关键点:物理更新必须与帧率解耦。常见做法是用pygame.time.Clock固定FPS,每帧按固定步长更新鸟的垂直位置和管道水平位移,避免高刷新率屏幕让游戏速度失控。
能直接运行的最小框架如下:
import pygame pygame.init() screen = pygame.display.set_mode((400, 600)) # 创建400x600窗口 clock = pygame.time.Clock() # 用于控制帧率 running = True while running: for event in pygame.event.get(): if event.type == pygame.QUIT: # 点关闭按钮时退出循环 running = False screen.fill((135, 206, 235)) pygame.display.flip() # 把缓冲内容绘制到屏幕 clock.tick(60) # 锁定60帧每秒pygame.display.set_mode里的元组控制窗口大小,Flappy Bird常用400×600或480×640,这样管道高度和鸟的像素尺寸比例比较合理。clock.tick(60)让程序每秒最多循环60次,游戏逻辑每帧约16.67毫秒。更新和绘制的顺序不能颠倒:先处理输入和逻辑,最后调用pygame.display.flip()把缓冲画到屏幕。很多“窗口卡住不动”的问题,都是漏掉了pygame.event.get()或flip()导致的。
2.3 坐标、贴图与帧率:pygame里的基础设定
pygame的坐标系以左上角为原点,x向右增大,y向下增大。“鸟下落”实际是鸟的y坐标增加,“跳跃”是让y方向的速度反向。玩家看到的所有运动都发生在虚拟二维坐标空间,贴图只是被画到这个坐标系上。下面这段代码展示用Surface和Rect管理鸟的位置:
bird_image = pygame.Surface((34, 24)) # 先用矩形贴图代替 bird_image.fill((255, 255, 0)) # 填充黄色,方便观察 bird_rect = bird_image.get_rect() # 获取矩形碰撞范围 bird_rect.center = (100, 300) # 鸟的初始中心点这里先生成一张34×24的占位矩形,用get_rect()拿到它的矩形范围,center设定初始位置。之后的移动和碰撞判断都基于bird_rect,而不是逐像素判断。管道、地面这些元素也应该统一用Rect描述,因为后面的碰撞判定要和pygame提供的方法直接对接。
帧率设置还会影响物理计算。如果把重力当作“每帧的下落像素数”,帧率变化会让游戏难度变化,这是新手容易忽略的问题。严谨的做法是把物理量统一成“每秒像素”。不过在固定FPS=60的前提下,按帧计算是第一版最直观的方式,性能也稳定。项目进入调优阶段后,再逐步把参数改造成基于时间计算,更符合正式项目习惯。
3. 可复现的完整实现:Python+pygame写一个能玩的Flappy Bird
3.1 初始化窗口、地面与游戏资源
一个完整的Flappy Bird项目,文件结构尽量简单,对初学者友好。建议目录结构如下:
flappy/ ├── main.py ├── assets/ │ ├── bird.png │ ├── pipe.png │ └── background.png └── requirements.txt先建主文件main.py,把常量和初始化放在顶部,后续调参集中在同一个位置:
import pygame import random pygame.init() WIDTH, HEIGHT = 400, 600 FPS = 60 GRAVITY = 0.5 # 每帧增加的下落速度 JUMP_SPEED = -8.5 # 按下空格后鸟的垂直初速度,向上为负 PIPE_SPEED = 3 # 管道每帧左移像素 PIPE_INTERVAL = 300 # 管道生成间隔,单位毫秒 screen = pygame.display.set_mode((WIDTH, HEIGHT)) clock = pygame.time.Clock()这些常量和参数共同决定游戏的基础手感,具体对应关系如下:
| 常量 | 推荐值 | 作用 |
|---|---|---|
| WIDTH / HEIGHT | 400 / 600 | 窗口与逻辑空间尺寸 |
| FPS | 60 | 锁定帧率 |
| GRAVITY | 0.5 | 每帧给下落速度加多少 |
| JUMP_SPEED | -8.5 | 跳跃时的垂直初速度 |
| PIPE_SPEED | 3 | 管道组的横向移动速度 |
| PIPE_INTERVAL | 300 | 相邻两组管道的生成间隔 |
管道通常是上下成对出现的,中间留出鸟能穿过的空隙。生成逻辑封装成一个函数:
def generate_pipe(): gap = 150 # 上下管道之间的空隙高度 pipe_height = random.randint(100, 400) # 上管高度随机 top_pipe = pygame.Rect(WIDTH, 0, 60, pipe_height) # 上管道 bottom_pipe = pygame.Rect(WIDTH, pipe_height + gap, 60, HEIGHT - pipe_height - gap) # 下管道 return top_pipe, bottom_pipe这里把两根管道当作两个矩形:上管道从窗口顶部向下延伸,下管道从空隙底部延伸到底部。gap为150像素时,第一版对新手比较宽容;想要原版那种压迫感,可以逐步减到130甚至120。random.randint让每根管道高度不同,避免游戏变成机械操作。
3.2 玩家控制:鸟的下落、跳跃与旋转
鸟在游戏中不是匀速运动,而是持续受重力影响。每帧更新逻辑是:速度增加一个重力值,位置加上速度,然后检查是否碰到地面或天花板。跳跃的本质是让垂直速度重置为负值,之后重力再把它拉回来:
bird_y_speed = 0 bird_rect = pygame.Rect(80, 250, 34, 24) # 主循环中的物理更新 bird_y_speed += GRAVITY bird_rect.y += bird_y_speed # 在事件循环里处理按键 if event.type == pygame.KEYDOWN: if event.key == pygame.K_SPACE: bird_y_speed = JUMP_SPEED # 给鸟一个向上的初速度这里没有修改鸟的x坐标,因为Flappy Bird中鸟的水平位置固定,玩家只控制垂直方向。GRAVITY值越大下落越快,游戏越“沉”;JUMP_SPEED为负数表示向上速度,绝对值越大跳得越高。要接近原版手感,GRAVITY多在0.4到0.6之间,JUMP_SPEED在-7.5到-9之间,两者必须配合测试,不能单独调一个。
为了让画面不呆板,可以给鸟加旋转效果,让它在上冲和下落时呈现不同俯仰角。方法是根据当前垂直速度旋转贴图:
angle = -25 if bird_y_speed < 0 else min(25, bird_y_speed * 3) rotated_bird = pygame.transform.rotate(bird_image, angle)旋转后图像尺寸会变化,绘制时要重新取中心位置,否则鸟旋转时会来回抖动。这个细节在换成真实素材后尤其明显。
3.3 管道生成、位置更新与碰撞检测
管道组移动很机械:每帧把所有管道左移PIPE_SPEED个像素,移除完全离开屏幕的管道,同时按间隔生成新管道。用列表保存所有管道矩形,方便遍历:
pipes = [] last_pipe_time = pygame.time.get_ticks() # 游戏循环中 now = pygame.time.get_ticks() if now - last_pipe_time > PIPE_INTERVAL: pipes.extend(generate_pipe()) # 上下两根同时加入 last_pipe_time = now for pipe in pipes: pipe.x -= PIPE_SPEED # 向左移动管道 pipes = [p for p in pipes if p.x > -70] # 移除完全离开屏幕的管道用pygame.time.get_ticks()获取程序运行毫秒数,和上次生成时间做差,能在不阻塞游戏循环的前提下按时间间隔生成管道。因为一次生成两根矩形,所以用pipes.extend()同时加入两个元素。清理管道时要求p.x > -70,确保整根管子完全移出屏幕才销毁,避免碰撞判定遗漏。
碰撞检测的写法依赖“两个矩形是否有重叠”:
for pipe in pipes: if bird_rect.colliderect(pipe): # 矩形相交即判定死亡 running = False if bird_rect.top <= 0 or bird_rect.bottom >= ground_y: running = Falsecolliderect在pygame里返回两个矩形是否相交,只要鸟和任意管道相交,游戏就进入结束流程。更严苛的做法是用pygame.mask.from_surface做像素级碰撞,但Flappy Bird并不需要,矩形碰撞已足够精确,还留有一定容错空间。
3.4 记分逻辑与游戏状态管理
为了让游戏有结束和重开流程,状态管理必不可少。常见做法是定义三个状态:START、PLAYING、GAME_OVER,主循环根据不同状态处理事件和绘制内容。下面是一个精简的计分逻辑:
state = STATE_START score = 0 # 每帧检查鸟是否通过某一组管道 if bird_rect.x > pipe[0].right and not pipe_passed: score += 1 # 越过该组管道才加分 pipe_passed = True计分只在鸟越过管道右侧时加一次,所以每组管道要带上“是否已得分”标记。如果不去重,同一组管道会在几十帧内反复加分数,这是新手最容易遇到的“分数疯狂跳动”问题。状态切换时,要重置鸟的位置、速度和管道列表,否则重开会继承上一局残留数据,导致开局即死。
重置函数的作用不是“给变量归零”,而是为下一局创建干净的初始环境:
def reset_game(): global bird_rect, bird_y_speed, pipes, score, state bird_rect.y = 250 bird_y_speed = 0 pipes.clear() score = 0 state = STATE_PLAYING所有全局可变状态在重开时都要覆盖,特别是下落速度。它如果不归零,新一局鸟会从空中直接砸向地面,玩家基本无法操作。
3.5 一版可运行的main.py完整示例
把以上模块组合起来,就是一个约120行的完整游戏。先加载资源再进入循环:
pygame.init() screen = pygame.display.set_mode((WIDTH, HEIGHT)) clock = pygame.time.Clock() bird_image = pygame.image.load("assets/bird.png") pipe_image = pygame.image.load("assets/pipe.png") background = pygame.image.load("assets/background.png")暂时没有美术素材时,用pygame.Surface画矩形代替完全可行。把图片、音效放进assets目录,用相对路径导入,能保证在项目目录下运行时不出现“找不到文件”的报错。pygame.image.load返回的Surface支持缩放:pygame.transform.scale(bird_image, (34, 24)),这能让不同尺寸素材统一到同一套物理尺寸。
到这里,一个可玩的版本已经成立。后续工作从“能运行”走向“手感好”,才是这个项目最值得投入的地方。
4. 游戏手感调优:重力、管道间距与常见运行错误排查
4.1 三个决定手感的参数:重力、跳跃初速、管道间距
Flappy Bird最骗人的地方在于规则简单,但手感极难调。影响手感的核心是GRAVITY、JUMP_SPEED和管道空隙gap三个值。这三者匹配决定一局是“轻松二十秒”还是“三秒去世”。我常用的调法是:先固定gap,再配合调整重力和跳跃速度,最后回头重新调整gap。
测试时,先设置一个能连续按键也不瞬间坠落的初始值,比如重力0.5、跳跃速度-8.5。观察鸟从底部弹起的高度,正常情况下一次跳跃的上升幅度约为屏幕高度的四分之一。如果上升太矮,把JUMP_SPEED绝对值调大;如果下落快得没有滞空感,把GRAVITY降低。每次只改一个参数,然后实际游玩对比,这是最有效率的调试路径。
| 参数 | 典型范围 | 对游戏的影响 | 手感倾向 |
|---|---|---|---|
| GRAVITY | 0.4 ~ 0.7 | 下落加速度,越大操作窗口越短 | 越大越“沉” |
| JUMP_SPEED | -7.5 ~ -10 | 一次按键的上升幅度 | 绝对值越大越“飘” |
| gap | 120 ~ 170 | 管道空隙,越小越苛刻 | 越小容错越低 |
| PIPE_SPEED | 2 ~ 4 | 管道横向推进速度 | 越大难度越高 |
这些参数是联动的,gravity=0.6配jump=-10和gravity=0.4配jump=-7.5,实际手感完全不同。没有双人测试条件时,可以记录“从开局到撞到第一根管道的时间”来量化难度曲线,这是一个可复现的验证方法。
4.2 碰撞边界:图片透明区域和Rect的误差
鸟和管道的PNG通常带透明边距,直接用完整矩形做碰撞,视觉上会出现“明明没碰到却死了”的情况。原因是透明区域也被包在矩形里。处理方向有两个:一是美术素材尽量紧凑,减少透明内边距;二是手动把碰撞矩形缩小一圈。pygame中可以直接基于原矩形生成缩小判定区域:
hit_rect = bird_rect.inflate(-6, -4) # 宽缩小6像素,高缩小4像素 if hit_rect.colliderect(pipe): running = Falseinflate传入负值,表示在水平和垂直方向各向内收缩。这里宽度缩6像素、高度缩4像素,玩家的死亡判定比视觉边界宽松几个像素,能明显降低“莫名死亡”的挫败感。但不要过度缩小,否则鸟半个身子穿过管道却不出局,这个界限要靠反复试玩来把握。
4.3 常见错误排查:模块导入失败、图片路径、帧率不稳
初学者在本地运行这个项目,最容易卡在三个环节。第一,ModuleNotFoundError: No module named 'pygame',几乎都因为运行Python的环境和安装pygame的环境不一致。vscode里要确认左下角解释器已切换为venv;终端要确认虚拟环境已激活。用python -m pip install pygame代替pip install pygame,能避免系统多版本Python造成的装错位置。
第二,pygame.error: Couldn't open bird.png。这通常由工作目录不对导致。一个稳定的解决办法是在程序入口切换工作目录:
import os os.chdir(os.path.dirname(os.path.abspath(__file__)))把当前工作目录切换到脚本所在目录,随后用相对路径加载资源,无论在哪个位置启动项目都不会报错。这一点对从网页教程复制代码的读者尤其重要,因为很多教程默认你已经站在项目根目录运行。
第三,游戏画面卡顿。原因往往是图片加载和缩放每帧都在做,导致性能浪费。正确做法是在主循环外完成加载与缩放,循环内只做blit绘制和坐标计算:
bird_image = pygame.image.load("assets/bird.png").convert_alpha() bird_image = pygame.transform.scale(bird_image, (34, 24))convert_alpha()把图片转换成当前显示模式匹配的格式,后续逐帧绘制会更快。处理带透明背景的PNG效果尤其明显,是切素材后最容易被忽略的小优化。
5. 一键打包与运行验证:把pygame项目变成可执行文件
双击运行自己写好的游戏,项目才算真正完成。把Flappy Bird打包成Windows可执行文件,常用工具是PyInstaller。先安装打包依赖:
pip install pyinstaller pyinstaller -F -n FlappyBird --add-data "assets;assets" main.py-F表示打包成单个exe文件,--add-data把assets目录一并打进包。分号前是源目录,分号后是解压后的目标目录,Windows用分号分隔,Linux和macOS需要改成冒号。打包完成后,exe在dist目录里。
使用-F打包的缺点是启动较慢,程序运行时先把依赖解压到临时目录。更看重启动性能时,可以去掉-F改用目录模式:
pyinstaller -D -n FlappyBird --icon bird.ico --add-data "assets;assets" main.py这样生成dist/FlappyBird/文件夹,内部FlappyBird.exe双击即可运行。如果代码里用了os.chdir切换工作目录,PyInstaller打包后临时目录会不同,读取资源最好以sys._MEIPASS为基准:
base_path = getattr(sys, "_MEIPASS", os.path.abspath(".")) bird_image = pygame.image.load(os.path.join(base_path, "assets", "bird.png"))sys._MEIPASS是PyInstaller在单文件运行时存放解压资源的临时目录,开发环境没有该属性,所以用getattr设默认值。这样同一套代码既能源码运行,也能打包后运行。打包完成后做一次完整验证:双击exe,测试开始界面、跳跃、管道生成和计分;把assets目录临时改名,确认程序不会因缺少资源闪退;关闭窗口后打开任务管理器,确认FlappyBird.exe没有残留。出现缺少python311.dll的报错,说明打包环境Python版本与运行机器不一致,需要搭建对应版本的干净环境后重新执行PyInstaller。把dist目录压缩成zip之前,先确认exe已退出进程,再连同assets目录一起交付压缩包。
本文还有配套的精品资源,点击获取