BSP Room Dungeons
About this tutorial
This tutorial is free and open source, and all code uses the MIT license - so you are free to do with it as you like. My hope is that you will enjoy the tutorial, and make great games!
If you enjoy this and would like me to keep writing, please consider supporting my Patreon.
A popular method of map generation uses "binary space partition" to sub-divide your map into rectangles of varying size, and then link the resulting rooms together into corridors. You can go a long way with this method: Nethack uses it extensively, Dungeon Crawl: Stone Soup uses it sometimes, and my project - One Knight in the Dungeon - uses it for sewer levels. This chapter will use the visualizer from the previous chapter to walk you through using this technique.
Implementing a new map - subdivided BSP, the boilerplate
We'll start by making a new file in map_builders
- bsp_dungeon.rs
. We start by making the basic BspDungeonBuilder
struct:
This is basically the same as the one from SimpleMapBuilder
- and we've kept the rooms
vector, because this method uses a concept of rooms as well. We've added a rects
vector: the algorithm uses this a lot, so it's helpful to make it available throughout the implementation. We'll see why it's needed shortly.
Now we implement the MapBuilder
trait to the type:
This is also pretty much the same as SimpleMapBuilder
, but build_map
has a comment reminding us to write some code. If you ran the generator right now, you'd get a solid blob of walls - and no content whatsoever.
We also need to implement a constructor for BspMapBuilder
. Once again, it's basically the same as SimpleMapBuilder
:
Lastly, we'll open map_builders/mod.rs
and change the random_builder
function to always return our new map type:
Once again, this isn't in the slightest bit random - but it's far easier to develop a feature that always runs, rather than keeping trying until it picks the one we want to debug!
Building the map creator
We'll worry about swapping out map types later. Onto making the map! Note that this implementation is ported from my C++ game, One Knight in the Dungeon. We'll start with room generation. Inside our impl BspMapBuilder
, we add a new function:
So what on Earth does this do?
- We clear the
rects
structure we created as part of the builder. This will be used to store rectangles derived from the overall map. - We create the "first room" - which is really the whole map. We've trimmed a bit to add some padding to the sides of the map.
- We call
add_subrects
, passing it the rectangle list - and the first room. We'll implement that in a minute, but what it does is: it divides the rectangle into four quadrants, and adds each of the quadrants to the rectangle list. - Now we setup a room counter, so we don't infinitely loop.
- While that counter is less than 240 (a relatively arbitrary limit that gives fun results):
- We call
get_random_rect
to retrieve a random rectangle from the rectangles list. - We call
get_random_sub_rect
using this rectangle as an outer boundary. It creates a random room from 3 to 10 tiles in size (on each axis), somewhere within the parent rectangle. - We ask
is_possible
if the candidate can be drawn to the map; every tile must be within the map boundaries, and not already a room. If it IS possible:- We mark it on the map.
- We add it to the rooms list.
- We call
add_subrects
to sub-divide the rectangle we just used (not the candidate!).
- We call
There's quite a few support functions in play here, so lets go through them.
The function add_subrects
is core to the BSP (Binary Space Partition) approach: it takes a rectangle, and divides the width and height in half. It then creates four new rectangles, one for each quadrant of the original. These are added to the rects
list. Graphically:
############### ###############
# # # 1 + 2 #
# # # + #
# 0 # -> #+++++++++++++#
# # # 3 + 4 #
# # # + #
############### ###############
Next up is get_random_rect
:
This is a simple function. If there is only one rectangle in the rects
list, it returns the first one. Otherwise, it rolls a dice for of 1d(size of rects list)
and returns the rectangle found at the random index.
Next up is get_random_sub_rect
:
So this takes a rectangle as the parameter, and makes a mutable copy to use as the result. It calculates the width and height of the rectangle, and then produces a random width and height inside that rectangle - but no less than 3 tiles in size and no more than 10 on each dimension. You can tweak those numbers to change your desired room size. It then shunts the rectangle a bit, to provide some random placement (otherwise, it would always be against the sides of the sub-rectangle). Finally, it returns the result. Graphically:
############### ########
# # # 1 #
# # # #
# 0 # -> ########
# #
# #
###############
Finally, the is_possible
function:
This is a little more complicated, but makes sense when you break it down:
- Take a rectangle as a target, representing the room we are looking at.
- Create a mutable copy of the rectangle called
expanded
. We then expand the rectangle out by 2 tiles in each direction, to prevent rooms from overlapping. - We iterate every
x
andy
coordinate in the rectangle:- If
x
ory
are out of the map boundaries, we markcan_build
asfalse
- this won't work. - If we still can build it, we look at the existing map - if it isn't a solid wall, then we've overlapped an existing room, and mark that we can't build.
- If
- We return the result of
can_build
.
So now that we've implemented all of these, the overall algorithm is more obvious:
- We start with a single rectangle covering the entire map.
- We sub-divide it, so now our map has 5 rectangles - one for each quadrant, one for the map as a whole.
- We use a counter to ensure that we don't loop forever (we'll reject a lot of rooms). While we can still add rooms, we:
- Obtain a random rectangle from the rectangles list. Initially, this will be one of the quadrants - or the whole map. This list will keep growing as we add subdivisions.
- We generate a random sub-rectangle inside this rectangle.
- We look to see if that's a possible room. If it is, we:
- Apply the room to the map (build it).
- Add it to the rooms list.
- Sub-divide the new rectangle into quadrants and add those to our rectangles list.
- Store a snapshot for the visualizer.
This tends to give a nice spread of rooms, and they are guaranteed not to overlap. Very Nethack like!
If you cargo run
now, you will be in a room with no exits. You'll get to watch rooms appear around the map in the visualizer. That's a great start.
Adding in corridors
Now, we sort the rooms by left coordinate. You don't have to do this, but it helps make connected rooms line up.
sort_by
takes a closure - that is, an inline function (known as a "lambda" in other languages) as a parameter. You could specify a whole other function if you wanted to, or implement traits on Rect
to make it sortable - but this is easy enough. It sorts by comparing the x1
value of each rectangle.
Now we'll add some corridors:
This iterates the rooms list, ignoring the last one. It fetches the current room, and the next one in the list and calculates a random location (start_x
/start_y
and end_x
/end_y
) within each room. It then calls the mysterious draw_corridor
function with these coordinates. Draw corridor adds a line from the start to the end, using only north/south or east/west (it can give 90-degree bends). It won't give you a staggered, hard to navigate perfect line like Bresenham would. We also take a snapshot.
The draw_corridor
function is quite simple:
It takes a start and end point, and creates mutable x
and y
variables equal to the starting location. Then it keeps going until x
and y
match end end of the line. For each iteration, if x
is less than the ending x
- it goes left. If x
is greater than the ending x
- it goes right. Same for y
, but with up and down. This gives straight corridors with a single corner.
Don't forget the stairs (I nearly did!)
Finally, we need to wrap up and create the exit:
We place the exit in the last room, guaranteeing that the poor player has a ways to walk.
If you cargo run
now, you'll see something like this:
.
Randomizing the dungeon per level
Rather than always using the BSP sewer algorithm, we would like to sometimes use one or the other. In map_builders/mod.rs
, replace the build
function:
Now when you play, it's a coin toss what type of map you encounter. The spawn
functions for the types are the same - so we're not going to worry about map builder state until the next chapter.
Wrap-Up
You've refactored your map building into a new module, and built a simple BSP (Binary Space Partitioning) based map. The game randomly picks a map type, and you have more variety. The next chapter will further refactor map generation, and introduce another technique.
The source code for this chapter may be found here
Run this chapter's example with web assembly, in your browser (WebGL2 required)
Copyright (C) 2019, Herbert Wolverson.