☰
Python Pygame实现Flappy Bird:从游戏循环到碰撞检测全解析
2026/10/14 11:55:48 网站建设 项目流程

简介:基于 Python 与 pygame 开发的「飞扬的小鸟」完整源码,面向游戏开发初学者和 Python 入门者,适合用来自学 pygame 游戏开发流程或作为课堂教学案例。资源共 76 个文件,压缩包仅 134KB:72 张 png 素材覆盖背景、小鸟动画、管道、数字、按钮、奖牌和文字提示,3 个 py 文件包含主程序、图集处理等代码,另附 1 个 txt 说明文件,整体结构清晰,便于学习者快速定位文件。已有 3397 人学习下载。通过这份源码可以理解 pygame 的初始化、窗口创建、图像加载、事件循环、帧率控制、物理模拟(重力、速度)和矩形碰撞检测等关键机制;代码以类封装 Bird、Pipe 等对象,主循环简洁直接,便于阅读、调试和二次修改,这对理解游戏状态管理也很有帮助。拿到手后既能直接运行体验游戏,也能在此基础上扩展计分、音效或新关卡,是练习 Python 与游戏开发不可多得的轻量级项目。

1. 飞扬的小鸟用 Python 和 Pygame 做出来,难度比你想的低一个量级

python版飞扬的小鸟源代码,远不像它的游戏画面那样让人手忙脚乱。这个项目把 Pygame 最常用的能力全部串了一遍:加载素材、事件响应、重力模拟、管道循环、矩形碰撞、得分记录。整套代码加上几张图片和一个主控制脚本,就是一份能跑、能改、能做课程设计的基础套餐。适合刚学完 Python 语法但没见过完整游戏循环的人,也适合需要给学生演示“状态机 + 精灵组”的指导者。下面从我拆包和重跑的经验出发,把运行方式、参数边界和真正坑人的细节一次说清楚。

2. 代码结构与素材组织:打开压缩包后先认清这几件事

拿到资源后别急着运行。先看目录和素材。大多数类似项目会有一个 main.py,有的还会拆出 config.py、sprites.py。素材目录是 assets,里面分成 images 和 audio。这个结构不是随便排的,Pygame 在加载素材时不会帮你找路径,所有路径都必须由代码拼接出来,而且不同素材的尺寸会直接影响游戏运行效果。

2.1 素材加载:convert_alpha 和绝对路径缺一不可

先看一个常见的目录树:

flappy_bird/ ├─ main.py ├─ config.py ├─ sprites.py └─ assets/ ├─ images/ │ ├─ background.png │ ├─ bird.png │ ├─ ground.png │ └─ pipe.png └─ audio/ ├─ hit.wav └─ point.wav

代码中加载图片不能只写文件名。因为运行脚本时当前工作目录可能是终端所在的位置,而不是 .py 文件所在目录。如果不拼绝对路径,就会遇到 FileNotFoundError 或者黑屏闪退。我一般会用下面这段兜底:

import os import pygame BASE_DIR = os.path.dirname(os.path.abspath(__file__)) IMAGE_DIR = os.path.join(BASE_DIR, "assets", "images") AUDIO_DIR = os.path.join(BASE_DIR, "assets", "audio") def load_image(filename): path = os.path.join(IMAGE_DIR, filename) try: # convert_alpha 能提升透明图片绘制效率,也保留 alpha 通道 surface = pygame.image.load(path).convert_alpha() except pygame.error as err: raise RuntimeError(f"图片加载失败: {path}, {err}") from err return surface def load_sound(filename): path = os.path.join(AUDIO_DIR, filename) try: # wav 文件建议直接加载,ogg 也可以 sound = pygame.mixer.Sound(path) except pygame.error as err: raise RuntimeError(f"音频加载失败: {path}, {err}") from err return sound

逻辑说明:BASE_DIR 用file得到脚本所在目录,再用 os.path.join 拼出资源目录。这样即使从其他目录启动,也不会因为相对路径找不到资源。convert_alpha() 是把图片转成带透明度的 Surface,对 bird.png 这种有镂空的素材尤其重要;如果不调用,半透明边缘可能会变成黑色方块。参数说明:如果你手头素材是 .jpg,就不要用 convert_alpha,直接 convert() 就行,因为 JPEG 没有 alpha 通道;wav 格式兼容性最好,mp3 在部分环境可能出现延迟。

