C 和 Go 下雪 snow demo

在 2015 年底那会刚好下雪时候, 就想起大学一位对我非常好老师 C 语言老师. 想用她教的知识写一个下雪代码送给她.
当现在2024年底, 重新翻修下用于感谢老师的照顾帮助.
之前老版本很粗糙, 主要 window 和 linux 都能跑, 动画 采用 1s 40 帧, 雪花具有 x 轴速度和 y 轴速度.
如果在网上搜 C语言下雪相关的 很多抄袭我写的老版本, 这里不再提了, 这次写一个简单新玩具, 具备简单物理引擎, 例如重力, 风对雪花影响.
准备环境, 最好 Linux Ubuntu C2X
如果是 Window 推荐使用最新 Window 版本, 开启 WSL 安装 Ubuntu 子系统, 配置下 VSCode 远端开发环境也完全可以.
好的那我们开始吧.

安装 libncurses-dev 包
sudo apt update sudo apt install libncurses-dev
ncurses (new curses)是一套编程库,它提供了一系列的函数以便使用者调用它们去生成基于文本的用户界面。
这里下雪页面绘制就是基于这个 GNU GUI 库二次开发
#include <time.h> #include <stdlib.h> #include <unistd.h> #include <ncurses.h> // 每行最大雪花密度(自适应屏幕宽度的比例) #define MAX_SNOWFLAKE_DENSITY_PER_ROW 0.06 // 微秒延迟,控制帧率 #define FRAME_DELAY 160000 // 重力加速度 #define GRAVITY 0.04 // 风力强度 #define WIND 0.4 struct snowflake { // 雪花位置(浮点数支持平滑移动) float x, y; // 垂直速度 float velocity_y; // 水平速度 float velocity_x; // 雪花字符 char shape; // 是否激活(是否在屏幕中出现) bool active; }; struct snowfall { // 雪花数组 struct snowflake * snowflakes; // 最大雪花数量 int max_snowflake_count; // 第一行 top 行最大雪花数量 int max_snowflakes_top_row; // 屏幕宽度 int screen_width; // 屏幕高度 int screen_height; }; // 找到空闲没有激活的雪花对象, 生成一个新的雪花, 并放置在屏幕第一行处 void snowflake_spawn(struct snowfall * sl) { for (int i = 0; i < sl->max_snowflake_count; i++) { if (!sl->snowflakes[i].active) { // 随机水平位置 sl->snowflakes[i].x = rand() % sl->screen_width; // 从顶部出现 sl->snowflakes[i].y = 0; // 随机垂直速度 & 拍脑门微小随机量 sl->snowflakes[i].velocity_y = GRAVITY + (rand() % 50) / 100.0; // 随机水平风速 & 拍脑门微小随机量 sl->snowflakes[i].velocity_x = WIND * ((rand() % 200) / 100.0 - 1); // 随机选择雪花形状 static const char shapes[] = {'*', 'o', '*', '.', '*', '+', '*'}; sl->snowflakes[i].shape = shapes[rand() % (sizeof (shapes) / sizeof (*shapes))]; // 激活雪花 sl->snowflakes[i].active = true; // 一次只生成一个雪花 break; } } } // 更新雪花位置 void snowflakes_update(struct snowfall * sl) { int top_row_count = 0; for (int i = 0; i < sl->max_snowflake_count; i++) { if (sl->snowflakes[i].active) { // 更新垂直位置 sl->snowflakes[i].y += sl->snowflakes[i].velocity_y; // 更新水平位置 sl->snowflakes[i].x += sl->snowflakes[i].velocity_x; if ((int)sl->snowflakes[i].y == 0) { top_row_count++; } // 如果雪花超出屏幕范围,取消激活 if (sl->snowflakes[i].y >= sl->screen_height || sl->snowflakes[i].x < 0 || sl->snowflakes[i].x >= sl->screen_width) { sl->snowflakes[i].active = false; } } } // 确保第一行生成的雪花数量不超过限制, 但也不会太少 for (int i = top_row_count; i < sl->max_snowflakes_top_row; i++) { snowflake_spawn(sl); i += rand() % 2; } } // 绘制雪花 void snowflakes_draw(struct snowfall * sl) { // 清屏 clear(); for (int i = 0; i < sl->max_snowflake_count; i++) { if (sl->snowflakes[i].active) { // 绘制雪花 mvaddch((int)sl->snowflakes[i].y, (int)sl->snowflakes[i].x, sl->snowflakes[i].shape); } } // 刷新屏幕 refresh(); } /* * gcc -g -Wall -O2 -Wextra -o snow snow.c -lncurses & ./snow */ int main() { // 初始化随机数种子 srand((unsigned)time(NULL)); // 初始化 ncurses initscr(); // 隐藏光标 curs_set(0); // 非阻塞模式 nodelay(stdscr, true); // 禁用回显 noecho(); // 初始化屏幕尺寸和雪花数量 struct snowfall sl; getmaxyx(stdscr, sl.screen_height, sl.screen_width); // 计算每行最大雪花数量 sl.max_snowflakes_top_row = sl.screen_width * MAX_SNOWFLAKE_DENSITY_PER_ROW; // 计算总雪花数量 sl.max_snowflake_count = sl.max_snowflakes_top_row * sl.screen_height; // 分配雪花数组的内存 & 初始化内存 sl.snowflakes = calloc( sl.max_snowflake_count, sizeof(struct snowflake)); if (sl.snowflakes == NULL) { endwin(); fprintf(stderr, "Memory allocation failed\n"); return -1; } // 按 'q' 退出 while (getch() != 'q') { // 更新雪花位置 snowflakes_update(&sl); // 绘制雪花 snowflakes_draw(&sl); // 延迟模拟帧率 usleep(FRAME_DELAY); } // 释放雪花数组的内存 free(sl.snowflakes); // 恢复终端设置 endwin(); return 0; }
Build & Run
gcc -g -Wall -O2 -Wextra -o snow snow.c -lncurses ./snow
这个冬天,雪花很美,(。⌒∇⌒)

