落沙游戏(Falling Sand)

由 Rust + WebAssembly 逐格模拟沙、水、火、烟的相互作用

🖱️ 拖拽 / 👆 手指 撒下所选材料  |  木头遇火会燃烧,水能灭火,沙会沉到水底

📖 原理说明

「落沙游戏」是一类经典的像素沙盒:屏幕上每个格子是一粒材料,沙子会下落堆积,水会流动摊平,火会点燃木头并化作烟。整个世界没有物理引擎,只靠每个格子根据邻居执行几条简单规则,复杂的流动、燃烧、沉淀现象就自然「涌现」出来。

🧮算法原理

下 → 斜下 → 横向每种材料按优先级依次尝试移动方向
沙子
先尝试向下,被挡住就随机选一侧斜向下滑,所以会堆成约 45° 的沙丘。沙子可以和水交换位置,因此会沉到水底。
水
和沙子一样先往下、再斜往下;都走不了时向左右流动,每帧最多流 3 格,所以一桶水倒下去会很快摊平成水面。
火与烟
火有寿命,每帧有一定概率点燃上下左右的木头;旁边还有燃料时留在原地燃烧,没有燃料就向上飘。寿命耗尽变成烟,烟会上升并逐渐消散。火旁边只要有水就立刻熄灭,变成一缕烟。
扫描顺序
每帧从下往上逐行扫描,这样下落的颗粒一帧只走一步;每一行的扫描方向左右交替,否则所有东西都会慢慢偏向一边。每个格子记录「本帧已经移动过」,防止同一颗粒在一帧里被搬两次。

🔄Rust 与 JavaScript 的分工

  1. JS拖拽时 JS 把两次指针事件之间的连线插值成一串点,对每个点调用 sand_paint(x, y, 半径, 材料),快速划动也不会断开。
  2. Rust每帧调用 sand_step(),Rust 按上面的规则更新全部 30,000 个格子。材料、寿命、颜色抖动分别存在几个紧凑的 Vec<u8> 里。
  3. Rustsand_render() 按材料和每格固定的随机明暗值输出 RGBA,沙子因此呈现颗粒感。
  4. JSJS 把 200×150 的小图关闭平滑放大 4 倍绘制,保留像素风格;每秒读一次 sand_counts() 显示各材料的数量。

⚡性能要点

  • 规则都是局部的、没有浮点运算,200×150 的世界每帧通常只需零点几毫秒,大部分时间花在绘制上。
  • 随机数来自共享的 xorshift 生成器(crates/common),种子固定,所以同样的操作每次得到同样的结果,测试可以精确复现。
  • 测试里验证了几条「守恒律」:沙子和水的总量在运动中保持不变,墙永远不动,火最终会熄灭。

源码crates/games/falling-sand/src/lib.rswww/falling-sand/index.js