素材尺寸也值得提。鸟的贴图一般 40x30 左右,管道图宽度在 60 到 80 像素之间,背景图建议和窗口尺寸一致。如果素材过大,比如背景图是 1920x1080,但窗口只有 400x700,Pygame 不会自动缩放,结果只会显示图片左上角一块,游戏界面直接“切掉一半”。遇到这种情况需要:

background = pygame.transform.scale( load_image("background.png"), (WIDTH, HEIGHT) )

参数说明:transform.scale 第一个参数是原图,第二个参数是目标尺寸。我习惯把窗口尺寸 WIDTH、HEIGHT 放在 config.py,这样换背景素材时只要改一处。

素材文件命名也要留意:有些压缩包里的图片是大写或者带中文名,Pygame 对中文路径在老版本里会有编码问题。所以如果下载的素材名带中文,我会第一时间改成小写英文。类似 bird.PNG、background2x.png 这类命名,建议在复制进项目时顺手统一风格。

下面这张表是我整理素材时常用的参考,按这个口径去准备,基本不会出大问题:

文件建议尺寸/格式用途
background.png与窗口等大,PNG-32背景层
bird.png约 40x30,PNG-32小鸟主体
ground.png与窗口等宽,高度约 100地面滚动条
pipe.png宽约 70,高约 400,PNG-32上下管道共用

2.2 精灵组和矩形碰撞:Pygame 管理对象的关键习惯

游戏里的小鸟、管道、地面,本质上都是 Sprite 对象。Pygame 的 Sprite.Group 是一个集合容器,统一处理 update()、绘制和碰撞检测。常见做法是在 main.py 里创建两个组:all_sprites 管所有元素,pipes 只管管道,方便单独判断碰撞。

import pygame all_sprites = pygame.sprite.Group() pipes = pygame.sprite.Group() bird = Bird(120, 350) all_sprites.add(bird) def spawn_pipe(): pipe = PipePair(400) pipes.add(pipe.top_pipe, pipe.bottom_pipe) all_sprites.add(pipe.top_pipe, pipe.bottom_pipe)

逻辑说明:把不同游戏对象加入同一个 all_sprites 后,主循环只要执行 all_sprites.update() 就能让所有对象移动,all_sprites.draw(screen) 就能按顺序绘制。pipes 组单独保留,是为了碰撞时不需要遍历 all_sprites 里其他对象。参数说明:注意管道是一上一下两根,一个 PipePair 逻辑对象需要把两根管道分别加入组,否则绘制和碰撞会漏掉一半。

另外,Pygame 的 Sprite 默认自带 rect,rect 的中心点或左上角决定了图片画在哪。碰撞检测的本质是比较 rect 的矩形区域是否相交。对初学来说,这个矩形碰撞已经够用,但要注意后面避坑章节里提到的“透明区域误撞”。

这里还要强调一点:不要把素材文件和代码混在同一个目录。至少保留 assets 子目录。后续如果要把游戏打包成 exe,素材目录独立会让打包工具的 --add-data 参数好写很多。我在实际操作中见过太多把图片和 .py 放一起的项目,改两行代码就把 png 覆盖了。

组内排序同样有讲究。Pygame 的 draw 方法按加入顺序绘制,后加入的会盖住先加入的。我一般把背景 ground 放在最底层,其次是管道,最后是小鸟。否则小鸟会被管道遮住。如果代码里习惯用 Z 序,就维护一个 depth 属性,在 draw 前按 depth 排序。对一个教学项目来说,直接用加入顺序控制更直观。

3. 环境准备与启动参数:把这份源码跑起来的前三步

这个项目理论上只需要 Python 和 Pygame,但版本差异能带来不少“玄学”问题。先确定环境,再谈运行。很多下载资源在 README 里写得含糊,只说“运行 main.py”,结果新手上路第一关就是装环境。

3.1 python 和 pygame 版本怎么选

我建议使用 Python 3.10 或 3.11 稳定版,配 pygame 2.5 以上。pygame 的 1.9 系列也能跑这个项目,但在 Windows 高 DPI 和 Python 3.9 以上的环境会遇到兼容问题。安装只需要一条命令:

python -m pip install pygame

装完立即验证:

