ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Python+Pygame零基础实现新年烟花粒子动画

Python+Pygame零基础实现新年烟花粒子动画

简介:这是一份面向Python初学者与图形编程爱好者,尤其是具备基础语法知识、希望掌握图形编程技巧的学习者的Pygame烟花动画实践文档,围绕新年烟花展示场景,涵盖环境准备、依赖安装、粒子系统建立、烟花发射与爆炸逻辑、颜色配置、事件循环、显示刷新及背景音乐播放等核心环节,帮助读者理解Pygame图形界面应用的基本开发流程。压缩包仅含1个docx文件,大小约16KB,便于直接查阅与按步骤实践。已有143人学习,说明其对入门练习具有一定参考价值。文档提供可运行代码示例,细化Particle粒子类与Firework发射器类的职责,展示随机颜色、粒子减速与生命周期控制,并提示按实际环境调整音乐文件路径、粒子数量、发射频率与按键触发方式,读者可据此搭建自己的烟花程序,并在此基础上做节日效果扩展。

1. 先弄明白一件事:新年烟花动画,本质是一堆会动的小圆点

想给新年做点不一样的动静,很多人第一反应是翻模板、刷教程视频,半小时过去,一个文件都没建。其实用 Python 配合 Pygame 写一个新年烟花动画,门槛比想象中低得多:屏幕上那些炸开的光点,拆到底就是一堆会移动的小圆点。每一颗小圆点都有自己的位置、速度、颜色和寿命,程序每秒钟把它们擦掉重画六十次,看起来就是烟花。这份资源给的是一份可以直接运行的完整代码,包含粒子系统、发射与爆炸逻辑、颜色配置和背景音乐。它适合刚学完 Python 基础语法、还没写过任何图形程序的人,也适合想在节日里给家人朋友看一眼「这是我自己写的」的人。下面按「先懂概念、再装环境、最后跑通第一个例子」的顺序走,不需要提前懂图形学,也不需要数学很好。

2. 为什么值得学:Pygame 是零基础最容易看到成果的一条路

2.1 屏幕上会动的东西,比黑框里的一行字更能留住人

很多人学 Python 的第一段路是这样的:装好环境,跟着敲 print,算个九九乘法表,写个猜数字小游戏,然后就没有然后了。不是学不会,是看不到东西。黑框里输出一行字,跟你在纸上写一行字,对大脑的刺激差不多,你不会觉得自己「做出了什么玩意儿」。

图形程序完全不一样。你改一个数字,屏幕上那颗粒子就飞得快一点;你把减速系数从 0.95 改成 0.9,烟花散开的范围立刻变大、变散。这种「改一下、立刻看到」的循环,是新手能坚持下去的最重要原因。我见过不少刚起步的人,卡在函数和类上很久,但只要让他写点会动的东西,他能自己坐着调两个小时参数,还嫌时间不够。

烟花这个项目刚好踩中几个新手友好的点:视觉反馈强烈、逻辑分层清楚、可以无限加料。今天让一颗圆点飞起来,明天让它炸成一百颗,后天加音乐加倒计时。每一步都有肉眼可见的进展,不会出现「我学了两周但不知道自己在干嘛」的情况。对比一下两种练习方式,差别很直观:

练习方式你看到的结果改错后的反馈坚持一周的概率
控制台算数、打印图形一片文字滚过去要回头翻上面的输出找区别偏低
图形动画屏幕上的东西变了一眼就看出来明显更高

这张表不是贬低基础练习,基础语法该练还得练。它想说的是:当你已经有了一点语法底子,最该做的事是找一个能「看见」的项目把语法用起来,而不是继续刷选择题。

2.2 Pygame 替你干掉的杂活:窗口、声音、按键、帧率

不用 Pygame 能不能画?能,但你要自己去调操作系统的图形接口,一行一行往显存里塞像素。那是个黑洞,新手掉进去三天出不来。

Pygame 的位置很好理解:它是一间已经装修好、水电都通了的铺面,你只需要把货摆进去。具体来说,它替你干了四件杂活:

