C++祝福烟花源码实战:粒子系统与帧循环从零跑通

发布时间:2026/9/28 23:50:03
C++祝福烟花源码实战:粒子系统与帧循环从零跑通
简介这是一份面向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的固定时间步长、以及图形库自带的事件循环。如果你用的是 EasyXWindows 常见教学图形库它本身没有事件循环你需要自己写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_distributionfloat angleDist(0.0f, 6.28318f); std::uniform_real_distributionfloat 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::durationfloat(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::vectorParticle存所有粒子每帧遍历更新死亡的粒子用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::vectorParticle particles, float cx, float cy) { std::uniform_real_distributionfloat angleDist(0.0f, 6.28318f); std::uniform_real_distributionfloat speedDist(1.5f, 4.5f); std::uniform_int_distributionint 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::vectorParticle 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::durationfloat(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 或简单的keyvalue文本把粒子数量、重力、颜色范围、爆炸半径、升空速度都做成可调的。这样改效果不用重新编译改完配置文件重启程序就行。另一个实用技巧是加文字烟花在爆炸位置用outtextxy画一句祝福语然后让文字也参与粒子衰减或者用粒子拼出文字轮廓。EasyX 的outtextxy不支持透明度但你可以用settextcolor配合背景色模拟淡出或者把文字转成点阵每个点生成一个粒子这样文字会随着粒子飘散效果比静态文字好很多。验证方法很简单把粒子数量调到 500重力调到 100颜色范围设成暖色运行看是否流畅、颜色是否舒服。如果卡顿先降粒子数如果颜色不对检查 HSV 转换的饱和度参数。我一般会保留一个debug开关打开时在窗口左上角显示当前粒子数和帧率方便调参。帧率用1.0f / dt算每 30 帧更新一次显示避免数字跳动太快看不清。最后说一个我踩过的坑不要用system(pause)来暂停窗口那样会阻塞消息循环窗口会变成“无响应”。正确做法是在主循环里检测按键按 ESC 退出。另外如果你要发布 exe 给别人记得把 Visual C 运行库一起打包或者用静态链接编译否则对方可能打不开。这个方向值不值得做如果你只是想交个课程设计上面的最小版本足够了如果你想做一个能发给朋友、能改参数、能当桌面摆件的祝福程序那花两个小时把配置和文字烟花加上完全值得。希望帮到你。本文还有配套的精品资源点击获取