python -c "import pygame; print(pygame.version.ver)"

这个命令会输出类似 "2.5.2" 的版本号。如果报 ModuleNotFoundError,说明当前 python 解释器和 pip 不是同一个环境,常见于电脑里装了两个 Python。解决方法是改用 python3 -m pip install pygame,或者直接使用虚拟环境:

python -m venv venv source venv/bin/activate # Windows 下用 venv\Scripts\activate python -m pip install pygame

参数说明:python -m pip 比 pip 单独命令更可控,它确保 pip 装到当前 python 对应的 site-packages 里。很多“明明装过却导入失败”的问题,根源就是 pip 和 python 指向不同的解释器。虚拟环境的好处是项目依赖隔离,哪怕系统里已经有其他 pygame 版本,也不会互相污染。

如果你所在网络下载慢,可以给 pip 指定镜像源,但我不建议在文章里推荐特定镜像。实际处理时,只要把-i https://pypi.tuna...这类参数换成可用的机构镜像即可。注意这些源更新时机不同,装出来的 pygame 可能不是最新版,但也足够跑这个项目。

3.2 启动脚本、入口函数和帧率控制

资源里一般都有一个 main.py。先看是不是有if __name__ == "__main__":这段入口。规范的写法是:

import pygame from config import FPS, WIDTH, HEIGHT class Game: def __init__(self): pygame.init() pygame.display.set_caption("Flappy Bird") self.screen = pygame.display.set_mode((WIDTH, HEIGHT)) self.clock = pygame.time.Clock() self.running = True def run(self): while self.running: for event in pygame.event.get(): if event.type == pygame.QUIT: self.running = False # 核心更新逻辑 pygame.display.flip() self.clock.tick(FPS) pygame.quit() if __name__ == "__main__": Game().run()

逻辑说明:while 循环是游戏的生命线,pygame.event.get() 把键盘、鼠标和关闭窗口事件取出来处理。pygame.display.flip() 把绘制内容显示到屏幕上。self.clock.tick(FPS) 用来让循环每帧停顿一定时间。参数说明:FPS 设在 config.py 里,一般 60 最流畅;如果电脑性能较差,30 也可以跑,但管道速度要相应调整,否则游戏节奏会比原版慢。

这里要特别指出:pygame.event.get() 必须在每一帧调用,否则窗口会“假死”。这是新手最常见的翻车点,后面避坑章还会再提。

如果源码里没有 config.py,也可以把 FPS 直接定义在 main.py 顶部。我更建议拆一个 config.py 出来,因为后续调整窗口大小、管道间隙、重力系数都会很频繁。把所有魔法数字集中到一个文件里,别人接手时也容易看懂。

窗口尺寸方面,经典 Flappy Bird 是竖屏 400x700。如果改成横屏 700x400,需要同步调整背景、地面高度和管道生成范围,不是简单改 set_mode 就能完成。所以刚开始跑源码时,不要随意改窗口比例。

3.3 素材缺失和当前工作目录的排查

启动时最容易遇到的黑匣子就是 file not found。但代码里明明写了 assets/images/bird.png,为什么找不到?因为 Python 打开相对路径时参考的是“当前工作目录”,不是脚本所在目录。如果你在桌面上打开终端,运行python ~/downloads/flappy/main.py,相对路径就指向桌面。解决方式是像 2.1 节那样用file拼绝对路径。

如果资源本身就没复制全,可以加启动检测:

import sys import os import pygame REQUIRED = [ "assets/images/background.png", "assets/images/bird.png", "assets/images/pipe.png", ] def check_assets(): for item in REQUIRED: if not os.path.exists(item): print(f"[检查失败] 缺少 {item}") sys.exit(1)

逻辑说明:启动前把关键文件挨个检查一遍。文件不存在就直接终止,比运行到一半才报错容易定位。我之前拆包遇到一个项目,图片文件都在,但音频少了一个,导致运行到“得分”时才会触发异常,这种问题很难第一时间定位。提前检查能少走弯路。参数说明:REQUIRED 列表里的路径是相对项目根目录的,运行时要把根目录设成 project 目录。

更常见的坑是素材文件占位错误:下载的压缩包里有 bad_apple.mp3 或者 Docs.txt 这类多余文件,却被代码误用。所以检查时最好校验文件大小,小于 1KB 的很可能是坏文件。如果代码是用 pygame.mixer.music.load 加载 mp3,还需要把初始化提前:

