noobtuts

Clojure Procedural Dungeons

Foreword

Procedural Dungeon
When making games, there are two ways to make a dungeon. The common method is to design one in the CAD tool of our choice (or to draw one in case of 2D games).

The alternative is to automatically generate random Dungeons by using a few very powerful algorithms. We could automatically generate a whole game world if we wanted to, but let's take one step after another.

In this Tutorial we will implement procedural Dungeons in Clojure, while keeping everything as simple as possible so everyone can understand it.

The Challenges

If we want to generate Dungeons, we will face a few challenges. We could start by spawning random rectangles in the game world.

Overlapping Rectangles

This could lead to two problems. At first, all (or many) of them could overlap each other, which would result in some really boring dungeons:
Overlapping Rectangles

Lonely Rectangles

On the other hand, perhaps none of them will overlap each other and we will get a dungeon like this one:
Lonely Rectangles

Which isn't really a dungeon at all. There is no way that a player can start in one room and reach all the other rooms.

Connecting all Rooms

Which leads us to another challenge: the player should be able to reach all rooms. There is no point in generating a dungeon where the boss room can't be reached. We have to make sure that every single room can be reached from every other room.

Of course the easy way to do this would be by directly connecting every room with every other room:
Rectangles connected with each other

But this gives us a really boring dungeon because the player could directly walk from the entry room to the boss room.

What we really want is a dungeon with just a few big boss rooms and many small rooms along the way, just as we know it from all those RPGs out there.

The Solution

Now the good news: all of those problems have been solved before. There are several really powerful algorithms in the computer science field that will help us generate our Dungeons.

So let's get to work and implement them!

Random Rectangles

Alright we will start with some random rectangles. We will use a hash-map to represent a rectangle like this one:

(def r {:x 0 :y 1 :w 10 :h 20})

Where x and y are the position, w and h are the width and height.

Now spawning a few of them is very easy, thanks to the repeatedly function. We will implement a gen-rects function that has a parameter n (amount of rectangles) and a parameter r (radius in which to spawn them).

Here is the first version which simply spawns n rectangles with width = 10 and height = 10 at random positions within the radius r:

(defn gen-rects [n r]
  (set
    (repeatedly n (fn [] {:x (rand-int r)
                          :y (rand-int r)
                          :w 10
                          :h 10}))))

Note: (rand-int r) returns a random number between 0 and r.

Now let's modify the function so it also gives the rectangles a random width and height, depending on how big the radius is (because.. why not?). However we have to be careful that they never have a too small size like 0. We will make it so that the size is always between r/2 and r:

; (gen-rects 2 2) => #{{:x 2, :y 3, :w 1, :h 1} , ...
(defn gen-rects [n r]
  (let [rhalf (/ r 2)]
    (set
      (repeatedly n (fn [] {:x (rand-int r)
                            :y (rand-int r)
                            :w (+ rhalf (* (rand) rhalf))
                            :h (+ rhalf (* (rand) rhalf))})))))

Note: this works because the (rand) function always returns a number between 0 and 1, so the size is always between r/2 + 0 * r/2 and r/2 + 1 * r/2.

There is one last thing to understand about our gen-rects function: it returns a set, because this makes our lives much easier later on.

Alright, now we are able to spawn random rectangles, for example:

(gen-rects 3 100)
; => #{{:x 97, :y 89, :w 83.35, :h 96.99}
;      {:x 51, :y 66, :w 76.32, :h 58.52}
;      {:x 22, :y 84, :w 99.67, :h 60.97}}

Here is how it looks if we draw them on the screen:
gen-rects Function Result

Note: a draw-dungeons function can be found in the project files, but you can use any drawing library/function that suits your needs.

Rectangle Separation

Horizontal Separation

As mentioned before, we don't want them to overlap. The easiest solution would be to move every intersecting rectangle to the right until it doesn't intersect with any other rectangle anymore.

The algorithm would be very easy to implement, but it would give us some very boring dungeons that always go from the left to the right:
Rectangle Separation X

A better Approach

So let's implement a real rectangle separation algorithm. It will also go through one rectangle after another, moving it into some direction until it doesn't intersect with any other rectangle anymore. The only difference is that instead of always moving it to the right, it will move it away from the center.

Let's assume we have a few rectangles where the red one should be separated:
Rectangle Separation

Rectangle Centers

We will first calculate each rectangle's center:
Rectangle Centers

With this function:

; calculate rect center
; (rect-center {:x 0 :y 0 :w 4 :h 5}) => [2 2.5]
(defn rect-center [{:keys [x y w h]}]
  [(+ x (/ w 2))
   (+ y (/ h 2))])

Note: the function's parameter looks so weird because Destructuring was used.

. . .

Premium Tutorial

Enjoyed this preview? Become a Premium member and access the full Clojure Procedural Dungeons Tutorial!

All Tutorials. All source codes & project files. One time payment.
Get Premium today!