Below is a simple Sand-game like Simulator game that I wrote over last weekend using Rust and Bevy.
Left-click to emit particles. Press s
to emit sand and w
to emit water. r
to reset the grid. On mobile, hold to spawn particle. Use two fingers and tap to get change particle spawn type.
Growing up, I remembered playing a game made from a Japanese Developer called Powder Game 2 made by Dan-Ball JP.
In it you can spawn different types of particles that each have unique properties and would interact with one another. There are basic particles like sand that drop easily and pile up. Then there were gases that would expand and go upwards. Little me was excited to see c-4 particles interact with fire particles to produce fascinating explosions!
Levels can be saved and shared among registered users. Even now the levels are saved and snapshotted by various users and produce some fascinating works / contraptions.
Here is one made by Billy G. based-off a Knight and Dragon.
The dragon's mouth have some emitters setup where fire expels from the dragon which burns the knight.
The complexity of the engine is such that users can create tensile based materials. Here is one called "Strength Block" that was one of the top rated creations since 2017 made by skyk:
Powder Game 2 really was quite an interesting type of game. Young me would attempt to build different structures like a child at a playground if said child was provided the opportunity to then ignite the sandbox with fuel and fire.
Sandspiel was a project after Powder Game 2 and by the time it was released, I had not played it as a child. However, Sandspiel was worth a mention as I found it recently on their website and found the developer's journey to writing their own clone which inspired me to go on the same path.
To my surprise, they had written the game several times and the latest edition is in Rust, one which I had also set out to pursue. I had not read their code, however, as I wanted to come in with fresh eyes and try the problem myself.
The original creators actually wrote a blog post and you can find their game here Sandspiel.
Another honorable mention: Noita was a game produced in 2020 that is an action rogue-like with a unique twist: the world is based-off a simulated pixel environment. I bought the game during early access and have enjoyed it ever since.
The game is fully of chaotic interactions and there are secrets hidden in the world. It takes the idea of simulation and procedural generation of the sand-genre to another level. There are parallel universes, secret spells, and special particle interactions waiting to discover in the world of Noita. But be warned, this game is chaotic and hard in nature!
Setting out, I wanted to write in Rust to support Web-assembly and sharpen my own Rust skills. Below are some notable highlights in my implementation.
Most online discussions and implementations seem to get into very lowlevel C++ struct. Given my weekend time constraint, I wanted to implement it as quickly as possible.
I knew early on through some research, the simulator I wanted to emulate should include some physics-esque drop and slide which would make the game feel snappier. The following is what I ultimately used to encode the pixels:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
enum PixelType {
AIR,
SAND,
WATER
}
enum PixelColor {
SANDA,
SANDB,
SANDC,
WATER,
AIR
}
pub struct PixelData {
pixel_type: PixelType,
color: PixelColor,
velocity: Vec2
}
The compactness of the struct could be tighter, however the implementation is sufficiently efficient for my use-case. Furthermore, without any chunk-ing optimizations (where we only update areas where there is movement), the game runs buttersmooth on desktop, even in the browser!
The most basic of particle-based games is akin to a cellular automata game, starting the update loop from the bottom of the screen upwards:
Water and fluids are similar however adds two more rules to this before terminating:
Notice how the movement of the particle goes from left first then right. This behavior produces a zigzag pattern where sand must first occupy and slide left-ward. We can do better.
A few updates to the rules help even out dropping behavior.
Now our hills form more naturally:
However, with just the rule above, water looks a bit off.
Notice how water flows to the right faster than the left. In this case, the update of the board is first done on the right before the left. As a result, the water flows more naturally in one direction. It is important that the global
update per-particle also switches from left to right and vice versa.
With these two updates, sand flows naturally and water updates properly.
From this exercise, I found how to exchange for loop iterators conditionally using Rust's helper libraries:
1
2
3
4
5
6
7
8
9
fn screen_update_iterator(
rev: bool,
) -> itertools::Either<impl Iterator<Item = usize>, impl Iterator<Item = usize>> {
if !rev {
itertools::Either::Left(0..WINDOW_SIZE)
} else {
itertools::Either::Right((0..WINDOW_SIZE).rev())
}
}
This iterator returns an Either
type. A special Either
is necessary here as Rust treats 0..WINDOW_SIZE.rev()
as a separate type than 0..WINDOW_SIZE
. The reason for this is how traits
are not concrete types and Iterators
are traits. You can find more information here.
To make things snappier, each pixel is provided with a velocity component to track acceleration.
The velocity idea was based-off a video I found on Youtube. Once again, I didn't look at any source code but took the concepts and ran with it.
Once velocity is introduced, a particle can travel more than one tile per tick. Similar to the above video, we performance basic linear-collision path tracing to check if a particle could move among the tiles. If so, move the tiles, otherwise, stop at the collision point. Velocity is removed if collision occurs otherwise velocity is compounded for the next frame.
This idea seems simple on paper but bugs took a while to iron out. Here's one during development:
With some fine-tweaking:
Emergent behavior begins to appear. The sand in my implementation seems to be with a certain delay that I really find to be soothing, similar to real sand. Sand would often slide at the bottom first moving along large chunks above. The behavior emerges as sand has a high slide friction with fast drop-rate.
Water has low slide-friction and high drop-rate. This meant that water can easily fly farther away from the source and ends up producing particle-like droplets.
Here's a snippet of the code on how linear movement is calculated per pixel:
1
2
3
4
5
6
7
8
9
10
let m: f32 = velocity.y / velocity.x;
let b: f32 = src_y as f32 - (m * src_x as f32);
let mut start_x: usize = src_x;
let mut start_y: usize = src_y;
let dest_x: i32 = velocity.x.abs() as i32 + 1;
for dx in 1..(dest_x.abs()) {
// We use m * dx + b to check all paths.
}
Most of the code is basic derivation of a linear equation and then checking each position of a quantized x input. Further optimizations can be done here using linear equations though the optimizations may be little compared to chunking algorithms.
Overall, the project was a fun weekend excursion. I've picked up a few Rust tricks like conditional iterators as well as apply basic linear equations on a discrete space. I am hoping to continue to iterate on this project in the future and seeing if I can add more features to it. Once more, Rust has demonstrated the agile development cycles and the ability to use WASM, its portability.
There are lots of things I wish to explore in this implementation however I ran out of time. Things like chunking (the reduction of rows to update to the top most rows of a particle body) as well as explosion dynamics. Perhaps we will leave this for next time.