第一件是开窗口。pygame.display.set_mode((800, 600))这一句,等于向系统要了一块 800 像素宽、600 像素高的画布,之后你画的所有东西都落在这块布上。

第二件是画基本图形。画圆用pygame.draw.circle,画线、画矩形都有现成的函数,你不用管抗锯齿、不用管像素格式。

第三件是收键盘和鼠标事件。用户点了关闭按钮、按了空格、划了鼠标,Pygame 会把这些打包成事件放进一个队列,你只要在一个循环里挨个取出来判断就行。

第四件是控制节奏。pygame.time.Clock()加上clock.tick(60),意思是「这一帧画完,如果还没到 1/60 秒,就睡一会儿」。没有它,你的动画在性能好的机器上快得像快进,在旧机器上又卡成幻灯片。

音乐同理,pygame.mixer.music几行就能加载一个音频文件并循环播放,不需要你自己去解码波形。

2.3 整套程序只有四个零件,认清它们就不慌了

新手看完整代码最容易犯的毛病,是从第一行往下读,读到第一百行就晕了,因为不知道哪些行属于同一块。其实这份烟花程序只有四个零件,认清楚了,八百行也能拆成四堆看:

零件干什么大致对应代码
屏幕与时钟开窗、定帧率、准备颜色常量开头十几行初始化
Particle(粒子)一颗小圆点:在哪、往哪飞、多快、活多久一个类,约 20 行
Firework(烟花)管一批粒子:什么时候发射、什么时候炸一个类,约 25 行
主循环每帧收事件、更新、重画、清场一个 while,约 25 行

为什么是这四个?因为动画的本质就是一个循环:擦掉上一帧、算出这一帧每个东西的新位置、画上去、等一小会儿、再来一次。Particle 负责「算位置」和「画自己」,Firework 负责「管理一批粒子」,主循环负责「什么时候算、什么时候画」。屏幕和时钟是舞台和节拍器,不参与逻辑。

想明白这一层,你再看任何 2D 小游戏或者动画的源码,基本都能套进这个框架:有主角对象、有管理对象、有一个每帧跑一次的循环。后面几章就是把这四个零件一个个装起来。

3. 动手前的准备:把环境装到能弹出一个黑窗口为止

3.1 装 Python:安装界面上只有一个勾必须打

去 Python 官方网站下载安装包,选 Python 3 的稳定版本就行,不用纠结具体小版本号,这份代码用到的语法在 Python 3 各个版本上都通用。

安装过程中会有一步特别关键:Windows 上安装向导底部有一个「Add Python to PATH」的勾选框,一定要勾上。这个勾的意思是「把 Python 加到系统能找到的路径列表里」,勾了之后你在命令行敲python就能直接调用;不勾,你敲python系统会回你一句「不是内部或外部命令」,很多新手就卡在这一步,以为自己装失败了,其实只是系统找不到它。

装完打开命令行(Windows 是 cmd 或 PowerShell,macOS 是终端),敲:

# 看看系统认不认 python 这个命令 python --version

正常会回一行版本号,类似Python 3.x.x。如果没反应或者报错,先试py --version(Windows 专属的启动器),再不行就试python3 --version(macOS 和部分 Linux 上默认叫这个名字)。三个里总有一个能通,记住能通的那个,后面所有命令都换成它。

提示:如果你的电脑上装过好几个 Python 版本,最好确认一下现在这个python指的是哪一个,后面装库会装到它对应的目录里,装错地方就会出现「明明装了却说找不到」的玄学问题。

3.2 装 Pygame:一行 pip 命令,和它常见的两种失败

Pygame 不是 Python 自带的库,要单独装。用 pip 这个包管理工具,一行命令:

# 用 python 自带的 pip 安装 Pygame python -m pip install pygame

