Overview
The system can generate a broad variety of types of caves that combine further into complex cave systems.
- Written in: Unity / C#
- Development period: Sporadic development over a period of 3 weeks
- Team size: Solo Project
- Source repository
This page is WIP. Below is an early draft text without images. The source code is fully complete however, you can check that out.
I made seeded procedural world gen for a 2D world viewed from the side. I use a 2D array of single byte-sized enums to store the different types of blocks during generation, only when generation is done do I convert it to Unity's tilemap to connect it with the engine game world. Having the generation separate from the engine (besides the tweakable generation parameter values being Unity serialized fields, of course) until the last step lets me very easily switch what the generated world is used for. I made it export as a .png image which you see above, since screenshotting the Unity editor or a build would lose the transparancy. For a larger world where having everything in a single tilemap that is fully loaded at all times would be too performance intensive, I can split the generated world into chunks and save each one as a file, so that you can have only the chunks nearby the player be loaded.
The actual generation is done through an array of steps that can individially be disabled or reordered without a single change in the code. Each step takes the current 2D array and the seed and then modifies the array in some way.
First the elevation curve is made, I used OpenSimplexS2 for the noise, very similar to the more known Perlin noise, the most notable difference being that it gives values between -1 and +1 instead of between 0 and +1 (so not a big difference). To allow for wider flatter areas while still keeping the hills tall, I square the absolute value of the noise, which gives flatter flats and steeper hills. Using cubing or an even higher power would give even starker variations if deemed necessary.
I then added some less vertically scaled but more zoomed out noise to give the ground some roughness. The exact thickness of the dirt layer has similar noise applied to make it not uniformly deep. Then grass and oceans are added but they don't use any randomness so I'll skip them to get to the most interesting part, the caves, check the source code if you're curious about them.
To get the path of the caves I use a modified 8-directional random walk. This random walk has a heavy bias toward keeping the same direction, and when it changes direction it can only turn 45 degrees. Going straigh up is always banned and there adjustable biases against going diagonally up or for continuing sideways. You can see some caves go down quite steeply, some are more sideways oriented, and the thinnest ones have no bias toward any direction (besides the straight up ban) so they wiggle randomly.
Each cave type has it's own values for average radii and max random variation from average. The random variation from the average is determined by OpenSimplex noise, with the left and right walls of the caves using different parts of the noise so they're not symmetric. Making the walls this way, by determining a distance from a center line, has a problem in a discrete space like an int grid because the outer walls won't connect in many cases.
This is fixed by using an implementation of Bresenham's line algorithm to connect the blocks on a wall.
The generation process of each cave is fully ignorant of where the paths of other caves go (besides surface caves which are forbidden from spawning to close to each other) so they naturally cross over each other all the time. I've also made a system of cave branching which looks like this:
This is done by having a random chance every 70 or so blocks (or whatever the parameter is set to) to add a branch cave, if the chance falls true, then a new cave (with it's max length reduced by how far the current cave has already gone) which starts at the current location of the cave walk with a perpendicular initial direction, is added to the cave queue. A restriction is also added to the current generating cave and the new one which applies for some amout of steps. The restriction is that can't turn to a direction that is more than 45 degrees from the direction they had at the split. This is to ensure the split caves don't turn toward each other and rejoin right away.
Determining the position of a wall block when the center line is going diagonally has some interesting quirks, of course you have to divide the generated radius value by sqrt(2) before rounding to an int to prevent caves from being inherently wider when diagonal, but there's also the problem that locations perpendicular to a diagonal line exclude half of all grid positions, like how biships in chess can only to to their own color, unlike rooks (cardinal direction cave walks) which can reach all squares. This leads to a radius variation of one for a single block looking like this:
Which after being filled in with Bresenham's line algorithm looks like this:
This is an unnecessarily large variation, and a diagonal cave can be made more smooth by using the grid locations not directly perpendicular to squares on the walk. I solved it by rounding to the nearst half number, and if there's a .5, I put the square on a spot inaccesible to the bishop.