后记 - 2026年4月9日
利用 AI 能力, 写了一个新版本, 这里用的是个 go build -buildvcs=true -o snow.exe snow.go
package main import ( "flag" "fmt" "math" rand "math/rand/v2" "os" "time" tcell "github.com/gdamore/tcell/v3" ) const ( frameDelay = 33 * time.Millisecond minWidth = 70 minHeight = 24 ) type star struct { x int y int phase float64 } type tree struct { x int baseY int height int } type flake struct { x float64 y float64 vx float64 vy float64 phase float64 layer int glyph rune bright bool } type scene struct { screen tcell.Screen width int height int tick int gust float64 gustTarget float64 stars []star trees []tree farRidge []int midRidge []int ground []int drift []float64 driftBuf []float64 flakes []flake } func main() { smoke := flag.Bool("smoke", false, "run headless simulation") frames := flag.Int("frames", 240, "frames to simulate in smoke mode") flag.Parse() if *smoke { os.Exit(runSmoke(*frames)) } screen, err := tcell.NewScreen() if err != nil { fmt.Fprintf(os.Stderr, "create screen failed: %v\n", err) os.Exit(1) } if err := screen.Init(); err != nil { fmt.Fprintf(os.Stderr, "init screen failed: %v\n", err) os.Exit(1) } defer screen.Fini() screen.SetStyle(tcell.StyleDefault.Background(tcell.ColorBlack).Foreground(tcell.ColorWhite)) app := newScene(screen) app.run() } func runSmoke(frames int) int { app := newScene(nil) app.resize(100, 34) for range frames { app.update() } total := 0.0 peak := 0.0 for _, v := range app.drift { total += v if v > peak { peak = v } } fmt.Printf("frames=%d flakes=%d drift_total=%.2f drift_peak=%.2f gust=%.2f\n", frames, len(app.flakes), total, peak, app.gust) return 0 } func newScene(screen tcell.Screen) *scene { app := &scene{ screen: screen, } if screen != nil { w, h := screen.Size() app.resize(w, h) } return app } func (s *scene) run() { ticker := time.NewTicker(frameDelay) defer ticker.Stop() s.draw() for { select { case ev := <-s.screen.EventQ(): switch e := ev.(type) { case *tcell.EventResize: w, h := s.screen.Size() s.resize(w, h) s.screen.Sync() s.draw() case *tcell.EventKey: switch e.Key() { case tcell.KeyCtrlC, tcell.KeyEscape: return } switch e.Str() { case "q", "Q": return case "r", "R": w, h := s.screen.Size() s.resize(w, h) } } case <-ticker.C: s.update() s.draw() } } } func (s *scene) resize(w int, h int) { s.width = w s.height = h s.tick = 0 s.gust = 0 s.gustTarget = 0 s.buildLandscape() s.buildStars() s.buildTrees() s.seedDrift() s.seedFlakes() } func (s *scene) buildLandscape() { s.farRidge = make([]int, s.width) s.midRidge = make([]int, s.width) s.ground = make([]int, s.width) for x := 0; x < s.width; x++ { xp := float64(x) far := int(float64(s.height)*0.36 + math.Sin(xp*0.055)*2.6 + math.Sin(xp*0.019+1.7)*1.8 + math.Sin(xp*0.008+4.3)*2.4) mid := int(float64(s.height)*0.56 + math.Sin(xp*0.032+0.8)*2.2 + math.Sin(xp*0.011+2.1)*1.5) ground := int(float64(s.height)*0.78 + math.Sin(xp*0.05+0.6)*1.6 + math.Sin(xp*0.017+2.7)*1.3) if far < 4 { far = 4 } if mid < far+2 { mid = far + 2 } if ground < mid+3 { ground = mid + 3 } if ground > s.height-4 { ground = s.height - 4 } s.farRidge[x] = far s.midRidge[x] = mid s.ground[x] = ground } } func (s *scene) buildStars() { count := max(24, s.width/3) s.stars = make([]star, count) for i := range s.stars { s.stars[i] = star{ x: rand.IntN(s.width), y: rand.IntN(max(4, s.height/3)), phase: rand.Float64() * math.Pi * 2, } } } func (s *scene) buildTrees() { count := min(max(s.width/18, 4), 9) s.trees = make([]tree, count) step := float64(s.width) / float64(count+1) for i := range s.trees { x := int(float64(i+1)*step) + rand.IntN(5) - 2 x = min(max(x, 4), s.width-5) baseY := s.ground[x] - 1 height := 5 + rand.IntN(5) s.trees[i] = tree{ x: x, baseY: baseY, height: height, } } } func (s *scene) seedDrift() { s.drift = make([]float64, s.width) s.driftBuf = make([]float64, s.width) for x := 0; x < s.width; x++ { base := 0.6 + 0.4*math.Sin(float64(x)*0.11+0.7) + 0.25*math.Sin(float64(x)*0.031+2.2) s.drift[x] = max(base, 0) } } func (s *scene) seedFlakes() { target := max(140, s.width*s.height/16) s.flakes = make([]flake, target) for i := range s.flakes { s.flakes[i] = s.newFlake(true) } } func (s *scene) newFlake(anyY bool) flake { layerRoll := rand.Float64() layer := 0 switch { case layerRoll > 0.78: layer = 2 case layerRoll > 0.38: layer = 1 } y := -rand.Float64() * 4 if anyY { y = rand.Float64()*float64(s.height) - 2 } f := flake{ x: rand.Float64() * float64(s.width), y: y, phase: rand.Float64() * math.Pi * 2, layer: layer, } switch layer { case 0: f.vx = rand.Float64()*0.05 - 0.025 f.vy = 0.14 + rand.Float64()*0.06 f.glyph = '.' case 1: f.vx = rand.Float64()*0.08 - 0.04 f.vy = 0.21 + rand.Float64()*0.09 f.glyph = '+' case 2: f.vx = rand.Float64()*0.12 - 0.06 f.vy = 0.32 + rand.Float64()*0.14 if rand.IntN(3) == 0 { f.glyph = 'o' } else { f.glyph = '*' } f.bright = true } return f } func (s *scene) update() { s.tick++ if s.tick%90 == 0 { s.gustTarget = rand.Float64()*0.9 - 0.45 } s.gust += (s.gustTarget - s.gust) * 0.025 for i := range s.flakes { s.updateFlake(&s.flakes[i]) } if s.tick%3 == 0 { s.smoothDrift() } } func (s *scene) updateFlake(f *flake) { layerFactor := [3]float64{0.38, 0.72, 1.15}[f.layer] wave := math.Sin(float64(s.tick)*0.05+f.phase+f.y*0.08) * 0.05 f.x += f.vx + s.gust*layerFactor + wave f.y += f.vy if f.x < -2 { f.x += float64(s.width) + 4 } if f.x > float64(s.width)+2 { f.x -= float64(s.width) + 4 } if f.y < 0 { return } xi := min(max(int(math.Round(f.x)), 0), s.width-1) snowTop := s.snowTop(xi) if int(math.Round(f.y)) >= snowTop { s.depositSnow(xi, f.layer) *f = s.newFlake(false) return } if f.y > float64(s.height)+1 { *f = s.newFlake(false) } } func (s *scene) depositSnow(x int, layer int) { add := 0.16 + float64(layer)*0.16 + rand.Float64()*0.05 if s.gust > 0.24 && x < s.width-1 { x++ } if s.gust < -0.24 && x > 0 { x-- } s.drift[x] = min(s.drift[x]+add, 7.8) } func (s *scene) smoothDrift() { copy(s.driftBuf, s.drift) for x := 1; x < len(s.drift)-1; x++ { avg := (s.drift[x-1] + s.drift[x]*2 + s.drift[x+1]) / 4 s.driftBuf[x] = s.drift[x]*0.84 + avg*0.16 if s.gust > 0.18 { s.driftBuf[x] += (s.drift[x-1] - s.drift[x+1]) * 0.02 } if s.gust < -0.18 { s.driftBuf[x] += (s.drift[x+1] - s.drift[x-1]) * 0.02 } s.driftBuf[x] = max(s.driftBuf[x], 0.2) } s.drift, s.driftBuf = s.driftBuf, s.drift } func (s *scene) snowTop(x int) int { top := s.ground[x] - int(math.Round(s.drift[x])) return max(top, s.midRidge[x]+2) } func (s *scene) draw() { if s.screen == nil { return } s.screen.Clear() w, h := s.screen.Size() if w < minWidth || h < minHeight { s.drawSmallPrompt(w, h) s.screen.Show() return } s.drawSky() s.drawMoon() s.drawStars() s.drawMountains() s.drawTrees() s.drawGround() s.drawFlakes() s.drawHud() s.screen.Show() } func (s *scene) drawSmallPrompt(w int, h int) { style := tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.ColorBlack) s.drawCentered(w, h/2-1, style.Bold(true), "窗口太小") s.drawCentered(w, h/2+1, style, "至少需要 70x24") } func (s *scene) drawSky() { top := [3]int{5, 12, 24} bottom := [3]int{34, 50, 72} for y := 0; y < s.height; y++ { ratio := float64(y) / float64(max(1, s.height-1)) r := mix(top[0], bottom[0], ratio) g := mix(top[1], bottom[1], ratio) b := mix(top[2], bottom[2], ratio) style := tcell.StyleDefault.Background(tcell.NewRGBColor(int32(r), int32(g), int32(b))) for x := 0; x < s.width; x++ { s.screen.SetContent(x, y, ' ', nil, style) } } } func (s *scene) drawMoon() { cx := int(float64(s.width) * 0.77) cy := max(4, s.height/6) halo := 7 for dy := -halo; dy <= halo; dy++ { for dx := -halo; dx <= halo; dx++ { dist := math.Sqrt(float64(dx*dx + dy*dy)) x := cx + dx y := cy + dy if x < 0 || x >= s.width || y < 0 || y >= s.height { continue } if dist > float64(halo) { continue } light := 1.0 - dist/float64(halo) bg := tcell.NewRGBColor( int32(46+light*28), int32(55+light*26), int32(78+light*16), ) style := tcell.StyleDefault.Background(bg) if dist < 3.2 { style = style.Foreground(tcell.ColorWhite) s.screen.SetContent(x, y, 'o', nil, style.Bold(true)) continue } s.screen.SetContent(x, y, ' ', nil, style) } } } func (s *scene) drawStars() { for _, star := range s.stars { twinkle := math.Sin(float64(s.tick)*0.05 + star.phase) style := tcell.StyleDefault.Foreground(tcell.NewRGBColor(195, 205, 220)) ch := '.' if twinkle > 0.65 { ch = '*' style = style.Bold(true) } s.screen.SetContent(star.x, star.y, ch, nil, style) } } func (s *scene) drawMountains() { farStyle := tcell.StyleDefault.Background(tcell.NewRGBColor(20, 32, 50)) midStyle := tcell.StyleDefault.Background(tcell.NewRGBColor(26, 41, 60)) crestStyle := tcell.StyleDefault.Foreground(tcell.NewRGBColor(140, 155, 174)).Background(tcell.NewRGBColor(26, 41, 60)) for x := 0; x < s.width; x++ { for y := s.farRidge[x]; y < s.height; y++ { s.screen.SetContent(x, y, ' ', nil, farStyle) } for y := s.midRidge[x]; y < s.height; y++ { s.screen.SetContent(x, y, ' ', nil, midStyle) } s.screen.SetContent(x, s.midRidge[x], '^', nil, crestStyle) } } func (s *scene) drawTrees() { leaf := tcell.StyleDefault.Foreground(tcell.NewRGBColor(26, 72, 54)).Background(tcell.NewRGBColor(26, 41, 60)) snow := tcell.StyleDefault.Foreground(tcell.NewRGBColor(232, 238, 243)).Background(tcell.NewRGBColor(26, 41, 60)) trunk := tcell.StyleDefault.Foreground(tcell.NewRGBColor(78, 58, 34)).Background(tcell.NewRGBColor(26, 41, 60)) for _, tree := range s.trees { for i := 0; i < tree.height; i++ { rowY := tree.baseY - 1 - i if rowY < 0 || rowY >= s.height { continue } span := 1 + i/2 for dx := -span; dx <= span; dx++ { x := tree.x + dx if x < 0 || x >= s.width { continue } ch := '^' style := leaf if i%2 == 0 && abs(dx) == span { if dx < 0 { ch = '/' } else { ch = '\\' } } if i%3 == 0 && abs(dx) == span-1 { ch = '.' style = snow } s.screen.SetContent(x, rowY, ch, nil, style) } } for y := tree.baseY - 1; y <= tree.baseY; y++ { if y < 0 || y >= s.height { continue } s.screen.SetContent(tree.x, y, '|', nil, trunk) } } } func (s *scene) drawGround() { groundStyle := tcell.StyleDefault.Background(tcell.NewRGBColor(16, 28, 42)) snowTopStyle := tcell.StyleDefault.Foreground(tcell.NewRGBColor(240, 244, 248)).Background(tcell.NewRGBColor(36, 52, 74)) snowFill := tcell.StyleDefault.Background(tcell.NewRGBColor(58, 76, 101)) for x := 0; x < s.width; x++ { top := s.snowTop(x) for y := top; y < s.height; y++ { style := groundStyle ch := ' ' if y == top { style = snowTopStyle ch = '_' } else if y < s.ground[x] { style = snowFill } s.screen.SetContent(x, y, ch, nil, style) } } } func (s *scene) drawFlakes() { for _, f := range s.flakes { x := int(math.Round(f.x)) y := int(math.Round(f.y)) if x < 0 || x >= s.width || y < 0 || y >= s.height { continue } style := tcell.StyleDefault.Foreground(tcell.NewRGBColor(200, 210, 220)) switch f.layer { case 1: style = tcell.StyleDefault.Foreground(tcell.NewRGBColor(226, 232, 238)) case 2: style = tcell.StyleDefault.Foreground(tcell.NewRGBColor(250, 252, 255)) } if f.bright { style = style.Bold(true) } s.screen.SetContent(x, y, f.glyph, nil, style) } } func (s *scene) drawHud() { titleStyle := tcell.StyleDefault.Foreground(tcell.NewRGBColor(228, 234, 240)).Bold(true) infoStyle := tcell.StyleDefault.Foreground(tcell.NewRGBColor(184, 196, 208)) windText := "风很轻" switch { case s.gust > 0.28: windText = "风向右吹" case s.gust < -0.28: windText = "风向左吹" case s.gust > 0.1: windText = "微风向右" case s.gust < -0.1: windText = "微风向左" } s.drawText(2, 1, titleStyle, "SNOW") s.drawText(2, 2, infoStyle, "自然雪夜特效") s.drawText(2, 3, infoStyle, windText) help := "Q/Esc 退出 R 重置" s.drawText(s.width-len(help)-2, 1, infoStyle, help) } func (s *scene) drawText(x int, y int, style tcell.Style, text string) { for i, r := range text { if x+i < 0 || x+i >= s.width || y < 0 || y >= s.height { continue } s.screen.SetContent(x+i, y, r, nil, style) } } func (s *scene) drawCentered(w int, y int, style tcell.Style, text string) { x := max((w-len([]rune(text)))/2, 0) s.drawText(x, y, style, text) } func mix(a int, b int, t float64) int { return int(float64(a) + (float64(b)-float64(a))*t) } func abs(v int) int { if v < 0 { return -v } return v }
效果炸裂

这几天北京下起了雪,感觉挺美. 外加上公司前端也在项目中引入了下雪的特效. 特别好看.
用的是opengl 绘制. 做为后端也想写一个. 写一个下雪情况表达对 自然的热爱. 主要是用的是C语言,
基于黑窗口命令行.
浙公网安备 33010602011771号