Roguelike Level Generation in Game Maker Studio 2
Roguelikes are all the rage in the indie game scene right now, and I know that myself and others have a plethora of great ideas they want to add to the genre. But one of the key tenets of the genre, randomly generated levels, can be tough to wrap your head around. My engine of choice is Game Maker Studio 2, and it especially lacks the features needed to make this a painless process. Good news though! I tackled it for you. Follow this guide and you’ll have a rudimentary level generation system up and running in no time.
To start, we need to outline what this level generation solution is and what it isn’t. This solution is not a noise based generation system, i.e. Minecraft or Terraria. We are instead going with the Spelunky method of level generation. You can watch this fantastic video by Mark Brown for a deeper dive into what Spelunky does well, but I’ll give you the gist. The levels are generated on a grid from designed templates. These templates ensure that the player comes across interesting curated content while also ensuring that content is different from the last time they played. We’re going to setup the barebones tools to make this possible. I will leave the individual design to you. This will be an intermediate tutorial; the setup will be guided but actually using these tools will be more difficult. Now then, down to business.
The general theory here is that we will build an empty 8x8 grid and give it information about where the player can move between the rooms. Let’s make an object called obj_initialize_new_room and give it some variables. Define the following in Variable Definitions for your object.
new_room_width = 8
new_room_height = 8
Next we need to make this object persistent. This will come in handy soon. Now let’s add some code to its create event.
room_list = []var max_width = new_room_width - 1
var max_height = new_room_height - 1for (var i = 0; i < new_room_width; i ++)
{
room_list[i] = []
for (var j = 0; j < new_room_height; j ++
{
room_list[i][j] = [false, false, false, false]
}
}
We’ve now created an array of arrays of arrays. The deepest nested arrays have four Booleans that inform us which way the player can move through the rooms. Keeping with Game Maker standards, each Bool corresponds to the following: 0 = right, 1 = up, 2 = left, 3 = down.
We’re now going to move through this array and create paths through the arrays. Below the code you already have in the create event, add the following.
sx = random(new_room_width)
sy = random(new_room_height)
do {
ex = random(new_room_width)
ey = random(new_room_height)} until ex !== sx || ey !== syvar xx = sx
var yy = sy
We’ve now defined a starter room and an ending room at random locations in the array. We’ve also given the starter coordinates to a temporary x and y set. We’re going to move this x and y to the end coordinates, opening paths along the way.
Let’s do that now, again directly below our last bit of code.
count = 4
var room_count = 0while count > 0
{
dir = floor(random(4))
//Note 1
while xx !== ex || yy !== ey
{
var px = xx
var py = yy
xx += (dir == 0) - (dir == 2)
yy += (dir == 3) - (dir == 1)
if xx > max_width
{
xx = max_width
}
if xx < 0
{
xx = 0
}
if yy > max_height
{
yy = max_height
}
if yy < 0
{
yy = 0
}//Note 2
if (xx == px and yy == py) or (random(100) < 50)
{
dir = floor(random(4))
}
else
{
if xx < ex
{
dir = 0
}
if xx > ex
{
dir = 2
}
if yy < ey
{
dir = 3
}
if yy > ey
{
dir = 1
}
}
// Note 3
var empty = true
for (var i = 0; i < 4; i ++)
{
if room_list[xx][yy][i]
{
empty = false
}
}
if empty
{
room_count ++
}
//Note 4
if xx > px
{
room_list[px][py][0] = true
room_list[xx][yy][2] = true
}
if xx < px
{
room_list[px][py][2] = true
room_list[xx][yy][0] = true
}
if yy > py
{
room_list[px][py][3] = true
room_list[xx][yy][1] = true
}
if yy < py
{
room_list[px][py][1] = true
room_list[xx][yy][3] = true
}
}
//Note 5
var empty = true
var against = []
var random_count = 0
do {
sx = floor(random(new_room_width))
sy = floor(random(new_room_height))
against = room_list[sx][sy]
if against[0] or against[1] or against[2] or against[3]
{
empty = false
}
random_count ++
} until empty = true or random_count > 20empty = true
against = []
random_count = 0
do {
ex = floor(random(new_room_width))
ey = floor(random(new_room_height))
against = room_list[ex][ey]
if against[0] or against[1] or against[2] or against[3]
{
empty = false
}
random_count ++
} until empty = false or random_count > 20xx = sx
yy = sy//Note 6
if count = 1 and room_count < (new_room_width * new_room_height) * 0.75
{
count ++
}
count -= 1
}
Ok, that was a lot. But let’s walk through this sucker using the notes I left starting at Note 1. Here we start looping until xx and yy are equal to the end_x and the end_y. We use dir to give xx and yy a direction. We also store xx and yy to px and py for later use. If xx or yy is smaller or larger than the boundaries we set, we place them back in the boundaries.
Moving on to Note 2. We’re choosing the next direction for xx and yy to go. In this case, we’re choosing it intelligently by moving it toward the end_x and end_y with a bias toward vertical movement. If you’re making a platformer, you might consider rearranging the checks so that the game biases horizontal movement. There is also a 50/50 chance that the direction will be chosen randomly, just to add a little noise. Finally, if for whatever reason, the xx and yy have not moved, the direction is chosen randomly.
Now we move to Note 3. Here we’re checking if the room we moved to is currently empty. If it is, we add one to the room count. This will come in handy later.
At Note 4 we are opening exits in each room we move through. By checking against our previous x and previous y, we can find out which exits to open in both rooms.
Next, in Note 5, we are finding a new start x and start y under the following stipulations: We want to start in an empty room and end in an non-empty room. Because these levels are generated randomly, we can’t just start and end in completely random places. The paths of the level might never cross, and you have essentially locked the player out of areas of the map.
Finally, in Note 6, we are checking how many rooms we have created. If we have created less than 75 percent of all potential rooms for the map, then we ensure we do not exit the loop by incrementing the count variable.
If you were to draw a line from the center of each non empty room to its exits, you might end up with something that looks like the following.
You can change the shape by changing some variables. For example, changing the minimum number of rooms, the minimum number of passes the generator makes, or the odds that a random direction will be picked will change how the level is generated.
Moving forward, we need to get comfortable with a few GML functions, namely layer_set_target_room() and room_add(). I recommend you read up on the documentation here and here. Essentially, these functions allow us to create a room on the fly and write information to it. We’re going to explore these concepts in the next part of this tutorial. Now that you have a randomly generated grid, you could attempt the actual level generation yourself and then see how I put mine together. More ideas are usually a good thing! If you want to jump right into a project that uses this solution, check out the alpha version of my project here. Thank you for reading and I’ll see you in the next part!