pygame.mixer.pre_init(44100, -16, 2, 512) pygame.init()

参数说明:44100 是采样率,-16 位深度,2 声道,512 缓冲区。缓冲区越大越不容易爆音,但加载稍有延迟。试玩时感觉音频有卡顿,优先调大这个值。

4. 核心玩法实现解析:三个函数看懂小鸟移动、管道循环与得分

整个游戏的玩法浓缩在几十行代码里。读懂这一段,改难度、换素材都不难。很多下载资源把代码写得又长又乱,但核心逻辑就是“重力、管道、碰撞”三件事。

4.1 小鸟的重力和扇动逻辑

小鸟不是真正在飞,而是用一个加速度公式模拟下落。先定义 Bird 类:

class Bird(pygame.sprite.Sprite): def __init__(self, x, y): super().__init__() self.image = load_image("bird.png") self.rect = self.image.get_rect(center=(x, y)) self.velocity = 0 # 当前垂直速度(像素/帧) self.gravity = 0.5 # 每帧速度增量 self.flap_power = -10 # 每次按键的瞬时速度 def update(self): self.velocity += self.gravity self.rect.y += self.velocity # 简单限制最高速度,避免一帧掉太多像素 if self.velocity > 15: self.velocity = 15 def flap(self): self.velocity = self.flap_power

逻辑说明:update() 每一帧把 gravity 加进 velocity,再把 velocity 加到 y 上,形成抛物线。flap() 把垂直速度重置成负值,让小鸟向上冲。限制最高速度是为了防止松弛状态下小鸟瞬间被甩出屏幕。参数说明:gravity 和 flap_power 是难度核心。gravity 越大,下落越快,难度越高;flap_power 负值越大,每次点击上升越猛。原版大约就是 0.5 和 -10,手感比较适中,适合教学演示。

注意这里的 velocity 单位是“像素/帧”。如果改动 FPS,实际手感会变化。60 帧下 0.5 的重力意味着每秒约 30 像素/秒的加速度。如果改成 30 帧,同样写 0.5,加速度只有一半,游戏会变得很飘。所以如果调 FPS,要把 gravity 同步放大或缩小。一个更稳妥的方案是把 gravity 写成基于时间的加速度,但教学项目多数直接写帧基数。

小鸟的贴图方向也值得注意。素材里的 bird.png 默认朝右,但原版游戏小鸟朝上,这就需要把图片旋转 90 度。Pygame 旋转:

self.original_image = load_image("bird.png") self.image = pygame.transform.rotate(self.original_image, -30) self.rect = self.image.get_rect(center=(x, y))

逻辑说明:transform.rotate 返回新 Surface,不会修改原图。我把原图保存在 original_image 里,方便根据上升或下降动态旋转角度。如果只旋转一次,rect 的尺寸会变化,需要用 get_rect(center=...) 重新对齐中心。

4.2 管道的生成、移动和循环

管道不能无限制 new 出来,否则内存会被撑爆。更稳妥的方案是复用一组管道,并在移出屏幕后回收。下面是用对象池思路写的生成段:

class PipePair: def __init__(self, x, gap_y, gap_height=150, speed=-4): self.speed = speed self.scored = False self.top_pipe = Pipe((x, gap_y - gap_height - 100)) self.bottom_pipe = Pipe((x, gap_y + gap_height + 100)) def move(self): self.top_pipe.rect.x += self.speed self.bottom_pipe.rect.x += self.speed def is_off_screen(self): return self.top_pipe.rect.right < 0

每次生成管道时,随机取一个 gap_y 作为通道中心,gap_height 是上下管道之间的空隙。这个空隙的取值要有边界,比如 130 到 180 之间,否则随机太小会形成“过不去的墙”。参数说明:speed 负值代表向左移动,绝对值越大游戏节奏越快。gap_y 要限制在 200 到 500 之间,确保最低管道避开地面,最高管道不顶到天花板。

这里的 Pipe 内部其实是一个 Sprite。如果只用一张管道图,上管道要旋转 180 度。简单实现可以在加载图片后:

top_surface = pygame.transform.flip(load_image("pipe.png"), False, True)

