by Pat Scott
I'm Pat Scott. Self-touted: indie generalist, systems gourmand, and occasional procgen addict. I made Destination Ares, among other things.
In this article, I'll talk about the differences between a couple of common biome-generation techniques, also taking time to define "random" vs. "procedural". Then, I'll dive into that simple little ooze technique of mine that you're really here for.
There are a couple common methods for procedurally generating biomes in games, and they come down to the same methods used in lots of procgen across the board. I'll call them: Drunken Walk and Noise Interpretation.
Imagine the following:
You’re in a dry, grassy field somewhere. Whenever you take a step, trees and flowers erupt from the ground around you (but never in your way). Birds come to life. Everything turns green. You’re basically a magical dryad princess, and the path you take is etched into the world itself.
Except you’re drunk. Like plastered, stumbling, tunnel-vision: drunk.
Each step is in an uncontrollably random direction. Backwards, forwards, side-to-side; there’s no consistency. And, sometimes, you fall over and stop taking steps.
That’s a Drunken Walk: random movement over a given space. It’s like “baby’s first A.I.”, except it’s a recorded path instead of NPC behavior.
Now a lot of people don’t consider this true procgen, for the simple reason that it’s unreliable. There’s nothing procedural (systematic and reproducible). The result changes every time you run it. Sometimes you fall over immediately. Sometimes you run in a straight line across the field. Usually, you tend to spin in circles or something.
But you can’t change any variables (booze intake, field size, starting direction, etc.) to ensure repeatable results.
Randomness, used judiciously, is great in games. It’s also a fast-and-easy solution to a lot of problems.
But randomness has issues. (And I don’t just mean for player experience, which I talked about at one point on my personal blog)
True randomness doesn’t work for client-side multiplayer (you need an authoritative server to roll ALL randoms). If one player rolls the dice and the other player rolls their own dice, there’s no guarantee the result is the same. In fact, it probably isn’t.
This is bad if it determines where something spawns, for example. One player sees it in one spot, and the other sees it in another. The same is true if damage values are different or, say, biome generation creates divergent terrain.
It also makes it hard to recreate conditions of a given playthrough. Replays? Just got harder to program for. Sharing content with friends? Good luck getting them to have the same experience. World saving and loading? Now you have to encode every object in the game, rather than only tracking the changes from baseline.
How is this normally solved?
Now imagine you’re back in that field. This time, someone hands you a pirate’s treasure map.
On the map are a bunch of numbers telling you how many steps to take, and then arrows for which direction to turn. If you follow its directions, you’ll weave your way through the field and land at the ‘X’, no matter how many times it made you loop over previously tread ground.
And anyone who grabs the map from you can follow it using the same route and get the same result.
Noise Interpretation is a bit more abstract than this… but it’s the same idea. Often the ‘map’ is a field of static called Perlin noise (or any of a myriad of noise types). It’s represented as an image to our eyes, like a map, but the important part isn’t what it looks like — it’s the value each pixel represents.
And since each pixel is located in dimensional space, we can use its position to map directly onto our generated biome. We create hills and valleys with high and low numbers, etc.
In essence, it’s a matrix of numbers. What those numbers mean is up to the algorithms that interpret them, just as our treasure map’s numbers only make sense when the arrows are present.
And since, as a matrix, it’s just a large series of numbers, the noise can be generated countless ways. Perlin noises, as well as related noises (e.g. Simplex), are used for their reliability, simplicity, and computational efficiency. And, Perlin noise, in particular, is used because it’s a well-understood standard and isn’t bogged down by patent restrictions.
Now let’s explore a technique that is related to these concepts, but ends quite differently. The goal is to keep it simple and expressive rather than real-world accurate (though real-world accurate implementations are superrrrr cool and useful, other people are chasing that rabbit). If I see cool coastlines, interesting-but-plausible biome changes, and a good variety of sizes and shapes, I’ll be happy.
Note: this technique could be used for things beyond biome generation, like side-scroller platforming levels, political boundary and conflict maps, or even something like charts that determine the motivations and behaviors of NPCs. But we’re only going to explore basic terrestrial biome generation today.
To do this, we’re gonna make biomes using seeded numbers, without the noise fields. This means the values won’t naturally have positionality. They could — if you wanted to spread them over a matrix — but that’s not necessary where we’re going!
We need a “seed.” This is a number we’ll use in place of noise. It can be derived from countless things: the system clock time (unrepeatable), a user ID, a pre-written list, player string input, or something else entirely. Usually it’s a consistent size or length.
In my examples here, I’ve taken a pre-defined seed (using text from user input) of any size, converted it from Unicode characters to a number, and mathematically normalized it to a 32-bit int, by way of a double. (Yay modulo!)
Aside: At one point, a friend dared me to generate a map using the complete text of War and Peace as my seed. So I did. My generator handled it fine… but it broke Unity’s Editor.
Anyway, we also need mathematical transformations for our seed. Write out a bunch of ways to change one number into another number. “x + 1”, “x + 2”, “x / 2”, “x ^ 2”, whatever, ad infinitum. Even better? Somehow create these calculations procedurally. Go hog wild.
Then we apply those transformations to the seed (and probably run them through a modulo just to be safe)… et voilà! We have a series of (many) subseeds.
Earlier, I explained how many drunken walks are fairly linear: you step in a direction, then you step in another direction from that point, one at a time. This creates snaky paths without much meat or structure, but good variability.
My solution is more like a spreading ooze or a naive slime mold. You check all empty adjacent spaces, and step into some of them, based on chance (rolled by the subseed) against a decaying ratio. Then you check each direction from those... and repeat until no more steps beat the ratio.
It’ll be easier to see than to explain.
If you watch closely in the following gif, you’ll see how each space of new growth has a chance to grow into all of the unoccupied spaces around it. As the blob gets bigger (takes more steps), the chance to grow into new spaces shrinks and the blob becomes less uniform. Eventually, there will be a step where no new growth happens successfully and the blob is complete.
So this technique creates lots of misshappen blobs. What’s the big deal? Why do I like them better than snaky lines?
Because I think blobs more closely mimic earth-scale natural processes: tectonic plates, atmospheric pressure zones, plant dispersal, etc. Even the slime molds agree with me.
So, now that we’ve got a solution for creating blobs, we’ve gotta start doing it a bunch.
Note that I don’t prevent blobs from overlapping with each other. In fact, I want them to overlap! The overlaps will create emergent shapes in the geography and create smoother blending between adjacent biomes.
In this example, each blob increments or decrements a value:
Then, once all of the blobs have been calculated and all the values have been applied to the spaces, we have a map of climates, landforms, and biomes!
Here I’ve applied altitude, temperature, and wetness to each grid space’s HSV color, for some semblance of readability:
For aesthetics, I’ve offset each HSV slightly (obviously the most important step):
Then I’ve generated a subgrid, averaged by adjacencies, to blend blob edges even more. Notice the organic blending and stepping between biomes:
(Aside: what’s cool about hexes is the many varieties of shapes spaces can take, especially when they don’t have to be uniform:)
A simple, extensible technique to change up how you generate your environments.