落沙游戏(Falling Sand)
由 Rust + WebAssembly 逐格模拟沙、水、火、烟的相互作用
🖱️ 拖拽 / 👆 手指 撒下所选材料 | 木头遇火会燃烧,水能灭火,沙会沉到水底
📖 原理说明
「落沙游戏」是一类经典的像素沙盒:屏幕上每个格子是一粒材料,沙子会下落堆积,水会流动摊平,火会点燃木头并化作烟。整个世界没有物理引擎,只靠每个格子根据邻居执行几条简单规则,复杂的流动、燃烧、沉淀现象就自然「涌现」出来。
🧮算法原理
下 → 斜下 → 横向每种材料按优先级依次尝试移动方向
- 沙子
- 先尝试向下,被挡住就随机选一侧斜向下滑,所以会堆成约 45° 的沙丘。沙子可以和水交换位置,因此会沉到水底。
- 水
- 和沙子一样先往下、再斜往下;都走不了时向左右流动,每帧最多流 3 格,所以一桶水倒下去会很快摊平成水面。
- 火与烟
- 火有寿命,每帧有一定概率点燃上下左右的木头;旁边还有燃料时留在原地燃烧,没有燃料就向上飘。寿命耗尽变成烟,烟会上升并逐渐消散。火旁边只要有水就立刻熄灭,变成一缕烟。
- 扫描顺序
- 每帧从下往上逐行扫描,这样下落的颗粒一帧只走一步;每一行的扫描方向左右交替,否则所有东西都会慢慢偏向一边。每个格子记录「本帧已经移动过」,防止同一颗粒在一帧里被搬两次。
🔄Rust 与 JavaScript 的分工
- JS拖拽时 JS 把两次指针事件之间的连线插值成一串点,对每个点调用
sand_paint(x, y, 半径, 材料),快速划动也不会断开。
- Rust每帧调用
sand_step(),Rust 按上面的规则更新全部 30,000 个格子。材料、寿命、颜色抖动分别存在几个紧凑的 Vec<u8> 里。
- Rust
sand_render() 按材料和每格固定的随机明暗值输出 RGBA,沙子因此呈现颗粒感。
- JSJS 把 200×150 的小图关闭平滑放大 4 倍绘制,保留像素风格;每秒读一次
sand_counts() 显示各材料的数量。
⚡性能要点
- 规则都是局部的、没有浮点运算,200×150 的世界每帧通常只需零点几毫秒,大部分时间花在绘制上。
- 随机数来自共享的 xorshift 生成器(
crates/common),种子固定,所以同样的操作每次得到同样的结果,测试可以精确复现。
- 测试里验证了几条「守恒律」:沙子和水的总量在运动中保持不变,墙永远不动,火最终会熄灭。
源码crates/games/falling-sand/src/lib.rswww/falling-sand/index.js