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;
}
snow.c

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
}
snow.go

 

效果炸裂

动画

 

posted on 2015-12-27 16:51  喜欢Ⅰ  阅读(5357)  评论(0)    收藏  举报