简介:这是一份用C语言编写的跨年祝福烟花程序源码,面向具备C语言基础、想通过图形编程练手或制作节日祝福效果的学习者。程序基于VC++6.0与EasyX图形库开发,通过粒子模拟实现烟花绽放动画,并配合音效与图片素材营造跨年氛围,适合作为课程设计、图形编程入门或节日小项目的参考案例。压缩包共20个文件,约5.59MB,包含cpp与h源码文件、exe可执行程序、ico图标、jpg图片、mp3与wav音频,以及dsp、dsw、opt等VC工程配置文件和rc资源脚本,源码、素材与工程文件齐全,可直接编译运行或二次修改。目前已有1669人学习下载。读者可从中掌握EasyX绘图、粒子动画、定时器与多媒体音效调用等实现思路,并参考工程目录组织方式快速搭建自己的图形项目。
1. 跨年烟花代码C语言实现:从零手搓一个控制台粒子动画
跨年那晚,与其对着屏幕刷别人的烟花视频,不如自己用 C 语言写一个。这份「跨年烟花代码 C 语言实现」资源,核心就是用标准 C 在控制台里跑出一套粒子烟花动画——发射、升空、爆炸、拖尾、重力回落,一整套流程全在终端字符画布上完成。它不依赖图形库,不需要显卡,Windows 下用 GCC 或 MSVC 编译就能跑,Linux 和 macOS 改几行终端控制代码也能用。适合两类人:一是刚学完数组和循环、想找个能跑起来又有点视觉反馈的练手项目的新手;二是想拿它当粒子系统入门模板、后面往 SDL 或 OpenGL 迁移的熟手。整个资源就是一份可编译的 C 源码加少量注释,逻辑密度不低,但拆开看每一块都不复杂。
2. 粒子系统原理与核心数据结构:为什么用数组而不是链表
2.1 烟花在控制台里到底是怎么画出来的
控制台本身没有像素概念,能操作的只有字符和光标位置。所以这套代码的思路是:把终端当成一块宽 × 高的字符网格,每个格子要么是空格,要么是某个代表烟花的字符(常见的是*、.、o、#)。每一帧先清屏,再根据当前所有粒子的坐标把对应格子填上字符,最后统一刷新输出。人眼看到的就是连续动画。
这里有个关键点:控制台刷新如果直接printf一整屏,闪烁会非常严重。常见做法是先把整帧内容拼进一个大的字符缓冲区,再用一次输出把缓冲区推出去,减少刷新次数。Windows 下还可以用SetConsoleCursorPosition把光标移回左上角,避免system("cls")那种整屏清空带来的抖动。这套代码在 Windows 分支里用的就是光标复位方案,Linux 分支则用 ANSI 转义序列\033[H把光标拉回原点。
粒子本身要记录的信息不多,但一个都不能少:当前位置x, y、当前速度vx, vy、剩余生命life、以及它属于哪一类(是上升的发射粒子,还是爆炸后的火花)。速度决定下一帧往哪走,生命决定它还能亮多久,类型决定它用什么字符、受不受重力影响。
2.2 用结构体数组管理粒子池
链表在粒子数量少的时候没问题,但烟花爆炸瞬间可能同时存在几百个粒子,频繁malloc/free会让帧率抖得厉害,而且新手很容易在这里写出内存泄漏。这套代码用的是固定大小的结构体数组当粒子池,所有粒子预分配好,用active标志位标记是否存活。这样内存是连续的,遍历快,也不会有碎片。
#include <stdio.h> #include <stdlib.h> #include <math.h> #include <string.h> #include <time.h> #define MAX_PARTICLES 1024 // 粒子池上限,爆炸密集时可调大 #define WIDTH 80 // 画布宽度,建议不超过终端实际列数 #define HEIGHT 24 // 画布高度,留出底部地面行 typedef struct { float x, y; // 当前位置(浮点,便于做亚像素速度累积) float vx, vy; // 当前速度分量 int life; // 剩余生命帧数,归零即回收 int max_life; // 初始生命,用于计算亮度衰减 int active; // 1 表示占用,0 表示可复用 char symbol; // 该粒子渲染用的字符 } Particle; Particle pool[MAX_PARTICLES]; // 从池中取一个空闲粒子,返回下标;池满返回 -1 int spawn_particle(void) { for (int i = 0; i < MAX_PARTICLES; i++) { if (!pool[i].active) { pool[i].active = 1; return i; } } return -1; // 池满,这一帧就丢弃新粒子,避免越界 }MAX_PARTICLES是最需要根据机器和终端调的参数。设太小,大爆炸时粒子不够,烟花看着稀;设太大,每帧遍历全部 1024 个槽位(哪怕大部分是空的)会浪费 CPU。我一般先设 1024,跑起来看帧率,卡就降到 512,想更密就上 2048。x, y用float而不是int,是因为速度往往小于 1 像素每帧,用整数会直接归零,粒子根本动不起来——这是新手最容易翻车的地方之一。
spawn_particle用的是线性扫描找空位,粒子池不大时完全够用。如果后面粒子数上千,可以维护一个空闲栈把复杂度降下来,但那是优化阶段的事,第一版没必要。
2.3 重力、阻力与生命衰减的数值设计
粒子飞出去之后不能一直匀速直线走,那样看着像激光不像烟花。真实感来自三个力:重力让粒子下坠,空气阻力让速度慢慢衰减,生命衰减让粒子逐渐变暗消失。
#define GRAVITY 0.08f // 每帧向下加速度 #define DRAG 0.985f // 速度阻尼,越接近 1 越飘 #define FADE_STEP 1 // 每帧生命递减量 void update_particles(void) { for (int i = 0; i < MAX_PARTICLES; i++) { if (!pool[i].active) continue; pool[i].vy += GRAVITY; // 重力只作用于 y 方向 pool[i].vx *= DRAG; // 阻力同时作用于两个方向 pool[i].vy *= DRAG; pool[i].x += pool[i].vx; pool[i].y += pool[i].vy; pool[i].life -= FADE_STEP; // 出界或生命耗尽就回收 if (pool[i].life <= 0 || pool[i].x < 0 || pool[i].x >= WIDTH || pool[i].y < 0 || pool[i].y >= HEIGHT) { pool[i].active = 0; } } }GRAVITY和DRAG这两个值直接决定烟花的「手感」。重力调大,火花下坠快,像礼花弹;调小,粒子飘得久,像仙女棒。阻力越接近 1,速度保持越久,拖尾越长。我试过DRAG = 0.99配GRAVITY = 0.05,出来的效果偏柔和;想要那种炸开四散、迅速下落的干脆感,就用0.97配0.12。这两个参数没有标准答案,建议做成宏,改一个数重新编译就能对比。
生命衰减用FADE_STEP控制,配合渲染时按life / max_life的比例选字符,就能做出从亮到暗的层次:生命满时用*,过半用+,快没了用.。这样即使没有颜色,视觉上也有衰减感。
3. 从发射到爆炸:完整动画循环的搭建步骤
3.1 主循环的帧节奏与输入处理
动画的本质是「更新状态 → 渲染 → 等待 → 再来一遍」。控制台程序没有垂直同步,帧率全靠自己控。常见做法是用Sleep(Windows)或usleep(Linux)在每帧末尾睡一小段时间,把帧率压到 30 到 60 之间。睡太少 CPU 跑满,睡太多动画卡顿。
#include <windows.h> // Windows 下用 Sleep,单位毫秒 int main(void) { srand((unsigned)time(NULL)); // 隐藏光标,避免闪烁(Windows 专用) HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE); CONSOLE_CURSOR_INFO cci = {1, FALSE}; SetConsoleCursorInfo(hOut, &cci); int frame = 0; while (1) { // 每隔若干帧发射一枚新烟花 if (frame % 40 == 0) { launch_firework(); } update_particles(); render_frame(); Sleep(33); // 约 30 FPS frame++; } return 0; }Sleep(33)对应大约 30 帧每秒,这是控制台动画比较舒服的节奏。想更顺滑可以降到 16(约 60 FPS),但控制台输出本身有开销,太快反而会因为输出跟不上而抖动。frame % 40控制发射频率,数值越小烟花越密。这里用取模而不是随机,是为了让发射节奏稳定,不会出现长时间空场。
Linux 下把Sleep(33)换成usleep(33000),并#include <unistd.h>即可。光标隐藏那几行是 Windows 专有 API,Linux 下用 ANSI 序列\033[?25l代替。
3.2 发射粒子的初始化与升空逻辑
一枚烟花分两个阶段:先是一颗发射粒子从底部往上飞,飞到一定高度后「炸开」,生成一批四散的火花粒子。发射粒子本身也要受重力和阻力影响,但初速度给得大,方向基本朝上略带随机偏移。
void launch_firework(void) { int idx = spawn_particle(); if (idx < 0) return; Particle *p = &pool[idx]; p->x = WIDTH / 2 + (rand() % 21 - 10); // 底部中间附近随机偏移 p->y = HEIGHT - 1; p->vx = (rand() % 100 - 50) / 500.0f; // 轻微水平漂移 p->vy = -(1.2f + (rand() % 40) / 100.0f); // 向上,速度随机 p->life = 45 + rand() % 15; p->max_life = p->life; p->symbol = '|'; p->active = 1; }vy取负值是因为控制台坐标 y 向下增大,向上飞就得减 y。-(1.2f + ...)保证初速度至少 1.2,否则粒子飞不起来。水平漂移控制在 ±0.1 以内,太大就不像垂直升空了。生命给 45 到 60 帧,配合 30 FPS,大约 1.5 到 2 秒升空时间,节奏刚好。
爆炸的触发点一般放在发射粒子生命耗尽或者速度由负转正的瞬间。这套代码用的是生命过半时炸开,逻辑简单,效果也稳。
3.3 爆炸火花的球面分布与随机扰动
爆炸是整套代码里最出效果的一步。核心是让一批粒子从爆炸点向四周均匀散开。最朴素的做法是遍历一圈角度,每个角度生成一个粒子,速度大小加随机扰动。
void explode(float cx, float cy) { int count = 60 + rand() % 40; // 每次爆炸 60 到 100 个火花 for (int i = 0; i < count; i++) { int idx = spawn_particle(); if (idx < 0) return; // 角度均匀分布 + 随机抖动,避免出现明显的射线状 float angle = (2.0f * 3.1415926f * i) / count + (rand() % 100 - 50) / 500.0f; float speed = 0.6f + (rand() % 100) / 200.0f; // 0.6 ~ 1.1 Particle *p = &pool[idx]; p->x = cx; p->y = cy; p->vx = cosf(angle) * speed; p->vy = sinf(angle) * speed; p->life = 30 + rand() % 20; p->max_life = p->life; p->symbol = '*'; p->active = 1; } }角度均匀分布保证火花不会全挤在一侧,随机抖动打破机械感。速度范围 0.6 到 1.1 让爆炸有层次,快的先冲出去,慢的留在中心,视觉上更饱满。count控制在 60 到 100,配合 1024 的粒子池,同时炸两三枚也不会溢出。
这里有个细节:cosf/sinf用的是弧度,别传角度进去。另外记得#include <math.h>,GCC 编译时加-lm链接数学库,否则会报未定义引用——这个坑我见过太多人踩。
3.4 渲染缓冲区的拼接与刷新
渲染这一步决定了画面闪不闪。直接对每个粒子printf会闪到没法看,正确做法是先拼一个HEIGHT行的字符串缓冲区,每行WIDTH个字符,最后一次性输出。
void render_frame(void) { static char canvas[HEIGHT][WIDTH + 1]; // 先全部填成空格 for (int y = 0; y < HEIGHT; y++) { memset(canvas[y], ' ', WIDTH); canvas[y][WIDTH] = '\0'; } // 把存活粒子画到对应格子 for (int i = 0; i < MAX_PARTICLES; i++) { if (!pool[i].active) continue; int px = (int)(pool[i].x + 0.5f); int py = (int)(pool[i].y + 0.5f); if (px < 0 || px >= WIDTH || py < 0 || py >= HEIGHT) continue; // 按生命比例选字符,做出亮度层次 float ratio = (float)pool[i].life / pool[i].max_life; char c = ratio > 0.66f ? '*' : (ratio > 0.33f ? '+' : '.'); canvas[py][px] = c; } // 光标复位后整屏输出 printf("\033[H"); for (int y = 0; y < HEIGHT; y++) { printf("%s\n", canvas[y]); } }canvas声明成static是为了避免每帧在栈上重新分配 24×81 字节,虽然不大,但养成习惯没坏处。坐标加 0.5 再取整是四舍五入,比直接截断更准。字符按生命比例分三档,简单但有效。\033[H是 ANSI 光标复位,Windows 10 以后的终端默认支持;老版本 Windows 得用SetConsoleCursorPosition,代码里可以做个条件编译。
4. 避坑与排查:控制台烟花最容易翻车的五个地方
4.1 画面疯狂闪烁,根本看不清烟花
现象:运行后整屏不停闪,粒子轨迹糊成一片。原因:每帧用system("cls")清屏再逐行printf,清屏和输出之间有时间差,人眼就看到了中间态。解决:改用光标复位\033[H或SetConsoleCursorPosition,并且把整帧拼进缓冲区一次性输出。清屏和渲染之间不要有Sleep。
4.2 粒子全部堆在底部不动
现象:发射粒子生成后原地不动或者只挪一点点。原因:x, y用了int,而速度是 0.0x 这种小数,int += float每次都被截断成 0。解决:坐标必须用float或double,渲染时再转int。这个坑几乎每个新手都会踩一次。
4.3 编译报 undefined reference tocosf
现象:GCC 编译通过,链接时报数学函数未定义。原因:math.h里的函数在单独的数学库里,GCC 默认不链接。解决:编译命令加-lm,写成gcc firework.c -o firework -lm。MSVC 不需要,但 GCC 和 Clang 都要。
4.4 爆炸瞬间粒子池溢出,烟花缺一块
现象:连续放几枚烟花后,新爆炸的火花明显变少甚至没有。原因:MAX_PARTICLES设小了,或者粒子回收不及时,池被占满,spawn_particle返回 -1 后新粒子被丢弃。解决:把MAX_PARTICLES调到 2048,并检查update_particles里出界和生命归零的回收条件是否都写了。另外发射频率别设太高,frame % 40改成% 60也能缓解。
4.5 终端窗口大小和画布不匹配,画面错位
现象:画布右边被截断,或者换行错乱导致整体斜掉。原因:WIDTH设成了 80,但终端实际列数不够,或者终端字体不是等宽。解决:把WIDTH降到 60 甚至 40,HEIGHT降到 20 以内,保证在默认终端里能完整显示。另外确认终端用的是等宽字体,否则字符宽度不一致,画面必然歪。
5. 进阶玩法:把单发烟花改成带颜色和拖尾的连发效果
基础版跑通之后,能加的东西其实不少。最值得先做的是颜色和拖尾,这两个对观感提升最大,改动量却不大。
颜色靠 ANSI 转义序列实现。在渲染每个粒子时,根据它的生命比例包一层颜色码,比如生命满用亮黄\033[93m,过半用橙\033[33m,快没了用暗红\033[31m,每行结束再\033[0m复位。注意颜色码要嵌在字符缓冲区里,不能输出完再补,否则整行都会变色。Windows 10 以后的终端默认支持 ANSI 颜色,老系统需要先开虚拟终端处理,这个在代码里加个SetConsoleMode调用就行。
拖尾的思路是保留前几帧的粒子位置。最简单的做法是给每个粒子加一个trail_x[3]、trail_y[3]环形缓冲,渲染时把历史位置也画上,字符用更暗的.。这样粒子飞过会留一条淡淡的尾迹,比单点好看很多。代价是每个粒子多几个字段,粒子池内存翻倍,但 1024 个粒子完全撑得住。
连发就是把launch_firework的触发改成随机间隔,并且允许同时存在多枚发射粒子。可以在主循环里维护一个next_launch_frame,每帧递减,归零就发射并重置成一个随机值,比如20 + rand() % 40。这样烟花出现的节奏不规律,更像真实跨年夜的此起彼伏。
参数上我一般会准备两套宏:一套「柔和型」GRAVITY 0.05 / DRAG 0.99 / 爆炸速度 0.5~0.9,一套「干脆型」GRAVITY 0.12 / DRAG 0.97 / 爆炸速度 0.7~1.2,编译前改一下就能切换风格。颜色和拖尾都加上之后,30 FPS 下画面已经相当能看,拿去当跨年背景跑一晚上也不腻。
从那以后我每次写这类粒子动画,都会先把MAX_PARTICLES、GRAVITY、DRAG三个宏单独拎出来做成配置区,改效果只动这三个数,不碰逻辑代码——这样调参不会把能跑的版本改崩。希望帮到你。
本文还有配套的精品资源,点击获取