How Water is simulated in Exipelago

This question came up here and there and I think it's an interesting thing to talk about, so let me try to put it in some words.

I always wanted the world of Exipelago fully simulated at all time which - especially on giant worlds is quite a problem due to hardware limits. Games like minecraft simply pause the simulation for blocks outside fo some radius and I didn't want that in Exipelago.

The early aproach

First versions of the game tried to run everything on the CPU. I ended up with a multithreaded, clustered approach where dynamic things would propagate across discrete parts of the world and trigger the simulation there. Back then, the Exipelago Engine still supported worlds without limits.


It turned out this approach was simply too slow and basically unsolvable on most hardware, so it was completely abandoned. This was also around the time Exipelago moved away from being "limitless" and instead became an island with a maximum size. The size is still huge, but having limits opened the door for other approaches, allowing water, light, grass, and similar simulations to run simultaneously across the entire world.

The actual approach

It became clear that this workload couldn’t be handled on CPU, so I started exploring ways to run it on GPU instead - and that’s basically what it does today. The entire dynamic world - water, grass growth, light distribution, and a few other systems - runs on GPU every frame. I use a custom texture format to pack as much data as possible into a 32-bit pixel, and the GPU runs various cellular automata (or other simulations) on this data.

For this approach to work, each pixel has to be deterministic about its neighbors - a pixel can read neighbor data but can only write to itself. That means any “movement” has to be calculated so that every pixel ends up with the same result, keeping the simulation in sync. Water is especially tricky because the flow to or from a cell depends on its neighbors - in Exipelago there’s a constant formula for flow along each edge based on its parameters.

Another challenge is that all this simulation data lives entirely on the GPU - which is fine for rendering water or lighting, since those systems just read the data to generate meshes or shading, but less ideal for game logic. Water, for example, can block paths or interact with other systems like darkness.

Exipelago isn’t just a flat world - it’s simulated in 3 dimensions. Exchanging all that data every frame is impossible, so the load is distributed - each frame only reads back a limited number of slices from the GPU. This way the data eventually synchronizes with the game logic, but not immediately. The player can actually see this when digging blocks where water is redistributing - it will “revert” to an earlier state because the game resets the simulation to the last known point while the visual simulation may be further ahead. It’s only a visual issue - it could be solved by masking bits in the simulation’s data buffer, but that’s quite involved, and since Exipelago never gained much popularity it never happened.

This simulation data buffer also does other things and I really ran out of space on those 32bits to simulate even more .. like temperature or such things .. it would've been cool to explore that tech further, but that would've been quite a rabbit-hole :)

Anyway, I hope you found this interesting to read - Exipelago became a very complex game under the hood for reasons like this, and there are many other things I may write about another time.