A strategy game of exploration, endurance, and space travel on an interstellar gothic monastery. Explore solar systems, harvest resources, construct outposts, and face hazards in the challenging universe of The Banished Vault.
[i]Today’s design diary is a technical deep dive into noise, a specific kind of random number generation. I personally had never used noise, so its implementation was a big transition point in the project. The second part is about decks, a system to produce non-linear results from a range of values.[/i]
[img]{STEAM_CLAN_IMAGE}/43803344/7c7ffcff8279f78f13ccb9e681674df0fe426df4.png[/img]
As The Banished Vault progressed from prototype and demonstration systems, a transition was necessary for the game to use Noise instead of RNG to procedurally generate its solar systems. The difference between the two is subtle but very important to make a procedural game functional. It was something I had heard of for a long time but never personally implemented. I’ll be writing as if to explain it to myself many years ago, who didn’t have a full understanding of the concepts. For a more technical explanation I would high recommend this GDC talk that goes into some detail on Noise and RNG.
The end result of Noise and RNG functions is the output of a random number. That number is then fed into the game at some point to produce a single value. When a solar system is generated, it’s configured to have somewhere between (for example) 4 and 10 planets. The random number is always produced as a float value between 0 and 1. That number rand is then interpolated between the 4 and 10, producing a value in between the two.
From an outside perspective, producing a simple random value in a game engine is easy. Simply call a function named something like Random() and a value is produced for you. This type of random number generation could be called linear or temporary: a value is produced, noted, and unrepeatable — like rolling dice. This method can be quickly implemented, but it’s hard to make a procedurally generated game with it. Generating large amounts of random values, like for complex terrain, becomes functionally impossible from a memory and code point of view. It’s very hard to save and load data, because every random value needs to be manually stored and retrieved. Debugging a game requires situations to be reproduced, also another impossibility.
[h3]Noise[/h3]
Imagine standing in a vast grid of random numbers that are already generated. Instead of rolling the dice, you simply move to the next dice and look at the value that was already rolled. This is noise. The values are random, but any given value can be reaccessed whenever you need. To produce a random value you simply pick a spot on this field and access the value. Because the random values are known already, this makes them repeatable.
With noise you start with a value called a seed, and supply a position. A noise function takes a seed value, a position value, and does some math to output a random value. If the game needs to re-access that random value, it inputs the same seed and position, and the function produces the same random value. In this way we can can save and load an entire solar system in The Banished Vault with just a seed value and how many solar systems (the position) into the seed. Examples of noise functions can be seen in the talk linked above.
How this works in game is straightforward, but requires a degree of planning. Each solar system has its own seed. From then, we assign fixed positions to access the random numbers we want. The number of planets is position 0, the color of the sun position 19. Each planet is then given it’s own seed, based in part on the seed of the solar system. The planet’s seed functions the same way, with positions assigned for each random number want: number of moons, mass, atmosphere, which resources it has, and so on.
In this way with noise, the exact same solar system can be reproduced without issue. For testing its perfect, as a bug can be easily recreated, or playtesting can all be done on the same solar system. It took a while for me to understand and implement noise but its absolutely essential for a game like this.
[h3]Choosing a Value
[/h3]Now that we have our random numbers being generated, we want to use it to choose a value to generate a solar system with. Let’s expand on the example above, producing a solar system with a number of planets between 4 and 10. Interpolating a random value will produce an even distribution of values between those two limits. Frequently we don’t want that at all, but what is the clearest way to overcome that? There are many ways to achieve this, but I’ll only go through the ones I briefly tried and the current method I ended up with.
If we’re trying to replicate solar systems that look like ours, most of the time we’d like a number of planets between 7 and 9, only rarely seeing something as low as 4 or 5. We still want the total range to be 4 and 10, but we want the distribution to be uneven.
There’s many ways to achieve an uneven distribution. You can take the random number generated and bend it to a curve, such as a exponential, logarithmic, arch, smoothstep, etc. I don’t like this method because it requires a lot of cross-talk between code and data, and additional information such as the bounds of the number and the type of curve you want to apply to it. And it doesn’t easily allow for funky shapes, like a peak at the 7–9 values and extremely low chance of 4 and 10. With many points of failure it’s possible to introduce bugs or data errors.
Alternatively, you can define your own custom curves in the engine, manually drawing your own peak at 7–9 and trough at 4 and 10. This works and condenses the range to variable, but counterintuitively for me is hard to visualize. Seeing a graph of the probabilities is a good reference, but editing the probability with a graph is harder. Suppose I want to have an equal chance of the range to produce 7 as I do 4,5,6,8,9,10. It’s possible to do this on a custom curve, but it’s not trivial.
You can have a value and weighted chance, such as 7,0.2 which means there’s a 20% chance to produce a 7. This method is clear on the probability, but requires the weights to add up to 1, or a remapping in the back end to make sure there’s no overflow. This also adds more classes to code and extra work for verification.
[h3]Decks
[/h3]The method used in The Banished Vault I call a deck*. A deck is simply a collection of whatever type of value I’m randomly wanting to pick from, with whatever distribution I’d like. If I want a perfectly even distribution of values, each value is represented once in the deck — 4,5,6,7,8,9,10. If I want my funky curve, I might go 4,5,6,6,7,7,7,7,8,8,8,8,9,9,10. The random number picker will still evenly pick between any options in this list, but because the list itself is uneven, it’s more likely to pick a value that has more duplicate entries. To assemble the example above, the list could look like 4,5,6,8,9,10,7,7,7,7,7,7, and now 7 has a 50% chance and every other number is an equal slice of the other 50%. To adjust the weighting I just add or remove an entry, and can still have a rough idea of how much each value is weighted against another. Frequently I don’t need to know the exact probability of any given value, just roughly what the distribution looks like. There’s less chance for bugs or errors, since I know any entry in the deck has some chance of appearing in game. Additionally its just a list of values, which requires no additional classes or verification work to choose values correctly.
This deck method has proved extremely useful, and is used all over the game. For resource amounts on planets, I don’t need to specify that some resources have a spawn chance of zero, they simply aren’t in the list. I can have big gaps in data, such as a solar system 0 or 5 asteroid fields. Because a deck is just a list of any data item, you aren’t limited to numbers or resources. I use decks for colors, gradients, textures, and all sorts. The progression of solar systems is itself a deck of multiple decks.
[i]*In strict game design terms, it’s not a deck. A deck’s probability changes as entries are pulled from it (once the Ace of Clubs is drawn it can’t be re-drawn), so in these terms this system is actually a multi-sided die with non-linear and repeatable results. But dice are already in the game and it’s possible this system could be used to reflect deck behavior, so that’s the name it got.[/i]