Procedural Generation (Part I)


Procedural generation is something that I have wanted to play with for a long time. It is one of those things that has no hard-and-fast rules, encourages creative approaches and techniques, and allows for endless tuning and tweaking. Frankly, these are characteristics that I appreciate, but that also stretch my personality quite a bit – I like stuff to be "perfect" and "done right", which is nearly impossible in this case.

I have recorded an associated video devlog for this post. It deep-dives a bit more into the actual code aspect of the procedural generation that this post describes.

Crash Course

What is procedural generation, you ask? Wikipedia describes it as ↝

...a method of creating data algorithmically as opposed to manually...

It is a technique that is used all over the place in game development. Many major titles – like Minecraft, Terraria, and Nethack – all leverage it to create unique, endless experiences for their players. Instead of designers and developers creating game worlds by hand, procedural generation leverages mechanisms like random number generators, noise generators, and rules (e.g. "trees don't grow in deserts") to create them programmatically. The technique is not limited to the environment of a game – items, quests, character dialog, and more can all be generated procedurally.

If you want to learn more, a great article that I stumbled upon while trying to wrap my head around the concept t is Procedural Generation in Game Development by Vandrake.

Approach

In Firetale, procedural generation is going to be used for absolutely everything possible. This will give the game extreme replayability by providing new experiences, and requiring unique strategies, every single time. Everything from the game world itself, to the items and monsters that the player discovers, to the side-quests that the player encounters, will have a procedurally generated aspect to them.

The screenshots attached to this devlog show the current state of Firetale's game world generator, which I will describe below. The assets currently being used are from CuddlyClover's Fantasy Hex Tiles project. In the following sections, I will discuss each of the generation phases that currently execute to produce worlds like those that are shown.

Setting the Stage

Nowadays, I use Godot Engine for game development. Luckily, it provides a built-in implementation of the OpenSimplexNoise noise generation algorithm. It also provides other useful mechanisms, like regular random number generators. All of this combined gives a great foundation for performing procedural generation.

To get started, I went ahead and implemented a singleton "map service" class to house all of my generation code while also maintaining an in-memory data structure that holds map data. Said data structure is a simple 2-dimensional array, where the first dimension is the x-coordinates of the map, and the second dimension is the y-coordinates. The values of this 2-dimensional array are dictionaries that hold various details about the associated position in the game world. Each x and y-coordinate pair corresponds to a single hex tile in the game world.

Generation Phases

Phase 1 – Biome Generation

Biome generation was my first attempt at a procedural generation implementation in Firetale. After a lot of iteration, I ended up using three different biomes ↝ grasslands, desert, arctic. Eventually, more can be added (e.g. swamp, jungle, and so on), but these three provide a great starting point for the game world.

Using OpenSimplexNoise, I was able to generate a gradient noise map with values between -1 and 1. The idea being that each position on the noise map directly correlates to a position in the game map. The advantage of using a noise generator rather than a regular random number generator in this case was that noise values smoothly increase and decrease rather than jump around – allowing me to properly cluster different biome types together.

I then assigned each biome type a "threshold" value between -1 and 1. For each position in the noise map, if the noise value was less than a given biome's threshold value, the associated game map position was assigned to said biome.

When I first started coding this phase of generation, I actually had a fourth biome – water. This led to some interesting discoveries. If I simply gave the water biome a single threshold level, I would end up with water that only touched the biomes who's thresholds were right next to it – which was not very realistic. Further, if I gave the water biome two thresholds – for example, one lower than the grasslands biome, and another one higher than it – I would end up with grasslands surrounded by water in an almost island-like fashion.

Phase 2 – Terrain Generation

For terrain generation, I took a similar approach to biome generation. I defined a number of different terrain types – water, plains, forests, foothills, mountains, and so on. I then created lookup tables for each biome type to their supported terrain types' tilemap tiles. This later allowed me to very generically obtain an actual tile graphic for each position in the game map.

OpenSimplexNoise was once again used to create a gradient noise map, and thresholds were assigned to each terrain type. This time, the noise map was almost treated as an altitude map – mountains had a very high threshold, while water had a very low threshold. This allowed me to generate very natural-feeling terrain transitions.

Phase 3 – Town Generation

Town generation was arguably the simplest generation phase to implement. I spent almost no time tuning it before I was happy with its output.

In this phase, I iterated over each position in the game map. On each position, I used a random number generator to "roll a die". If the number that was rolled was above a certain threshold, that position would get a town.

Initially, this ended up generating a lot of towns right next to each other – which did not look very good. I ended up creating a rule that would completely bypass the rolling of the die if another town existed within a certain number of positions from the position currently being processed. This seemed to work very well.

Next Steps

There are quite a few more generation steps to implement just for Firetale's basic world generation. I want to implement a special "coastal town" generator – which will be a bit more complex than the basic town generator described above. A position difficulty generator also must be implemented – likely leveraging OpenSimplexNoise again – to assign each map position a level that will influence many of its characteristics – such as strength of enemies that can be encountered, rarity of items that can be looted, and so on.

So far, this is a very enjoyable project. I am learning a ton, and am very pleased with the progress that has already been able to be made.

Leave a comment

Log in with itch.io to leave a comment.