逻辑说明:pygame.transform.flip 第二个参数 False 表示水平不翻转,第三个 True 表示垂直翻转,正好用作上管道。注意翻转后图片的矩形的尺寸不变,但方向变了,坐标要对准。常见错误是上下管道共用一个相同 rect,导致顶上那根管道“飘”在屏幕里。最好在构造 top_pipe 时用 top_surface.get_rect(midbottom=...) 或 manual 定位。

管道生成要有节奏。在主循环里用一个距离计数器,每隔固定像素生成一对新管道:

if distance_since_last_pipe > PIPE_INTERVAL: spawn_pipe() distance_since_last_pipe = 0 else: distance_since_last_pipe += abs(PIPE_SPEED)

逻辑说明:distance_since_last_pipe 累加管道移动距离,当距离超过 PIPE_INTERVAL 时生成新管道。这样即使 FPS 有小幅波动,管道生成间隔也基本稳定。如果直接用帧计数,玩家设备帧率不同,游戏难度就会不一样。参数说明:PIPE_INTERVAL 一般在 220 到 280 像素之间,数值越小管道越密。我用 240 比较舒服。

4.3 得分与碰撞判定:别抠得太细,但边界必须严

得分时机是“小鸟的 centerx 越过某根管道中心”后加 1,且每根管道只能计一次,所以用 scored 标记。代码:

score = 0 for pipe_pair in active_pipes: if not pipe_pair.scored and bird.rect.centerx > pipe_pair.top_pipe.rect.right: score += 1 pipe_pair.scored = True

逻辑说明:用 top_pipe 的 right 边缘当判据值,是因为 bottom_pipe 的 left 略左于 top_pipe 的 right,两者几何关系容易混。撞地或撞天花板也要立刻结束,否则小鸟会无限下落。

碰撞判定一般用:

if pygame.sprite.spritecollide(bird, pipes, False): game_over() else: # 地面和天花板 if bird.rect.top <= 0 or bird.rect.bottom >= GROUND_Y: game_over()

参数说明:spritecollide 的第三个参数 False 表示不删除被碰撞的管道对象,否则会把管道组对象摧毁导致后续逻辑崩坏。矩形碰撞只计算矩形相交,透明背景也会撞到像素区域的矩形,所以后面会在避坑章里给像素级 mask 方案。

边界方面,我还建议在地面之上预留一个虚拟高度,让小鸟的翅膀贴近地面时不算撞。原因是原版素材里地面图片上方有一段草丛是视觉遮挡,矩形碰撞会把草丛算进去,玩家会觉得“明明没碰到管子就死了”。常见修法是设定地面碰撞线:

GROUND_Y = GROUND_IMAGE.get_height() COLLISION_GROUND_Y = GROUND_Y - 20 belly = bird.rect.move(0, 10).colliderect(ground_rect)

逻辑说明:把小鸟的矩形向下移动 10 像素再和地面矩形碰撞,等于给了 10 像素的“厚脸皮”容错。这样手感更接近直觉,也适合课程演示。如果直接在鸟的 rect 上改尺寸,会影响碰撞和绘制中心,所以我用 move 生成临时 rect,不污染原位置。

另外,管道生成后的清理也要及时。离线管道如果不从 active_pipes 和 all_sprites 中移除,group 越来越大,运行十几分钟后依然会卡顿。我一般在 is_off_screen() 返回 True 时执行:

for pipe_pair in active_pipes[:]: if pipe_pair.is_off_screen(): pipes.remove(pipe_pair.top_pipe, pipe_pair.bottom_pipe) all_sprites.remove(pipe_pair.top_pipe, pipe_pair.bottom_pipe) active_pipes.remove(pipe_pair)

注意这里用active_pipes[:]做切片复制,避免在遍历列表时删除元素导致的索引错位。这是一种 Python 常见误用,值得给新手特别说明。

5. 避坑:五份源码最常见的运行与手感问题

这一章专门列我踩过的坑,每条都按现象、原因、解决展开,希望能帮你省掉重复试错的时间。

5.1 窗口一闪而过,还没看清楚就退出

现象:运行 python main.py 后,窗口弹出不到一秒就消失。原因:代码里没有主循环,或者 while 循环里没有处理事件导致窗口进程立刻退出。解决:先看代码是否包含 while running:,且在循环末尾要有 pygame.display.flip()。如果循环体为空,也可以加self.clock.tick(FPS)观察窗口是否保持。一个可靠的自检办法是在 main.py 末尾加一段打印:

