简介:这是一份面向C++初学者与图形编程爱好者的烟花特效完整源码,基于Visual C++开发,采用面向对象思想组织代码,可用于节日祝福、课程设计或编程练手等场景。压缩包共20个文件,约5.65MB,包含cpp与h源码文件、ico图标与rc资源脚本、dsp/dsw工程文件,以及jpg背景图、mp3与wav音效素材,工程可直接编译运行,方便读者对照学习粒子绘制、动画刷新与资源加载等实现细节。目前已有861人学习下载,说明该示例在同类资源中具有一定参考价值。源码结构清晰,将烟花绽放逻辑与界面资源分离,读者可在此基础上调整颜色、爆炸半径与音效,快速改造成自己的祝福动画,也能借此理解C++面向对象与Windows图形编程的基本配合方式。
1. 从一束会“呼吸”的烟花说起:C++ 祝福烟花源码到底能跑出什么
很多人第一次看到“祝福烟花”这四个字,脑子里浮现的是网页上那种 GIF 动图,或者某个小程序里点一下炸开一堆彩色像素。但如果你拿到一份基于 C++ 语言实现的祝福烟花效果完整源码,你会发现它和网页动画完全是两回事——它直接操作图形库的绘制接口,每一帧都在算粒子位置、颜色衰减、重力加速度和拖尾长度,最终在窗口里跑出一束会呼吸、会扩散、会自然熄灭的烟花。这类源码通常被拿来做节日祝福程序、桌面小工具、课程设计,或者单纯作为 C++ 图形编程的练手项目。它适合已经学过 C++ 基础语法、想找一个“看得见摸得着”的练手项目的人,也适合需要快速交付一个节日氛围程序的开发者。热搜里常出现“c++小游戏”“c++小游戏代码”“c++入门”这些词,其实烟花源码就是介于小游戏和纯算法练习之间的一个甜点级项目:逻辑不复杂,但涉及随机数、帧循环、粒子系统、颜色插值,能把 C++ 的多个基础点串起来。下面我不讲空泛概念,直接按“能跑起来、能改参数、能避坑”的路径拆开说。
2. 粒子系统与帧循环:烟花效果为什么不能靠一张图搞定
2.1 烟花的核心不是“画”,而是“算”
很多人以为烟花效果就是准备几张爆炸序列图,按时间切换。这种做法在网页里常见,但在 C++ 原生窗口里非常别扭:图片体积大、缩放失真、颜色无法动态变化,而且一旦你想改爆炸半径或粒子数量,就得重新做图。真正靠谱的做法是粒子系统:每一颗火星都是一个独立的数据对象,包含位置、速度、加速度、生命值、颜色、拖尾长度。每一帧做三件事——更新所有粒子的物理状态、根据生命值计算透明度或颜色、把粒子画到缓冲区。烟花升空阶段是一个粒子,爆炸瞬间生成几十到几百个粒子,每个粒子继承一个随机方向的速度,然后受重力影响逐渐下落,同时生命值递减,直到消失。这个模型的好处是参数全在代码里,改一个数字就能改变爆炸大小、持续时间、颜色范围。
热搜里“c++随机数”这个词在这里非常关键。爆炸方向不能是固定角度,否则每次炸开都是同一个形状。常见做法是用std::mt19937配合std::uniform_real_distribution生成角度和速度的随机值。注意不要用rand(),因为它的分布质量和线程安全性在现代 C++ 里已经不够看,而且rand()的最大值在不同平台可能不一样,会导致烟花在 Windows 和 Linux 上表现不一致。
2.2 帧循环的三种写法与选型理由
C++ 里做动画循环,常见有三种:while死循环加Sleep、基于std::chrono的固定时间步长、以及图形库自带的事件循环。如果你用的是 EasyX(Windows 常见教学图形库),它本身没有事件循环,你需要自己写while循环,里面调用BeginBatchDraw和FlushBatchDraw来避免闪烁。如果你用的是 SFML 或 SDL,它们提供while (window.isOpen())结构,你只需要在循环里处理事件和绘制。选型建议:纯 Windows 教学或课程设计,EasyX 上手最快;想跨平台或者以后转游戏开发,直接上 SFML。下面给一个基于 EasyX 的最小帧循环骨架,这段代码可以直接抄进你的项目里替换主循环。
#include <graphics.h> #include <chrono> #include <random> int main() { initgraph(800, 600); // 初始化 800x600 窗口 BeginBatchDraw(); // 开启批量绘制,防止闪烁 std::mt19937 gen(std::random_device{}()); std::uniform_real_distribution<float> angleDist(0.0f, 6.28318f); std::uniform_real_distribution<float> speedDist(1.0f, 5.0f); auto lastTime = std::chrono::steady_clock::now(); bool running = true; while (running) { auto now = std::chrono::steady_clock::now(); float dt = std::chrono::duration<float>(now - lastTime).count(); lastTime = now; // 更新粒子状态,dt 用于保证不同帧率下速度一致 // updateParticles(dt); cleardevice(); // drawParticles(); FlushBatchDraw(); if (GetAsyncKeyState(VK_ESCAPE) & 0x8000) { running = false; } } EndBatchDraw(); closegraph(); return 0; }这段代码里dt是上一帧到当前帧的时间差,单位是秒。为什么一定要用dt?因为如果你直接写x += speed,在 60Hz 屏幕和 144Hz 屏幕上粒子移动速度会差一倍多,看起来就是“这台电脑上烟花飞得快,那台上飞得慢”。用dt乘以速度,物理表现就和帧率无关了。BeginBatchDraw和FlushBatchDraw是 EasyX 的双缓冲机制,不加的话你会看到烟花一闪一闪,像接触不良的灯管。GetAsyncKeyState用来检测 ESC 键,注意它返回的是短整型,高位为 1 表示按下,所以用& 0x8000判断。
2.3 粒子数据结构怎么设计才不拖后腿
粒子数量一多,结构体设计直接影响性能。常见做法是定义一个struct Particle,里面放float x, y, vx, vy, life, maxLife; COLORREF color;。注意life和maxLife要分开:life是当前剩余生命,maxLife是初始生命,用life / maxLife可以算出一个 0 到 1 的衰减系数,用来控制透明度和颜色变暗。不要用int存位置,因为粒子移动速度可能小于 1 像素每帧,用int会直接截断成 0,粒子就卡住不动了。颜色建议用COLORREF或者自己定义一个 RGB 结构,不要用字符串存颜色名,那样每帧解析字符串会慢得离谱。
如果你要同时存在多个烟花,建议用std::vector<Particle>存所有粒子,每帧遍历更新,死亡的粒子用erase移除。但注意erase在循环里频繁调用会导致迭代器失效和内存搬移,更好的做法是标记死亡,然后在循环结束后用std::remove_if统一清理。这个细节在粒子数量超过 500 时非常明显,不处理的话帧率会从 60 掉到 20 以下。
3. 从零跑通一份祝福烟花源码:环境、编译与最小可运行版本
3.1 环境选型:Visual Studio 还是 VS Code
热搜里“vscode配置c/c++环境”“vscode c++”“dev c++官网”出现频率很高,说明很多人卡在环境上。如果你用的是 EasyX,它只支持 Visual Studio 和 MSVC 编译器,VS Code 配 MinGW 是跑不了的,因为 EasyX 依赖 Windows GDI 和 MSVC 的特定库。所以路线很明确:想用 EasyX,就装 Visual Studio Community 版本,安装时勾选“使用 C++ 的桌面开发”;想用 VS Code,就放弃 EasyX,改用 SFML 或 SDL2,它们支持 MinGW 和 CMake。不要试图在 VS Code 里硬配 EasyX,那是血泪经验,我见过太多人卡在graphics.h找不到的问题上,最后发现是编译器根本不匹配。
如果你已经装了 Visual Studio,还需要确认“microsoft visual c++ redistributable”是否安装。这个运行库是很多 C++ 程序运行的前提,虽然你自己编译的程序通常不依赖它,但如果你拿到的是别人编译好的 exe,缺少这个运行库会报“找不到 vcruntime140.dll”之类的错误。安装包在微软官网可以免费下载,直接搜“Microsoft Visual C++ Redistributable”就能找到。
3.2 最小可运行版本:一个会升空爆炸的烟花
下面给一个完整的最小可运行版本,基于 EasyX,包含升空和爆炸两个阶段。你可以直接复制到 Visual Studio 里新建的空项目中,把main.cpp替换掉,编译运行就能看到效果。
#include <graphics.h> #include <vector> #include <random> #include <chrono> #include <cmath> struct Particle { float x, y; float vx, vy; float life, maxLife; COLORREF color; }; std::mt19937 gen(std::random_device{}()); void spawnExplosion(std::vector<Particle>& particles, float cx, float cy) { std::uniform_real_distribution<float> angleDist(0.0f, 6.28318f); std::uniform_real_distribution<float> speedDist(1.5f, 4.5f); std::uniform_int_distribution<int> colorDist(0, 255); int count = 120; // 爆炸粒子数量,改这个值可以控制爆炸密度 for (int i = 0; i < count; ++i) { float angle = angleDist(gen); float speed = speedDist(gen); Particle p; p.x = cx; p.y = cy; p.vx = std::cos(angle) * speed; p.vy = std::sin(angle) * speed; p.maxLife = 1.2f + (speed / 4.5f) * 0.8f; // 速度越快生命越长 p.life = p.maxLife; p.color = RGB(colorDist(gen), colorDist(gen), colorDist(gen)); particles.push_back(p); } } int main() { initgraph(800, 600); BeginBatchDraw(); std::vector<Particle> particles; auto lastTime = std::chrono::steady_clock::now(); // 升空粒子:从底部中间往上飞 Particle rocket; rocket.x = 400.0f; rocket.y = 580.0f; rocket.vx = 0.0f; rocket.vy = -220.0f; // 负值表示向上 rocket.maxLife = 1.8f; rocket.life = rocket.maxLife; rocket.color = RGB(255, 220, 100); particles.push_back(rocket); bool exploded = false; while (true) { auto now = std::chrono::steady_clock::now(); float dt = std::chrono::duration<float>(now - lastTime).count(); lastTime = now; cleardevice(); for (auto& p : particles) { p.x += p.vx * dt * 60.0f; // 乘以 60 是为了让速度值更直观 p.y += p.vy * dt * 60.0f; p.vy += 180.0f * dt; // 重力加速度 p.life -= dt; if (p.life > 0.0f) { float alpha = p.life / p.maxLife; int r = GetRValue(p.color) * alpha; int g = GetGValue(p.color) * alpha; int b = GetBValue(p.color) * alpha; setfillcolor(RGB(r, g, b)); solidcircle((int)p.x, (int)p.y, 2); } } // 升空粒子生命结束或到达顶点时爆炸 if (!exploded && !particles.empty() && particles[0].life <= 0.0f) { spawnExplosion(particles, particles[0].x, particles[0].y); exploded = true; } // 清理死亡粒子 particles.erase( std::remove_if(particles.begin(), particles.end(), [](const Particle& p) { return p.life <= 0.0f; }), particles.end() ); FlushBatchDraw(); if (GetAsyncKeyState(VK_ESCAPE) & 0x8000) break; Sleep(16); // 约 60 帧每秒 } EndBatchDraw(); closegraph(); return 0; }这段代码的逻辑说明:spawnExplosion在指定坐标生成 120 个粒子,每个粒子有随机角度和随机速度,颜色也是随机的。主循环里每个粒子按速度更新位置,同时vy受重力影响逐渐增大,模拟下坠。alpha是生命衰减系数,用来让粒子在消失前逐渐变暗。particles[0]是升空粒子,当它的生命归零时触发爆炸。注意erase和remove_if配合使用,这是 C++ 里删除 vector 中满足条件元素的惯用法,比手动循环 erase 更安全。
参数说明:rocket.vy = -220.0f控制升空速度,绝对值越大飞得越高;count = 120控制爆炸粒子数,改到 300 会非常密集但可能掉帧;p.vy += 180.0f * dt里的 180 是重力加速度,改小会让烟花下坠更慢,看起来更“飘”;p.maxLife的计算方式让速度快的粒子活得更久,这样爆炸边缘的粒子不会突然消失,视觉上更自然。
3.3 编译时常见的三个报错与处理
第一个报错是cannot open source file "graphics.h",说明 EasyX 没装或者没配置到包含目录。去 EasyX 官网下载安装包,安装时它会自动检测你的 Visual Studio 版本并配置好。第二个报错是unresolved external symbol,通常是链接器没找到 EasyX 的库文件,检查项目属性里的链接器输入是否包含EasyXa.lib或EasyXw.lib。第三个报错是std::remove_if找不到,需要包含<algorithm>头文件,这个在代码里没写但必须加,否则编译不过。
4. 让烟花“漂亮”起来的五个参数:颜色、拖尾、重力、随机与性能
4.1 颜色插值与 HSV 模型
上面最小版本用的是随机 RGB,颜色虽然多但看起来有点“脏”,因为 RGB 随机出来的颜色饱和度不可控。想让烟花漂亮,常见做法是用 HSV 模型生成颜色:固定饱和度和明度,只随机色相,这样出来的颜色鲜艳且不刺眼。C++ 标准库没有 HSV 转 RGB 的函数,需要自己写一个,大概十行左右。转换后把 RGB 值存进粒子,绘制时再按生命衰减。另一个技巧是同一束烟花用相近色相,比如爆炸时色相在 0.0 到 0.15 之间随机,这样炸开是一团暖色,而不是五颜六色的杂色团。
4.2 拖尾效果:不是画线,而是记录历史位置
很多人以为拖尾就是画一条从上一帧位置到当前位置的线。这样做在粒子速度慢的时候还行,速度快的时候会看到明显的折线。更好的做法是每个粒子保存最近 5 到 8 个历史位置,绘制时从旧到新画圆,半径逐渐减小,透明度逐渐增加。历史位置用环形缓冲区存,避免频繁内存分配。这个改动会让粒子结构体变大,但视觉效果提升非常明显,尤其是爆炸初期粒子速度快的时候,拖尾会拉出漂亮的弧线。
4.3 重力与阻尼:让烟花有“下落感”但不突兀
重力加速度不能太大,否则粒子会像石头一样直直掉下去,失去烟花那种轻盈感。我一般用 120 到 200 之间的值,配合 0.98 到 0.995 的阻尼系数。阻尼每帧乘以速度,让粒子逐渐减速,模拟空气阻力。注意阻尼要乘以dt相关的系数,不能直接写vx *= 0.99,否则帧率不同阻尼效果不同。正确做法是vx *= std::pow(0.99f, dt * 60.0f),这样在 60 帧和 144 帧下表现一致。
4.4 随机数质量与性能的平衡
std::mt19937质量好但构造和调用比rand()慢。如果每帧生成大量随机数,比如爆炸时生成 500 个粒子,每个粒子调用两次分布器,总共 1000 次,这在现代 CPU 上完全不是问题。但如果你在每帧的粒子更新里也调用随机数,比如给每个粒子加随机扰动,那就要注意了。常见优化是预生成一批随机数存到数组里,用的时候取,或者用更快的std::minstd_rand。不过对于烟花这种粒子数量在几百到几千的项目,mt19937完全够用,不需要过早优化。
4.5 性能边界:多少粒子会开始掉帧
在普通集成显卡的笔记本上,EasyX 绘制 2000 个实心圆大约能维持 60 帧,超过 3000 个会明显感觉到卡顿。如果你要同时放多个烟花,建议把粒子总数控制在 1500 以内,或者改用更高效的绘制方式,比如把粒子画到内存 DC 再一次性贴图。另一个技巧是远处的小粒子用putpixel画单像素,近处的大粒子才用solidcircle,这样能省不少绘制时间。热搜里“c++游戏”相关的项目经常遇到性能问题,其实大部分不是 C++ 慢,而是绘制调用太频繁。
5. 避坑与排查:烟花源码从能跑到好看的五个翻车点
5.1 烟花一闪一闪像接触不良
现象:窗口里的烟花每帧都在闪烁,粒子时有时无。原因:没有使用双缓冲,cleardevice之后直接绘制,屏幕刷新和绘制不同步。解决:在initgraph之后调用BeginBatchDraw(),每帧绘制完调用FlushBatchDraw(),程序结束前EndBatchDraw()。如果用了 SFML,确保用window.display()而不是手动清屏。
5.2 粒子在高速移动时变成一条虚线
现象:爆炸初期粒子飞得很快,但看起来是一段一段的虚线,不连贯。原因:粒子每帧移动距离超过粒子半径,两个绘制位置之间有空隙。解决:要么增大粒子半径,要么在上一帧位置和当前位置之间插值画多个圆。更简单的做法是限制粒子最大速度,让每帧移动不超过 3 像素。如果不想限速,就用拖尾历史位置来填补空隙。
5.3 程序运行几分钟后越来越卡
现象:刚开始很流畅,放了几十个烟花后帧率骤降。原因:死亡粒子没有及时清理,std::vector越来越大,每帧遍历和绘制的粒子数越来越多。解决:每帧结束后用remove_if清理life <= 0的粒子。注意不要在遍历过程中直接erase,那样会导致迭代器失效,程序可能崩溃。另外检查是否有粒子生命值永远不会降到 0 的情况,比如life被错误地重置了。
5.4 换台电脑烟花飞得特别快或特别慢
现象:在自己电脑上正常,发给别人运行速度完全不对。原因:用了固定帧率假设,比如直接x += vx而没有乘以dt。解决:所有与时间相关的更新都必须乘以dt,包括位置、速度、生命值衰减。Sleep(16)只能控制大概帧率,不能保证每帧恰好 16 毫秒,所以dt是必须的。如果用了Sleep,注意dt可能会因为系统调度而波动,建议对dt做上限截断,比如dt = min(dt, 0.05f),防止某一帧卡顿后粒子瞬移。
5.5 颜色发灰、发暗,没有“祝福”的喜庆感
现象:烟花炸开颜色偏暗,像蒙了一层灰。原因:RGB 随机时三个通道独立随机,容易生成低饱和度颜色;另外生命衰减时直接乘 RGB 值,导致颜色变暗的同时饱和度也下降。解决:改用 HSV 生成颜色,固定饱和度和明度;衰减时只降低明度,保持饱和度不变。具体做法是把颜色转成 HSV,每帧减少 V 分量,再转回 RGB 绘制。这样粒子消失时是变暗而不是变灰,视觉上更干净。
6. 进阶玩法:把烟花做成可配置的祝福程序
如果你已经跑通了上面的版本,下一步可以把它做成一个真正能拿得出手的祝福程序。我的习惯是加一个配置文件,用 JSON 或简单的key=value文本,把粒子数量、重力、颜色范围、爆炸半径、升空速度都做成可调的。这样改效果不用重新编译,改完配置文件重启程序就行。另一个实用技巧是加文字烟花:在爆炸位置用outtextxy画一句祝福语,然后让文字也参与粒子衰减,或者用粒子拼出文字轮廓。EasyX 的outtextxy不支持透明度,但你可以用settextcolor配合背景色模拟淡出,或者把文字转成点阵,每个点生成一个粒子,这样文字会随着粒子飘散,效果比静态文字好很多。
验证方法很简单:把粒子数量调到 500,重力调到 100,颜色范围设成暖色,运行看是否流畅、颜色是否舒服。如果卡顿,先降粒子数;如果颜色不对,检查 HSV 转换的饱和度参数。我一般会保留一个debug开关,打开时在窗口左上角显示当前粒子数和帧率,方便调参。帧率用1.0f / dt算,每 30 帧更新一次显示,避免数字跳动太快看不清。
最后说一个我踩过的坑:不要用system("pause")来暂停窗口,那样会阻塞消息循环,窗口会变成“无响应”。正确做法是在主循环里检测按键,按 ESC 退出。另外如果你要发布 exe 给别人,记得把 Visual C++ 运行库一起打包,或者用静态链接编译,否则对方可能打不开。这个方向值不值得做?如果你只是想交个课程设计,上面的最小版本足够了;如果你想做一个能发给朋友、能改参数、能当桌面摆件的祝福程序,那花两个小时把配置和文字烟花加上,完全值得。希望帮到你。
本文还有配套的精品资源,点击获取