简介:这份资源面向对Python图形编程感兴趣的初学者与爱好者,围绕Pygame库讲解如何从零搭建一个带音效的新年烟花动画程序,帮助读者理解粒子系统、图形界面渲染与事件循环等基础概念,并可作为节日庆祝的小项目实践。资源包内共1个docx文档,压缩后约16KB,文档以完整代码示例为主线,配合逐段讲解,涵盖环境准备、Particle粒子类与Firework发射器类的设计、烟花升空与爆炸逻辑、颜色随机配置以及背景音乐加载播放等内容,读者可据此调整音乐路径与粒子数量、发射频率等参数,快速运行并改造出自己的烟花展示效果。目前已有140人学习下载,适合希望掌握Pygame基本用法、提升图形界面应用处理能力的开发者参考。
1. 用 Python 和 Pygame 手搓一场新年烟花:从粒子到音效的完整落地
跨年夜想给家里人整点不一样的,又不想开那些花里胡哨的在线烟花页面,我干脆用 Python 和 Pygame 自己写了一个。跑起来之后屏幕上会随机升空、炸开、拖尾、熄灭,背景音乐一响,客厅那台老笔记本居然也撑住了 60 帧。这个项目不依赖任何游戏引擎,纯 Pygame 加标准库的 random 和 math,一个文件就能跑,特别适合想入门图形界面编程、又不想啃厚重框架的开发者。它把粒子系统、动画循环、音效播放、事件响应这几件事串成了一条完整链路,代码量不大,但该有的骨架都在。下面我就按自己拆项目的习惯,把这份源码从环境准备到二次开发完整讲一遍。
2. 环境准备与项目骨架:一个文件跑起来的图形程序
2.1 依赖安装与目录约定
这个项目对环境的唯一硬性要求就是 Pygame。它底层封装了 SDL,负责窗口创建、绘图、事件和音频,Python 这边只做逻辑调度。安装命令很直接:
pip install pygame装完之后建议顺手验证一下版本和音频子系统是否正常,因为烟花项目里音乐播放是最容易翻车的一环:
python -c "import pygame; print(pygame.version.ver)"目录结构上,我一般会保持极简,一个入口文件加一个资源目录就够了:
fireworks/ ├── main.py # 全部逻辑,粒子类 + 发射器类 + 主循环 └── assets/ └── bgm.mp3 # 背景音乐,路径在代码里替换提示:音乐文件不要用中文名,也不要放在带空格的路径下,Pygame 的 mixer 在某些系统上对这类路径解析会出问题。
2.2 初始化与窗口配置
Pygame 的程序入口有一套固定仪式:先pygame.init(),再创建显示表面,最后进主循环。窗口尺寸这里定的是 800×600,这个比例在大多数屏幕上不会触发缩放,粒子坐标计算也简单。
import pygame import random import math pygame.init() # 窗口尺寸,800x600 是粒子数量和性能的平衡点 screen_width, screen_height = 800, 600 screen = pygame.display.set_mode((screen_width, screen_height)) pygame.display.set_caption("New Year Fireworks") # 加载背景音乐,-1 表示无限循环 pygame.mixer.music.load("assets/bgm.mp3") pygame.mixer.music.play(-1)这里几个参数值得说清楚:set_mode返回的screen就是后面所有draw操作的目标表面;mixer.music.play(-1)里的-1是循环次数约定,传 0 是播一次,传正数是播几次。如果你发现程序启动后没声音,先确认pygame.mixer.init()是否被pygame.init()自动带起来了,个别精简环境需要手动调一次。
颜色常量也在这里定义,白色给上升粒子用,黑色给背景清屏用:
WHITE = (255, 255, 255) BLACK = (0, 0, 0)背景每帧都要用screen.fill(BLACK)重刷,否则上一帧的粒子会残留在屏幕上形成拖影。有人故意不刷背景来做拖尾效果,那是另一种玩法,但默认版本必须刷。
3. 粒子系统拆解:一个 Particle 类怎么撑起整场爆炸
3.1 粒子属性设计与物理模拟
烟花好不好看,九成取决于粒子。这个项目里Particle类承担了单个光点的全部状态:位置、颜色、大小、生命周期、运动角度和速度。构造函数里用random给每个粒子一点随机扰动,这样爆炸出来才不是一坨整齐的圆。
class Particle: def __init__(self, x, y, color): self.x = x self.y = y self.color = color self.size = random.randint(4, 7) # 粒子半径 self.life = 25 # 生命周期,单位是帧 self.angle = random.uniform(0, 2 * math.pi) # 360 度随机方向 self.speed = random.uniform(2, 5) # 初速度 def move(self): # 按角度分解速度到 x/y 轴 self.x += self.speed * math.cos(self.angle) self.y -= self.speed * math.sin(self.angle) self.life -= 1 self.speed *= 0.95 # 每帧减速,模拟空气阻力和重力 def draw(self): if self.life > 0: pygame.draw.circle(screen, self.color, (int(self.x), int(self.y)), self.size)逻辑上分三步看。第一步是方向分解,math.cos和math.sin把标量速度投影到两个轴上,这是所有 2D 粒子系统的通用做法。第二步是life递减,它同时充当「还能活几帧」和「什么时候该被移除」两个角色。第三步是speed *= 0.95,这个系数是手感的关键——调大到 0.9 粒子会很快停住像凝固,调小到 0.99 又会飞得太远,0.95 是我试下来比较像真实烟花扩散的值。
draw里的int()转换不能省,Pygame 的绘图接口只接受整数坐标,传浮点会直接报类型错误。这是新手最常踩的一个坑。
3.2 发射器逻辑:从升空到爆炸的状态切换
Firework类管理一组粒子,它其实是个状态机:先有一个白色粒子往上飞,飞到一定高度后删掉它,原地生成 100 个彩色粒子炸开。
class Firework: def __init__(self): self.particles = [] self.explosion_color = random.choice([ (255, 0, 0), (0, 255, 0), (0, 0, 255) ]) def launch(self): # 从屏幕底部随机位置发射一个上升粒子 self.particles.append( Particle(random.randint(0, screen_width), screen_height, WHITE) ) def explode(self): # 以当前上升粒子位置为中心,炸出 100 个彩色粒子 for _ in range(100): self.particles.append( Particle(self.particles[0].x, self.particles[0].y, self.explosion_color) ) del self.particles[0] # 移除上升粒子 def update(self): if not self.particles: return False for particle in self.particles[:]: particle.move() particle.draw() if particle.life <= 0: self.particles.remove(particle) # 只剩上升粒子且飞到 100 高度以下时触发爆炸 if len(self.particles) == 1 and self.particles[0].y < 100: self.explode() return Trueupdate里有两个细节值得单独拎出来。一是遍历时用了self.particles[:]切片拷贝,因为循环体内会remove元素,直接遍历原列表会漏掉元素甚至抛异常,这是 Python 里处理「边遍历边删除」的标准写法。二是爆炸触发条件len == 1 and y < 100,y < 100是因为 Pygame 的 y 轴向下为正,数值越小越靠上,所以粒子飞到接近顶部时才炸。这个阈值你可以按屏幕高度比例调,比如改成screen_height * 0.15会更自适应。
爆炸颜色只从红绿蓝三原色里选,是为了保证在黑色背景上足够醒目。想更花哨可以扩成一组十六进制色值,但要注意别选太暗的颜色,否则粒子在暗背景上几乎看不见。
4. 主循环与事件驱动:帧率、随机发射和键盘交互
4.1 事件处理与键盘触发
主循环是 Pygame 程序的心脏,它每帧做三件事:处理事件、更新状态、刷新画面。事件部分负责响应关闭窗口和空格键手动放烟花。
fireworks = [Firework()] clock = pygame.time.Clock() running = True while running: for event in pygame.event.get(): if event.type == pygame.QUIT: running = False elif event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE: fireworks.append(Firework())pygame.event.get()会一次性取走当前帧所有事件,不取的话事件队列会堆积,窗口就会卡死无响应。空格键追加一个新的Firework实例,注意这里只是创建对象,真正发射是在后面统一调launch(),这样能保证所有烟花走同一套更新逻辑。
4.2 帧率控制与随机生成节奏
帧率用clock.tick(60)锁在 60 帧,这是保证动画流畅又不吃满 CPU 的关键。不锁帧的话循环会跑到几百帧,粒子速度参数全部失真。
screen.fill(BLACK) for firework in fireworks[:]: if not firework.update(): fireworks.remove(firework) # 每帧 5% 概率自动发射一朵新烟花 if random.random() < 0.05: fireworks.append(Firework()) fireworks[-1].launch() pygame.display.flip() clock.tick(60) pygame.quit()random.random() < 0.05这个概率控制的是自动发射频率。在 60 帧下,平均每秒会触发约 3 次(60 × 0.05),实际观感是屏幕上同时有两三朵烟花在炸。嫌太密就降到 0.02,嫌太冷清就提到 0.1。fireworks[:]同样是切片拷贝,因为循环里可能移除已结束的烟花对象。
pygame.display.flip()负责把这一帧画好的内容真正推到屏幕上,少了它你只会看到一个黑窗口。clock.tick(60)放在循环末尾,它同时起到延时和返回帧间隔两个作用。
5. 避坑与常见问题排查
5.1 音乐不播放或报错
现象:程序能跑,烟花正常,但一点声音都没有,或者启动时直接抛pygame.error。原因通常是音频文件路径不对,或者系统音频设备没被 mixer 正确初始化。解决:先用绝对路径确认文件存在,再在pygame.init()后加一句pygame.mixer.init()显式初始化音频子系统。如果还是不行,换一个采样率标准的 mp3 文件,某些高码率文件 mixer 解不了。
5.2 粒子拖影或画面闪烁
现象:烟花炸开后屏幕上留着一道道残影,或者整个画面在抖。原因基本是screen.fill(BLACK)漏写或写在了错误的位置。解决:确认清屏语句在每帧绘制粒子之前执行,且只执行一次。如果用了双缓冲,flip()也要确保每帧调用。
5.3 帧率忽高忽低导致爆炸速度不一致
现象:在性能不同的机器上,烟花扩散速度明显不一样。原因是没有锁帧,循环速度跟着 CPU 走,而粒子的speed是按帧递减的。解决:坚持用clock.tick(60),并且所有跟时间相关的参数都按帧来设计。如果要做跨设备一致,可以把speed *= 0.95改成基于delta time的计算,但对这个项目来说锁帧已经够用。
5.4 粒子数量一多就卡顿
现象:连续放十几朵烟花后帧率掉到 20 以下。原因:每朵烟花爆炸产生 100 个粒子,十几个就是上千个draw.circle调用,Pygame 的逐圆绘制在软件渲染下开销不小。解决:把爆炸粒子数从 100 降到 50,或者给Firework加一个存活上限,超过就移除最早的。另一个思路是把粒子画成小方块而不是圆,draw.rect比draw.circle快不少。
5.5 窗口关闭后进程不退出
现象:点了关闭按钮,窗口没了但终端还卡着。原因:pygame.quit()没被调用,或者主循环的running标志没被正确置为False。解决:确认QUIT事件里把running设成False,并且循环结束后执行pygame.quit()。用 IDE 强制停止不算,要保证正常退出路径干净。
6. 二次开发:把烟花改成你自己的节日特效
这个项目的扩展空间比它本身大得多,我拿它改过好几次,下面说几个改动成本低但效果明显的方向。
第一个是加拖尾。现在粒子是实心圆,每帧重绘。你可以在Particle.draw里改成画一条从当前位置到上一帧位置的短线,用pygame.draw.line,颜色带一点透明度衰减。这样粒子飞过会留下光轨,观感立刻上一个档次。代价是每帧多一次线段绘制,粒子数要相应降一点。
第二个是让爆炸形状可控。现在angle是uniform(0, 2π)全向随机,你可以改成只在某个扇形范围内取角度,就能做出心形、环形、甚至字母形状的爆炸。比如环形爆炸就让所有粒子角度均匀分布,速度一致,炸出来就是一个标准圆环。
第三个是接入真实音频事件。现在背景音乐是循环播放的,跟烟花爆炸没有联动。可以在explode()里调pygame.mixer.Sound播放一个短促的爆炸音效,每个烟花实例持有一个 Sound 对象。注意 Sound 对象要提前加载好,不要在爆炸瞬间去读文件,否则会卡帧。
第四个是参数外置。把粒子数、生命周期、减速系数、发射概率这些全部提到文件顶部的配置字典里,改效果不用翻代码。我一般会这么组织:
CONFIG = { "particle_count": 100, "particle_life": 25, "speed_decay": 0.95, "launch_probability": 0.05, "fps": 60, }然后Particle和Firework里引用CONFIG["particle_count"]这类键。这样调参就是改一个数字的事,不用在类定义里到处找。
验证改动是否生效,我有个笨但可靠的习惯:先把launch_probability调到 1.0,让每帧都发射,这样能快速看到大量烟花同时炸的效果,方便观察粒子和性能瓶颈。确认没问题再调回正常值。另外每次改完粒子物理参数,都强制跑一遍 60 秒,看帧率会不会随时间下降——有些内存泄漏(比如粒子没被正确移除)要跑一会儿才暴露。
从那以后我每次动粒子系统的参数,都强制走一遍「高密度压测 → 观察帧率曲线 → 再调回正常值」这个流程,省得在节日当天当着全家人的面翻车。希望这个项目能帮到你,跨年那晚屏幕上亮起来的时候,还是挺有成就感的。
本文还有配套的精品资源,点击获取