这里我故意写成python -m pip而不是直接写pip,原因很实在:很多机器上pip这个命令指向的是另一个 Python 环境,而python -m pip的意思是「用我刚确认过的这个 Python,去跑它自带的 pip 模块」,两者一定对得上。这是一种省事的写法,能躲开一大半「装完了但导不进来」的坑。

跑完会看到下载进度条,最后出现Successfully installed pygame-x.x.x就算成功。常见的两种失败:

一是提示pip 不是内部或外部命令。说明 pip 没在 PATH 里,用上面python -m pip的写法基本能绕过。

二是下载卡住或者超时。这种情况下换一个软件源(很多公司和学校都提供镜像站),或者把命令多跑一次,通常就过去了。

装完立刻验证一下,不要等到写完一百行代码才发现库没装上:

# 进 Python 交互环境,试着导入一次 python -c "import pygame; print(pygame.__version__)"

能打印出版本号,说明库已经就位。

3.3 二十行代码验证:先让一个空窗口站住

在写烟花之前,先让一个纯黑的空窗口站住。这一步的意义是:把「窗口能不能开」和「动画逻辑对不对」这两个问题分开排查。窗口开不出来是环境问题,窗口能开但烟花不飞是代码问题,混在一起查会很痛苦。

新建一个文件,随便叫什么,比如test_window.py,内容如下:

import pygame # 1. 初始化 Pygame 的所有子系统(显示、声音、事件等) pygame.init() # 2. 向系统申请一块 800x600 的画布 screen = pygame.display.set_mode((800, 600)) pygame.display.set_caption("测试窗口") running = True while running: # 3. 把这一帧发生的所有事件取出来处理 for event in pygame.event.get(): if event.type == pygame.QUIT: # 用户点了右上角的叉 running = False # 4. 整块画布涂黑,相当于擦黑板 screen.fill((0, 0, 0)) # 5. 把画好的内容真正显示到屏幕上 pygame.display.flip() # 6. 退出前释放资源 pygame.quit()

跑起来应该看到一块纯黑的窗口,标题写着「测试窗口」,点右上角的叉能正常关掉。

几个参数值得说清楚。set_mode里的(800, 600)是宽和高,单位是像素,你可以直接改成(1000, 700)试试,窗口会变大,但要记得后面所有跟坐标有关的数字都基于这两个值,别写死。screen.fill((0, 0, 0))里的三元组是 RGB 颜色,三个数分别是红、绿、蓝,取值 0 到 255,全 0 就是黑。pygame.display.flip()是「翻转缓冲区」的意思:你前面画的都画在一块看不见的后台画布上,flip 一下才把它推到屏幕,所以这行必须放在循环的最后。

还有一个隐藏知识点:这个 while 循环每秒钟会转几百上千圈,如果没有后面的帧率控制,CPU 会被吃满。烟花代码里我们会加一个时钟把速度压到 60 帧。

3.4 文件夹怎么摆:代码、音乐、运行目录三者的关系

这一步新手极容易翻车,先说清楚。

建议建一个干净的文件夹,比如叫fireworks,里面放两样东西:一个是代码文件fireworks.py,一个是音乐文件,比如bgm.mp3。就这两个,别塞别的。

然后在命令行里cd到这个文件夹,再运行python fireworks.py。为什么强调这个?因为代码里写pygame.mixer.music.load("bgm.mp3")这种相对路径时,程序找的是「你当前所在的目录」,不是「这个 .py 文件所在的目录」。这两个目录经常不是同一个:如果你在桌面上敲python fireworks/fireworks.py,程序的工作目录是桌面,它会去桌面上找bgm.mp3,当然找不到。

音乐文件本身也有讲究:格式上 mp3、ogg、wav 都能被 Pygame 读取,但 mp3 在部分环境下兼容性一般,如果加载报错,先把音乐转成 ogg 或者 wav 再试。文件不要太大,几十秒的循环片段就够,一直循环播放听不出接缝。

注意:如果暂时没有合适的音乐文件,可以先把加载音乐的那两行注释掉,让画面先跑起来。音效是最容易拖慢第一步的东西,不要让它挡住你看到第一朵烟花。