print("正常退出")

如果这行根本没有执行,说明程序在 init 之后的某一行直接抛异常并退出了。终端会打印 Traceback,你需要看最后一行错误。常见的是缺少窗口图标文件或背景素材路径不对。另一个容易忽略的问题:代码里调用了sys.exit()而没有包在函数里,导致用户点关闭时退出码非零,闹得好像程序出错。建议统一用pygame.quit()再返回。

5.2 按键没反应,小鸟一直不跳

现象:窗口正常显示,小鸟挂在半空,按空格或鼠标没有扇动。原因:事件检测写在某个不会被重复执行的初始化代码里,而不是主循环里。解决:确保 event 检测在主循环内,并且空格键的索引是 pygame.K_SPACE。有的人想兼容鼠标点击,又少写了 MOUSEBUTTONDOWN。常见做法是:

for event in pygame.event.get(): if event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE: bird.flap() elif event.type == pygame.MOUSEBUTTONDOWN: bird.flap()

如果还是没反应,检查是否把事件处理放在if game_over:分支里而没写 else。游戏结束后按空格不应直接复位,而应进入开始状态,否则你按了一百次空格也只是把速度改成 -10,随后继续碰撞死亡。

还有一个不起眼的原因:窗口没有获得焦点。如果你启动游戏后又点了别的窗口,键盘事件不会发给 Pygame。这个问题常见于双屏环境。可以在 init 之后调用 pygame.event.set_grab(True) 强制捕获鼠标,但那样又不方便调试。我更推荐在窗口标题栏点一下再操作,避免误会。

5.3 撞到透明背景的位置却算死亡

现象:小鸟在管道矩形之外,但视觉上并没有撞到像素,却判死了。原因:Sprite 的 rect 是图片的完整外框,PNG 的透明区域也算矩形范围。解决:用 Pygame 的 mask 碰撞,按像素做命中检测。改法很简单:

pygame.sprite.collide_mask(bird, pipe)

使用前提:加载图片时用了 convert_alpha(),且每个 Sprite 的 self.image 已准备好。collide_mask 基于图像的非透明像素计算,性能略慢,但这个场景完全没有压力。我曾经在一个简化版源码里看到作者用self.rect = self.image.get_rect(center=...)之后,又手动缩了 rect 尺寸,结果把小鸟的 rect 缩成只有 8x8,导致玩家点按灵敏但经常穿管。手动调整 rect 是可以的,但要写清楚偏移:

self.rect = self.image.get_rect(center=(x, y)) self.rect.inflate_ip(-8, -8)

参数说明:inflate_ip 会同时缩小两侧,x 和 y 方向各减 8 像素。注意这只改变碰撞区域,不影响绘制位置,因为绘制的 Surface 仍然是 self.image。如果你缩得太多,玩家会明显觉得“明明撞上了管子却没死”。我对小鸟的缩放到 60% 左右比较稳妥。

5.4 管道间隙忽大忽小,随机数生成影响难度

现象:玩起来难度不稳定,有时两管之间好大空隙,有时连续两个极限窄缝。原因:每个管道随机 gap_y 和 gap_height 用的是无约束 random.randint,没有限定范围。解决:把随机生成放到一个 factory 函数里,固定 gap_height 在合理区间:

import random GAP_Y_RANGE = (180, 520) GAP_HEIGHT_RANGE = (130, 180) def random_pipe_gap(): gap_y = random.randint(*GAP_Y_RANGE) gap_h = random.randint(*GAP_HEIGHT_RANGE) return gap_y, gap_h

除此之外,两个相邻管道之间的水平距离如果也是随机,就会出现“前一根刚过去,后一根立即撞上”的负体验。建议固定 PIPE_INTERVAL 为 240 像素左右,不要随机。控制难度时优先调 speed 和 gap_height,不要动 interval 的随机性,否则不同玩家体感差异巨大。

还有一个更隐蔽的问题:如果使用 Python 的 random 模块而不设置 seed,每次启动游戏的随机序列都不同。这本身没问题,但如果你要录屏做教程,最好加一句random.seed(42),让管道序列可复现。这样录制演示时每次讲到的障碍都一样,不至于录到一半突然出现一个极难的间隙。

