Procedural Map Generation with Godot — Part 1
Today, we’ve decided to experiment with procedurally generated maps. There are so many approaches to generating random maps, and each has its own advantages and disadvantages. For example, if one was looking to generate a dungeon like map, with rectangular rooms all connected to each other by narrow corridors or maybe just a wall, then there exists some restrictions on the kinds of allowed shapes. Such a map can be generated by laying out randomly generated rectangles and then spreading them out until none collide. Another example, and the one we’ll be discussing in this post, is generating a less structured map with more random traversable paths. This is more suited for an outdoors map with natural paths and walls. Keep in mind that this is just one of many possible applications of what will be discussed in this series of posts, you will by no means be limited to applying the knowledge to an outdoors map or a map similar to this one. This is just an example of what we will be able to achieve! So for that, don’t be disinterested if you don’t like the design or style of the map, as the posts will be general enough to accommodate for people looking to create different types of maps. The main ideas will still be the same!
There are many intricacies to this design, as we have flower placement, dirt tiles far from walls placement, bushes, trees, houses, such that none of them overlap, traversable paths for AI while considering all the above objects and tiles, placing AI in places where they won’t be colliding with anything, etc. By the end of this series, we should have a project just like this one if not better.
The first post in this series will cover a method of generating procedural paths using random walkers. By the end of this post, you will be comfortable enough to venture into the world of procedural generation on your own, creating unique maps and generating very customized areas in your map!
Project Setup
Let’s dive right in! Create an empty project in Godot, and load in these tilesets provided by HeartBeast, which are specifically built for using Godot’s autotiler.
For more information on that, you can check out HeartBeast’s video tutorial on setting the tileset up:
After setting things up, you should end up with more or less the following scene:
Let’s create our main script on the World scene node, just for simplicity. In an actual project, you want to create a MapGenerator node and attach a script to that instead.
Procedural Generation Approach
First of all, let’s discuss the approach we’re going to take. Keep in mind that there are really many approaches to generating procedural maps, and this is just one of them. The nice thing about this approach is that it ensures that wherever we have a floor tile, we can reach that tile. Some algorithms will result in regions that are not connected to the walkable region, and so need further post-processing to remove those unwanted regions. This approach is also very simple, and uses what’s called random walkers. Random walkers are basically “agents” that take a step in every iteration, and that step is taken in a random direction. I don’t mean anything fancy by agents, just an entity that has a series of actions to perform in this world. This results in guaranteed interconnected areas, as the walkers simulate the player walking around the map. Let’s implement a basic version. First, setup a random number generator.
Now as a basic starter goal, let’s try to just place a path over our background sprite. By path, I refer to the dirt path tile provided by HeartBeast. To implement a random walker, we just have to walk in a random direction at every iteration. For the background sprite, I’m using a 1024x1024 rect with the repeating background image. The dirt tiles are 16x16 px, and this is why I set the CellSize below to that. To represent our map, we’ll be using a two dimensional array. At every position, we’ll place an enum (-1, 0, 1) corresponding to the tile at that position. We’ll have 3 tiles to begin with, empty(-1), wall (0), and floor(1). Lastly, the grid is a two dimensional array where we’ll be storing the contents at every tile. The width is the background sprite width / CellSize.x, and height is calculated similarly. This is so that we get a width and height in number of tiles that we can loop over simply. To convert between the two, we just scale by CellSize. This means that for a certain position (x, y) on this grid, that position corresponds to (x,y)*CellSize globally on the map.
We get the following from this little piece of code:
Let’s go over each part quickly:
Initializing the grid
To initialize the grid, we append empty arrays and then fill them with empties (-1).
Tilemaps in Godot
There are 3 operations that we’ll be using when it comes to tilemaps in code. Setting tiles, clearing all tiles, and updating the bitmask region for the autotiler to do its thing. In _clear_tilemaps(), we clear all tiles, and in _spawn_tiles, we loop over the grid and match whatever value we have to a tile type.
Random Walkers
For the random walker, we choose a random direction at every step, and mark that tile as a floor on the grid as we advance our walker. We also have to make sure that the tile we’re setting is inside the grid, and not out of bounds, so that is why we do the bounds check.
Note that this is the rawest we could do, with no customization, no post-processing, nothing. We already have a not-so-bad result. To make things easier, we could add the following to the _input() function so that we can generate a new map every time the space key is pressed.
Now why would we want to do more? If this were good enough, we’d stop here right? Well unfortunately, we sometimes get results like this:
In the next article, we’ll go over some solutions to this problem, and show you how easy it is to come up with your own and be creative. I honestly found that procedural generation leaves so much room for creativity, specially in the post processing step. I hope that by the end of this series, you find it as fun as I do!
About Me
I’m a backend engineer working Proton.ai, and I write about backend architecture @Softgrade and game dev on Medium.