4. 第一个能跑的例子:让粒子飞起来,再让它炸开

4.1 主循环骨架:动画为什么离不开一个 while

动画这个词听着高级,原理土得掉渣:一张图,换一点点,再换一点点,快到每秒六十次,眼睛就被骗了。所以任何动画程序的骨架都是同一个循环。

先写骨架的第一层,也就是初始化部分:

import random import math import pygame # 定义两个常用颜色,后面到处要用 BLACK = (0, 0, 0) WHITE = (255, 255, 255) pygame.init() # 屏幕尺寸单独存成变量,后面生成粒子时要用到宽度和高度 screen_width, screen_height = 800, 600 screen = pygame.display.set_mode((screen_width, screen_height)) pygame.display.set_caption("新年烟花") # 时钟对象,用来把循环速度压在 60 帧 clock = pygame.time.Clock()

random用来生成随机数(粒子的方向、速度、大小都靠它),math用来算三角函数(把「角度 + 速度」换算成横竖两个方向上的位移),pygame是主角。

screen这里是个全局变量,这一点很重要——后面Particle.draw里会直接用它,而不再作为参数传进去。新手写类的时候经常纠结「要不要把 screen 传进去」,小项目里直接用全局变量更省事,代码也短。

4.2 Particle 类:六个属性决定一颗粒子的命运

一颗粒子需要记住六件事:现在在哪(x、y)、长什么样(color、size)、往哪个方向飞(angle)、飞多快(speed)、还能活多久(life)。把这六个写进__init__:

class Particle: def __init__(self, x, y, color): self.x = x self.y = y self.color = color self.size = random.randint(4, 7) # 半径 4 到 7 像素 self.life = 25 # 还能活 25 帧 self.angle = random.uniform(0, 2 * math.pi) # 0 到 360 度,用弧度表示 self.speed = random.uniform(2, 5) # 每帧移动 2 到 5 像素

构造函数收三个参数:出生坐标和颜色。剩下的四个属性自己随机生成,这就是「一次爆炸看起来自然」的秘密——一百颗粒子如果速度完全一样、方向完全一样,炸出来就是一个整齐的圆环,很假;每个都随机一点,才有那种炸开的感觉。

angle用弧度不用角度,因为 Python 的math.cos和math.sin收的都是弧度。一圈是 2π 弧度,所以random.uniform(0, 2 * math.pi)就是「随便挑一个方向」。这个换算新手第一次看会别扭,记住一句话就行:角度除以 180 再乘 π 就是弧度,反过来乘。

size是圆的半径,画出来直径是 8 到 14 像素,在 800×600 的屏幕上大概是一个小米粒的大小。觉得太细可以改成random.randint(2, 4),画面会细腻很多,后面参数表里还会说。

4.3 move 与 draw:每一帧只挪一点点

粒子有了属性,还得会动、会画。这两个方法加起来不到十行:

def move(self): # 把「速度 + 方向」拆成横竖两个方向的位移 self.x += self.speed * math.cos(self.angle) self.y -= self.speed * math.sin(self.angle) # 屏幕 y 轴朝下,所以用减 self.life -= 1 # 活一帧少一帧 self.speed *= 0.95 # 每帧减速 5%,模拟重力拖拽 def draw(self): # 还活着才画,死了就不占屏幕了 if self.life > 0: pygame.draw.circle(screen, self.color, (int(self.x), int(self.y)), self.size)

move里那两行是整份代码的核心。一个粒子朝着某个方向飞,横向走了多少、纵向走了多少,用余弦和正弦就能算出来。这里有个屏幕坐标系的问题必须讲清楚:数学课上 y 轴朝上,屏幕上的 y 轴是朝下的,原点在左上角。所以「往上飞」在代码里是y变小,用减号。第一次写图形程序的人十有八九在这里翻车,画出来发现东西全往下掉,不是逻辑错了,是坐标系没转过来。