5.5 运行几秒后音频卡顿,或声音完全消失

现象:图片和碰撞都正常,但 hit.wav 播放时有爆音,偶尔还听不到。原因:pygame.mixer 的默认初始化参数和音频文件不匹配,或者加载的音频格式是 mp3,解码不稳定。解决:在 pygame.init() 之前先预初始化 mixer:

pygame.mixer.pre_init(44100, -16, 2, 512) pygame.init() point_sound = load_sound("point.wav") point_sound.play()

如果使用 bgm 循环,用 pygame.mixer.music.load 代替 Sound.load。Sound 适合短音效,music 适合长背景音乐。背景音乐不要也用 Sound,否则两个 Sound 同时播放可能挤压通道,出现自动停掉一个音效的奇怪现象。参数说明:mixer 缓冲区 512 是常见平衡点,如果仍有爆音可以换成 1024。如果用的是 ogg,几乎无需额外处理;如果用的是 mp3,建议转成 44.1k 16bit 的 wav,再小也不怕。

注意点:有些资源里的音频文件是采样率 22050 或 48000,Pygame 可以自动转换,但某些版本会自动转码,产生几十毫秒延迟。如果得分音效总是在文字已经显示后才响,可以把音频文件用格式转换工具统一转成 44100 16bit 单声道 wav。文件小,载入也快。

6. 进阶:把“能跑”变成“好改好发”的六个小习惯

这个项目跑通之后,很多人会停在这里。实际上,从“能跑”到“能演示、能发出去”还有几层能快速提升的地方。

第一个习惯:把全部魔法数字收进 config.py。重力 0.5、扇动力 -10、管道间隙 150,都可以放到类的常量或者 Config 类里。这样调参数不用翻代码,教学演示时可以当场改配置文件观察手感变化。比如我把 PIPE_INTERVAL 放在 config 里之后,学生只要改一个数字就能理解密度和难度的关系。

第二个习惯:用状态机组织开始、运行、结束三个场景。最简单的是用一个 game_state 字符串:start、playing、gameover。不同状态下的事件处理、更新逻辑都不同,能避免“鸟已经死了但还能扇动”的错乱。状态切换时,记得重置鸟的位置和速度,否则重新开始后小鸟会带着上一局的残速掉出屏幕。

第三个习惯:接入 delta time。原版基于帧的速度计算在不同帧率下体验不同。用dt = clock.tick(FPS) / 1000.0,所有位移都乘以 dt 倍数,60 帧和 120 帧下速度一致。这个更新对线性移动容易,对重力公式需要同步调整,建议在代码里注释清楚“每帧 dt 约 0.016s”。我不建议一开始就加 delta time,先保证纯帧版本跑通,再改为时间驱动。

第四个习惯:用日志替代 print。python 自带的 logging 模块只要两行:

import logging logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s: %(message)s")

在得分、碰撞、管道生成处各加一行 logging.info,定位问题比 print 干净得多。print 在打包后如果没重定向,会拖慢退出速度;logging 可以自由开关级别,发布时把 level 设为 WARNING 就行。

第五个习惯:验证素材的透明边缘。图像处理软件里打开 bird.png,把背景层改成纯色,确认 Alpha 通道正常。否则 mask 碰撞永远是全部像素都算透明,collide_mask 形同虚设。有些下载素材看起来透明,实际上边缘有一圈白色噪点,需要手动去边。我一般用图像软件里的“删除白色背景”再加一个 1 像素的收缩边界。

第六个习惯:打包前先自动化冒烟测试。不写 UI 测试,至少可以开一个头,跑 30 帧然后退出。用命令行加参数--smoke-test,让游戏主循环运行 20 帧后自动退出,确认素材路径、初始化都没问题再打包。打包时用--add-data "assets:assets",把 assets 目录原样带上。如果遗漏素材,你会在别人的电脑上收获一个打开即崩溃的 exe,这种事经历一次就不会再忘。

从那以后,我每次拿到这种 pygame 小游戏源码,都会先按 config、事件、碰撞、素材四个点走一遍冒烟测试,再决定改哪一行。这套习惯帮我少走了很多“明明跑起来但一伸手就翻车”的弯路。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询