简介:基于Python的烟花模拟代码,以pygame为图形库实现动态烟花绽放,面向Python初学者、对游戏动画或粒子效果感兴趣的开发者,正好解决“如何用Python快速做一个可运行的放烟花程序”这类问题。代码采用主程序与粒子模块分离的结构,在800×600窗口中随机生成发射数量、位置与颜色,创建烟花对象并循环调用update和draw方法,完整呈现从升空、爆炸到火花消散的视觉过程;粒子运动融入重力、初速度等物理规则,轨迹更真实,也能帮助理解事件循环、对象状态更新和逐帧屏幕绘制等pygame核心概念。压缩包为zip格式,共6个文件,包含2个Python脚本(分别承担主循环和粒子逻辑)、一个pyc字节码缓存文件、一张背景图片、以及Git和macOS相关的辅助配置文件,整体大小15.46MB,结构简洁,解压后可直接运行和调整参数。已有400人学习下载,适合在此基础上继续添加音效、鼠标交互、多种烟花类型或多线程同屏绽放,是一份实用性较强的pygame动画编程入门参考。
1. 什么是 python 烟花代码:一句话说清实现原理
别把放烟花想成什么高深特效,拆开看就三件事:一颗“炮弹”从地面升空,升到某个高度后停住,然后炸开成几十上百个带颜色的小点,这些小点受重力影响往下掉、慢慢变暗直到消失。python 烟花代码做的就是把这三件事用 turtle 画笔画出来,用最基础的数学模拟物理运动,不需要任何第三方游戏库,装好 python 环境就能直接跑。适合刚学完基础语法想练手的人,也适合给小朋友做图形化编程演示——你能改的只有参数,配色、高度、粒子数量都在代码顶部集中定义,改完刷新就是新效果。
2. turtle 实现烟花的底层逻辑:画笔模式、定时器与随机数
2.1 为什么用 turtle 而不是 pygame
turtle 是 python 标准库,画线条和圆点时本质是控制一支“笔”在画布上挪动、落下、抬起来,对做烟花这种“点集动画”非常够用。pygame 自然更强,能处理图片、音效和大量粒子,但引入它会让代码依赖加重,对只想看个烟花效果的人不划算。常见做法是,在一张黑色背景的画布上画彩色圆点,每帧把上一帧的点擦掉再画新位置,利用人眼视觉暂留形成动画——这正是 pygame 做粒子系统的老套路,用 turtle 实现时只需对应三个 API:screen.tracer(0)关闭自动刷新、turtle.dot(size, color)画圆点、screen.update()手动刷新。
2.2 三个关键状态的建模
烟花粒子在每一帧里需要维护的是位置(x, y)、速度(vx, vy)和生命周期(life)。位置决定画在哪,速度决定下一帧怎么动,生命周期决定它变暗和消失的节奏。代码里每个粒子就是一个小字典:
particle = { 'x': 0, 'y': 0, # 当前位置 'vx': 0, 'vy': 0, # 当前速度(像素/帧) 'life': 50 # 剩余存活帧数,每帧减 1 }运动方程是粒子系统的核心逻辑。现实中烟花爆炸后的碎片受重力和空气阻力影响,前者让 vy 每帧匀速减小,后者让 vx、vy 各自乘一个小于 1 的系数使速度逐渐衰减。代码在每帧更新时这样计算:
particle['vx'] *= 0.98 # 空气阻力:速度每帧略有损失 particle['vy'] = particle['vy'] * 0.98 - 0.3 # 重力:vy 额外减小 particle['x'] += particle['vx'] particle['y'] += particle['vy'] particle['life'] -= 1这里有两个参数可以调:0.98是阻力系数,调大(如 0.995)粒子飘得更远,调小(如 0.95)粒子很快停住;0.3是重力加速度,调大整个烟花坠落更快、更像下雨而不是开花,调小则粒子在空中停留更久,会有点像水母散开。选择字典而不是单独列表存各个属性,是因为每个粒子的属性要一起访问和更新,字典可读性最好,后期扩展颜色、大小也方便。
2.3 颜色生成:绕开 turtle 的 RGB 限制
turtle 的dot()方法接受颜色名或 RGB 三元组,但默认模式是 0 到 1 的浮点数,如果直接传 0 到 255 的整数值,颜色会完全错乱,这是一开始最容易翻车的地方。常见做法是先把画布颜色模式切到 255:
screen.colormode(255)然后生成烟火色。如果直接用随机 RGB,得到的颜色普遍偏灰暗,和真实烟花的鲜艳感差很远;更接近真实效果的是 HSV 转 RGB:H 色相随机取 0 到 360,S、V 固定偏亮,再转换回 RGB。这样整个烟花的各个粒子色相相近但略有不同,视觉效果是“同一朵花有渐变层次”,而不是扎眼的七彩乱闪。
import colorsys import random def hsv_to_rgb_255(h, s, v): r, g, b = colorsys.hsv_to_rgb(h / 360.0, s, v) return int(r * 255), int(g * 255), int(b * 255) h = random.uniform(0, 360) # 随机主色调 color = hsv_to_rgb_255(h, 0.9, 1.0) # 高饱和、高亮度参数说明:H 是色相,0 是红、120 是绿、240 是蓝,随机取一个值作为整朵烟花的主色;S 是饱和度,0.9 保证颜色鲜艳不发灰;V 是明度,1.0 保证亮眼。每一帧还可以让粒子在生命值逐渐减少时把 V 跟着降低,实现变暗渐隐的效果。
3. 把烟花拆成粒子:一粒一画的动画循环与内存管理
3.1 粒子数组的初始化与爆炸
一次烟花爆炸本质上就是一次性生成 60 到 120 个粒子,方向均匀地铺满 360 度,但每粒的速度大小要加一点随机抖动,这样爆炸后的形状不是完美圆形,而更像真实烟花那种边缘不规则的球状扩散。初始化弹道被写成一个独立函数:
def explode(x, y): particles = [] for _ in range(80): # 粒子数量,控制爆炸密度 angle = random.uniform(0, 2 * 3.14159) # 均匀取一个角度 speed = random.uniform(2.5, 6.0) # 速度抖动,制造不规则形状 vx = math.cos(angle) * speed vy = math.sin(angle) * speed particles.append({ 'x': x, 'y': y, 'vx': vx, 'vy': vy, 'life': random.randint(40, 70) # 每粒寿命不同,先后消失 }) return particlesrandom.uniform(2.5, 6.0)是粒子速度的上下界,上界越高速粒子飞得越远,一朵烟花的爆炸半径就越大;random.randint(40, 70)让粒子不是同一帧消失,而是依次变暗,这样烟花的尾巴是逐步收掉的,更像真的。
3.2 主循环:擦旧画新是动画的基本功
烟花能“动”起来靠的是主循环不断做的三件事:更新所有粒子位置、画在新位置、把上一帧的旧粒子从画布上移除。
def draw_frame(particles): for p in particles: p['vx'] *= 0.98 p['vy'] = p['vy'] * 0.98 - 0.3 p['x'] += p['vx'] p['y'] += p['vy'] p['life'] -= 1 alpha = max(0, p['life'] / 50.0) # 用剩余寿命计算亮度比例 r, g, b = hsv_to_rgb_255(h, 0.9, alpha) turtle.dot(5, (r, g, b)) # 画粒子,直径 5 像素这里有一个所有做动画的人都会踩的坑:turtle 的 dot 不是“精灵”,它是直接盖在画布上的永久墨迹,不清除的话下一帧旧点还在,画面会堆成一条混乱的轨迹。正常做法是每帧开始时用背景色填充一个全屏矩形把上一帧盖掉,相当于擦黑板:
turtle.color('black') turtle.begin_fill() for _ in range(4): turtle.forward(screen_width) turtle.left(90) turtle.end_fill()擦屏和画帧交替进行,速度要匹配得上,否则烟花会闪。这里screen.tracer(0, 0)配合screen.update()手动刷新就能稳定在几十帧——不要让 turtle 的默认自动刷新机制接管,它会每画一个粒子就同步一次屏幕,80 个粒子一次爆炸就要刷新 80 次,动画会被拖得一顿一顿的。
3.3 内存和速度的平衡
粒子是每帧全量更新、画完就释放的,不存在长期累积的问题,但要注意的是:别用 turtle 的clear()全清画布,它会把当前帧所有绘制痕迹都清掉,包括背景,导致你再画时要重新画整个背景,开销凭空多出好几倍。正确做法是始终维护一张黑色全屏底作为背景垫底,粒子死亡后直接丢弃其字典对象引用,让 python 的垃圾回收自然处理。如果发现某个烟花代码跑久了变慢,多半不是内存泄漏,而是主循环里频繁创建临时对象、或每帧刷新画布的次数过多,优化方向是减少循环里无意义的计算,把颜色模式、笔宽度等不变的状态提前设定好。
4. 单击放一发:点击事件、坐标映射与多窗口适配
4.1 点击坐标转画布坐标的陷阱
turtle 的onscreenclick()回调会给两个参数,分别是点击处的 x、y 坐标,但坐标系方向要注意:turtle 默认以画布中心为原点,向右是 x 正方向,向上是 y 正方向,这和很多 GUI 框架的“左上角为原点”相反。点击画布上半部分得到正 y、下半部分得到负 y,直接拿这个坐标给爆炸函数使用是没问题的,因为它已经是 turtle 的逻辑坐标了。真正容易出错的是设置画布大小时用setup(width, height),传入的宽高是按像素算的,而画布的逻辑坐标范围默认是 -400 到 400 或类似值,两者不是一回事。常见做法是:
screen = turtle.Screen() screen.setup(800, 600) # 窗口像素尺寸 screen.tracer(0, 0) screen.colormode(255) def launch(x, y): particles.extend(explode(x, y)) # 直接使用回调坐标 screen.update() screen.onscreenclick(launch)4.2 发射逻辑:炮弹升空后再爆炸
点击之后立刻爆炸虽然能出效果,但真实烟花先升空再炸开的过程才是灵魂。更完整的做法是点击时生成一个炮弹粒子,让它带着向上的初速度飞,升到某个高度后再调用爆炸函数:
def launch(x, y): rockets.append({ 'x': x, 'y': y, 'vx': 0, 'vy': random.uniform(8, 12), # 上升速度 'target_y': y + random.randint(120, 260) # 目标爆炸高度 }) screen.update() def update_rockets(): for r in rockets[:]: r['vy'] -= 0.25 # 上升过程中减速 r['x'] += r['vx'] r['y'] += r['vy'] if r['vy'] <= 0 or r['y'] >= r['target_y']: particles.extend(explode(r['x'], r['y'])) rockets.remove(r)这个设计里关键参数是target_y,决定了爆炸高度,取 120 到 260 之间的随机值会让每次爆炸高低错落,天空层次更丰满;vy从 8 到 12 随机取,配合重力逐帧减速,炮弹就能在最高点附近自然“悬停”再炸开——实际上是 vy 减到 0 时触发爆炸,效果和真烟花引信燃到顶点再炸完全一致。遍历rockets[:]而不是rockets,是因为循环中要删除当前元素,直接遍历原列表会跳过元素或报错。
4.3 一个常见问题:循环里更新动画要用定时器而不是 for 死循环
写动画代码时新手最容易写一个while True来无限循环刷新,这在 turtle 里会直接卡死事件响应,点击窗口没反应,因为onscreenclick()事件需要主循环有空闲才能被处理。正确做法是screen.ontimer(loop, 30)设置每 30 毫秒调用一次主循环函数,30 毫秒大约是 33 帧每秒,人眼看着是流畅的:
def main_loop(): update_rockets() draw_frame(particles) screen.update() screen.ontimer(main_loop, 30) main_loop() screen.mainloop()ontimer是 turtle 提供的定时器接口,回调自己(main_loop)再注册自己,就相当于一个循环,但它不会抢占事件循环,点击和窗口缩放都能正常响应。帧间隔 30 毫秒是通用值,若电脑性能偏弱,可以改成 40 或 50 毫秒牺牲流畅度换稳定;反之性能好的机器 16 到 20 毫秒也能跑,但要注意 Flash 的花哨感不是靠帧率堆出来的,30 毫秒已经足够。
5. 避坑:click 没反应、粒子堆成一团、动画越来越卡的三个常见问题
做烟花代码最容易翻车的不是原理,而是 turtle 的一些固有行为不搞清楚,看效果就像见了鬼。以下三条是我拆这个项目时实际踩过的坑,按“现象 → 原因 → 解决”写清楚。
现象一:点击屏幕后毫无反应,控制台也没有任何报错。
原因:常见于两处。一是screen.onscreenclick(launch)注册的launch函数写在screen.mainloop()之后——mainloop 一旦开启就会阻塞,注册代码永远执行不到;二是没有把点击函数绑定到 screen 对象上,而是绑定到了某个 turtle 实例上,turtle 实例是有自己独立坐标系的,绑定错对象事件根本不会触发。另外,在tracer(0)关闭自动刷新的情况下,点击回调里如果不手动调用screen.update(),画面不刷新,看起来就像没反应。
解决:把onscreenclick注册放在mainloop之前,统一用全局的screen对象绑定事件;回调函数里完成粒子追加后立刻screen.update()强制刷新一帧。推荐把注册代码和主循环代码写在一起,用注释分隔“事件注册区”和“动画循环区”。
现象二:烟花爆炸后粒子全部堆在一个地方不动,或者东一个西一个乱飞。
原因:粒子的运动逻辑里,如果对每个粒子都重新计算了角度、速度,但画点时却错误地把所有粒子坐标画成了同一个 x、y——比如误用了炮弹自身的坐标;或者代码里对particles列表做了切片复制,新粒子追加到了副本而不是原列表,导致主循环更新的是空列表。
解决:粒子数组只维护一份全局引用,在explode()里return新列表、调用处用particles.extend()追加,不要用particles.append(explode(...))这样把整个列表塞成嵌套结构。画点时逐粒子从自身 dict 里取坐标,不准引用火箭的坐标。日志排查时,可以在每帧打印len(particles)和第一个粒子的坐标,看到数字不涨或坐标不变,就能立刻定位。
现象三:运行几分钟后动画越来越卡,最后甚至像幻灯片。
原因:最常见的是把已死亡的粒子留在了particles列表里,每帧都在遍历和绘制大量life为负、坐标为负无穷的无效粒子;另一个隐蔽原因是每帧刷新画布时用了全屏clear(),导致 turtle 底层画布对象不断重建,内存膨胀。turtle 本身不是为高性能粒子系统设计的,粒子数量一旦上千,刷新开销会线性增长。
解决:主循环每帧开头先做一次过滤,只保留life > 0的粒子;同时不要用clear(),用“画背景色矩形覆盖”的方式擦旧帧。过滤代码:
particles[:] = [p for p in particles if p['life'] > 0] rockets[:] = [r for r in rockets if r['vy'] > -1]注意particles[:] = ...是原地修改列表,而particles = ...会创建一个新列表并把全局引用替换掉——如果主循环之外的代码还持有旧引用,就会更新不到新列表,所以一律用切片赋值原地过滤。
6. 进阶玩法:自定义发射方向、配色方案与时长控制
基础版能跑通之后,再往深走就是三件事:让烟花朝指定方向炸、自定义每朵花的颜色主题、控制整场烟花的持续时长和节奏。这三个需求对应三个代码级的调整点。
定向爆炸的实现思路其实不复杂,给explode()加两个参数:base_angle(主方向)和spread(扩散范围),粒子角度在base_angle - spread到base_angle + spread之间取,速度还是随机,这样就会朝特定方向炸开。模拟一个“转角烟花”:点击屏幕右侧则往左上方炸,点击左侧则往右上方炸:
def explode(x, y, base_angle=0, spread=360): particles = [] for _ in range(80): angle = base_angle + random.uniform(-spread / 2, spread / 2) speed = random.uniform(2.5, 6.0) particles.append({...})配色方案可以预设几个主题,类似“落日红橙”“深海蓝紫”“原野绿黄”,每个主题是一个 (h_base, h_range) 元组,色相在范围内取随机值:
themes = { 'sunset': (10, 40), # 红橙色系 'ocean': (200, 260), # 蓝紫色系 'forest': (80, 160), # 绿黄色系 }点击时随机选一个主题,传给explode()里的 h 计算位置,这样每朵花都有自己的主色调,不会全场都是同一个色。时长控制则是用一个全局计数器frame_count,满多少帧自动停止发射新的火箭,已爆炸的粒子仍然继续画完,这样末尾烟花自然散去,而不是瞬间消失:
def main_loop(): global frame_count frame_count += 1 if frame_count < total_frames and random.random() < 0.2: launch(random.uniform(-300, 300), random.uniform(-100, 100)) # 已有的更新和绘制逻辑随机发射间隔 0.2 的概率每帧判定一次,平均每 5 帧发射一发,约 0.15 秒一次,不会显得太密集也不会太冷清。把total_frames设成 1500(约 45 秒),播放完自动只剩零星粒子的余韵。从那以后我每次做这类动画 demo,都会把发射频率、颜色主题、爆炸方向三件事全部收敛成顶部参数,跑一遍再把参数给同伴让他们自己调颜色——这比教他们改代码省心太多。希望帮到你。
本文还有配套的精品资源,点击获取