self.speed *= 0.95这行看着不起眼,作用很大。它让粒子每帧慢一点点,配上方向不变,飞行轨迹就成了一条微微弯曲的抛物线,像被重力拽住。如果去掉这行,粒子会匀速直线飞出去,看起来像激光不像烟花。这个 0.95 就是「重力手感」的旋钮。

draw里的int()不能省。pygame.draw.circle要求坐标是整数,而我们的x、y是浮点数(因为有小数位移),不转换会直接报类型错误。

4.4 Firework 类:一个上升粒子指挥一次爆炸

有了粒子,接下来管一批粒子。Firework的职责很单纯:先放一个白色粒子往上飞,等它飞到差不多高了,把它删掉,在原地生成一百个彩色粒子。

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] # 上升弹完成使命,删掉

explosion_color只挑红绿蓝三种,因为这三个颜色在纯黑背景上最扎眼。想更热闹可以加(255, 255, 0)黄、(255, 0, 255)品红,随便加。

explode开头那个range(100)里的 100 是「一次爆炸的粒子数」。这个数字直接决定观感:改成 30,炸出来稀稀拉拉像火花;改成 300,一朵烟花就能铺满半个屏幕,但也会明显吃性能。新手建议先按 100 跑,跑通了再调。

del self.particles[0]这行删的是列表第 0 个元素,也就是那颗上升弹。如果不删,它会在原地继续减速、继续画,最后变成一朵烟花中间钉着一颗白点,很出戏。

然后是每帧调用的update:

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) # 只剩一颗(上升弹)而且飞得够高了,就引爆 if len(self.particles) == 1 and self.particles[0].y < 100: self.explode() return True

这里有两个细节值得停下来看。

第一,for particle in self.particles[:]里的[:]是复制列表。如果在遍历一个列表的同时删它的元素,Python 会跳过一些元素甚至报错,复制一份再遍历就没事了。这是新手写循环时的经典坑,后面排查章还会提。

第二,len(self.particles) == 1这个判断的意思是「现在只剩上升弹、还没炸」。为什么是 1 而不是 0?因为爆炸后列表里会有一百颗,数量不等于 1,就不会重复触发爆炸。如果写成<= 1,爆炸完那一瞬间列表会短暂为空,可能连炸好几次。

self.particles[0].y < 100是引爆高度,100 是「离屏幕顶部 100 像素」。调大一点(比如 200),烟花会更早炸开;调小一点,会飞得更高再炸。

4.5 主循环补全:随机发射、空格键、及时清理

零件齐了,剩下把它们串起来:

# 一开始先放一朵,保证屏幕上不是空的 fireworks = [Firework()] fireworks[0].launch() running = True while running: # 1. 处理事件 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()) fireworks[-1].launch() # 2. 擦黑板 screen.fill(BLACK) # 3. 更新每一朵烟花;返回 False 的说明它已经结束,从列表里移除 for firework in fireworks[:]: if not firework.update(): fireworks.remove(firework) # 4. 每帧有 5% 的概率自动放一朵,营造连绵不断的感觉 if random.random() < 0.05: fireworks.append(Firework()) fireworks[-1].launch() # 5. 显示并控制节奏 pygame.display.flip() clock.tick(60) pygame.quit()

第 4 步那个0.05很值得琢磨。每帧 5% 的概率,60 帧一秒,算下来平均每秒会冒出三朵左右。想放得更密就调到 0.1,想清静一点就调到 0.02。这种「每帧掷一次骰子」的写法在游戏里叫随机生成,好处是不用自己算时间间隔,代码也短。

第 3 步遍历fireworks[:]同样是复制,理由和粒子那边一样:循环里会删元素。

clock.tick(60)放在循环最后,它的返回值是「距离上一帧过了多少毫秒」,这份代码用不到返回值,但知道它有这个返回值,以后想做「按真实时间运动」的时候会用上。

4.6 背景音乐:加载、循环和最容易忽略的路径问题

画面跑通了,加音乐只要三行,位置放在初始化之后、主循环之前:

# 加载音乐文件,参数是文件路径 pygame.mixer.music.load("bgm.mp3") # -1 表示无限循环,1 表示只播一次 pygame.mixer.music.play(-1) # 音量 0.0 到 1.0 pygame.mixer.music.set_volume(0.6)

play(-1)里那个 -1 是「无限循环」的约定写法,写成 0 或者省略就是放一遍。新年烟花这种要一直跑的场景,肯定用 -1。

路径问题再强调一次:"bgm.mp3"是相对路径,找的是「运行命令时所在的目录」,不是代码文件所在的目录。最稳的做法是命令行先cd到项目文件夹,再python fireworks.py。实在不想折腾,可以用绝对路径,比如"C:/Users/xxx/Music/bgm.mp3",注意 Windows 下斜杠也要用正斜杠或者双反斜杠,不然会被当成转义字符。

如果运行时抛出一堆混音器相关的错误,多半是音频设备没就绪或者文件格式不对。先把load那行改成try/except包一下,音乐加载失败也不影响烟花继续放,这是很实用的一招:

try: pygame.mixer.music.load("bgm.mp3") pygame.mixer.music.play(-1) pygame.mixer.music.set_volume(0.6) except Exception: # 音乐出问题不影响画面,先放烟花 print("音乐加载失败,继续播放画面")

最后是参数手感表,想调出自己喜欢的味道,对着这张表改就行:

想达到的效果改哪个参数建议范围
烟花炸得更散Particle里的speed上限5 → 8
光点更细腻Particle里的sizerandint(4,7)→randint(2,4)
炸开后停留更久Particle里的life25 → 40
下落更明显move里的减速系数0.95 → 0.92
烟花更密集主循环里的自动发射概率0.05 → 0.10
炸得更早update里的引爆高度100 → 200

改完记得存盘再跑一次,别对着没保存的编辑器窗口发呆。

5. 新手最容易翻车的五个地方:一份按现象查的排查清单

5.1 窗口一闪就消失,或者根本不出现

现象:双击运行,黑窗口闪一下就没了,命令行里也没留下什么有用信息。

原因通常有两个。一是主循环根本没进去,running一开始就是 False,或者循环写在某个函数里但没被调用。二是程序跑完了直接结束,pygame.quit()执行后窗口自然关闭,你眼睛还没跟上。

解决:先别用双击,用命令行跑python fireworks.py,这样报错信息会停在窗口里给你看。如果是双击导致的问题,在文件最后加一行input("按回车退出")挡住,让窗口停住。如果是逻辑问题,在while前面打印一句print("进入循环"),看看有没有走到。

5.2 报错 No module named 'pygame'

现象:代码第一行import pygame就红,提示找不到模块。

原因:Pygame 没装上,或者装到了另一个 Python 环境里。电脑上有多个 Python 版本时特别容易出现——你用 A 版本装库,用 B 版本运行代码,两边对不上。

解决:先跑python -m pip show pygame看当前这个 Python 认不认这个库。不认就重装一次,务必带上-m:python -m pip install pygame。如果还是不行,用python -c "import sys; print(sys.executable)"打印出当前 Python 的完整路径,确认你运行的和你以为的是同一个。

5.3 烟花只炸一次,之后屏幕就一直空着

现象:第一朵炸完很漂亮,然后什么都没有了,按空格也没反应。

原因:九成是Firework.update里那个返回值出了问题。主循环靠if not firework.update(): fireworks.remove(...)来清场,如果update在粒子清空后仍然返回 True,这个烟花就会一直挂在列表里占位,看起来像卡住了;反过来如果提前返回 False,烟花被误删,后面自然没有新东西。另一个可能是自动发射那行概率写成了 0。

解决:在update开头和结尾各打印一次len(self.particles),看数量变化对不对。再检查自动发射那一行,random.random() < 0.05里的 0.05 是不是被改成了 0 或者负数。按空格没反应,则是事件判断里event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE的and写成了or,或者漏了KEYDOWN判断。

5.4 音乐文件明明在,却报错或者一点声音都没有

现象:pygame.mixer.music.load那一行抛异常,或者程序正常跑但静悄悄。

原因:路径不对是第一位——文件在代码旁边,但你的运行目录在别处。第二位是格式,mp3 在部分环境里解码会有问题。第三位是音量被设成了 0,或者系统音量、静音键的问题。

解决:先用绝对路径试一次,能响就说明是路径问题,回头把运行目录调对。格式问题就把音乐转成 ogg 或 wav 再试。用try/except包住加载过程,让音乐失败不影响画面,这是最实用的兜底。系统层面的事最后再查,别一开始就往那儿想。

5.5 加到十几个烟花就卡成幻灯片

现象:一朵两朵很流畅,屏幕上同时有十几朵时明显掉帧,鼠标拖动窗口都一顿一顿。

原因:粒子总数爆了。一朵烟花炸开是 100 颗粒子,十朵就是 1000 颗,每颗每帧都要算位置、画圆,还要遍历列表删死掉的粒子。列表越长,remove越慢,因为每次删都要从头找一遍。

解决:先把explode里的 100 降到 50,画面上看不出太大差别,性能直接翻倍。再把life从 25 降到 18,粒子死得快,屏幕上的总量就下来了。还可以限制同时存在的烟花数量,比如在自动发射那里加一个判断if len(fireworks) < 8:。这几招下去,普通笔记本跑到满屏都没问题。

6. 下一步学什么:从改参数到加拖尾的四级台阶

跑通之后最容易犯的错,是关掉编辑器不知道下一步干嘛。给一条我自己走过的顺序,四级台阶,每一级都能独立看到效果。

第一级:只改参数,不改结构。把第 4 章末尾那张表里的六个参数挨个改一遍,每改一个跑一次,用眼睛记住差别。这一步看着简单,其实是在建立「数字和画面之间的直觉」。我当初就是靠反复改speed和减速系数,才真正理解什么叫「手感」。

第二级:加拖尾。现在每帧都是screen.fill(BLACK)整块涂黑,等于把上一帧擦得干干净净。改成半透明覆盖,上一帧的残影就留下来了,烟花会拖出光带:

# 用一层半透明黑色盖住上一帧,制造拖尾残影 fade = pygame.Surface((screen_width, screen_height)) fade.set_alpha(30) # 0 全透明,255 全不透明,30 是淡淡一层 fade.fill((0, 0, 0)) screen.blit(fade, (0, 0))

把主循环里的screen.fill(BLACK)换成这三行就行。set_alpha的数字越小,残影留得越久;调到 100 以上就基本看不出拖尾了。这个技巧几乎所有粒子特效都用得上。

第三级:让爆炸声跟着烟花走。现在音乐是一整首循环,跟画面没关系。想更带感,可以准备几个短促的爆炸音效,在explode里触发播放,用pygame.mixer.Sound加载,sound.play()播放。这样每朵烟花炸开时都有对应的一声,临场感完全不一样。

第四级:加文字和倒计时。pygame.font.SysFont可以加载系统字体,把「新年快乐」和倒计时数字画到屏幕上。文字对象建议在循环外面创建一次,循环里只做blit,每帧重新创建字体会明显拖慢程序。

这四级台阶之外,还有一条更远的路:把烟花改成从底部斜着发射、加风向、加二次爆炸。走到那一步,你其实已经在做粒子系统了,可以去看一些开源小游戏项目的源码,会发现结构跟这份代码一模一样,只是零件更多。

最后说个血泪经验。我刚开始写图形程序时,习惯一口气把代码写完再运行,结果一报错就是十几处,根本不知道从哪查。从那以后我每次动手前都强制先跑一个空窗口,确认环境没问题,再一段一段往里加代码,每加一段就运行一次。慢是慢了点,但从来没在排查上熬过夜。希望你也能养成这个习惯,别等到屏幕上只剩一片黑才开始找原因。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进