Procedurally Generated Dungeon Tutorial in Java-Script

For many, procedural generation is a magical concept that is just out of reach. Only veteran game developers know how to build a game that can create its own levels... right? It may seem like magic, but PCG (procedural content generation) can be learned by beginner game developers. In this tutorial, I'll show you how to procedurally generate a dungeon cave system.

Our Goal

Our goal will be make a 100% Javascript Procedurally Generated Dungeon that at the end will look somewhat like this:

Creating a map

For this tutorial we will be making a map out of an array of objects, each object storing their column, row, x, y, a boolean set to false (whether or not it s a part of a room). To generate the map we will use nestled for loops. However first we need to create the vars we will need to and the function that will draw everything as well as update it and the functions that create the rooms and map.

var canvas = document.getElementById("game");
var canvasContext = canvas.getContext("2d");

var w = 20;

var rows = 50;
var cols = 50;

var grid = [];

var rooms = [];
var collide = false;

var amount = 10;
var size = 5; //the actual size will be a number between 5 and 10 | e.g: size+sizeMin
var sizeMin = 5;

var disX;
var disY;
var corridorW = 1;

function Cell(c, r, x, y) {
  this.c = c;
  this.r = r;
  this.x = x;
  this.y = y;
  this.empty = false;

  this.show = function() {
    if (this.empty == false) {
      canvasContext.fillStyle = "#323232";
      canvasContext.fillRect(this.x, this.y, w, w);
    } else {
      canvasContext.fillStyle = "#696966";
      canvasContext.fillRect(this.x, this.y, w, w);
    }
  };

  this.carve = function(dis, x, y) {
    for (var i = 0; i < rooms.length; i++) {
      if (
        this.c >= rooms[i].y / w &&
        this.c < rooms[i].y / w + rooms[i].h / w &&
        this.r >= rooms[i].x / w &&
        this.r < rooms[i].x / w + rooms[i].w / w
      ) {
        this.empty = true;
      }
    }
  };

  this.carveH = function(dis, x, y) {
    if (
      this.r >= x &&
      this.r < x + dis &&
      this.c < y + corridorW &&
      this.c > y - corridorW
    ) {
      this.empty = true;
    }
  };
  this.carveV = function(dis, x, y) {
    if (
      this.c >= y &&
      this.c < y + dis &&
      this.r < x + corridorW &&
      this.r > x - corridorW
    ) {
      this.empty = true;
    }
  };
}

function makeGrid() {
  for (var r = 0; r < rows; r++) {
    for (var c = 0; c < cols; c++) {
      var y = c * w;
      var x = r * w;
      var cell = new Cell(c, r, x, y);
      grid.push(cell);
    }
  }
}

function draw() {
  for (var i = 0; i < grid.length; i++) {
    grid[i].show();
    grid[i].carve();
  }

  for (var i = 0; i < rooms.length; i++) {
    rooms[i].draw();
  }
}

makeGrid();
createRooms();
draw();

Generating rooms

To generate the rooms we will choose a random x, y, width and height then we go through and see if any of the cells in our map are within the room if it is then we switch the boolean empty to true in the cell object.

function Room(x, y, width, height, i) {
  this.x = (x - 1) * w;
  this.y = (y - 1) * w;
  this.w = width * w;
  this.h = height * w;

  this.center = [
    Math.floor(this.x / w + width / 2),
    Math.floor(this.y / w + height / 2)
  ];

  this.draw = function() {
    canvasContext.fillStyle = "white";
    canvasContext.fillText(i, this.x + this.w / 2, this.y + this.h / 2 - 20);
  };
}

function createRooms() {
  for (var i = 0; i < amount; i++) {
    var room = new Room(
      Math.floor(Math.random() * rows) + 1,
      Math.floor(Math.random() * cols) + 1,
      Math.floor(Math.random() * size) + sizeMin,
      Math.floor(Math.random() * size) + sizeMin,
      i
    );
    rooms.push(room);
  }
}

Making sure they do not collide

To make sure the rooms do not collide we put some basic collision code in the function that generates the rooms we then delete on of the two rooms. There is also code to detect whether or not the rooms is outside of the canvas if so we delete the room. if everything goes well and the room is not off the canvas or colliding with other rooms then we add it to the array of rooms.


function createRooms() {
  for (var i = 0; i < amount; i++) {
    var room = new Room(
      Math.floor(Math.random() * rows) + 1,
      Math.floor(Math.random() * cols) + 1,
      Math.floor(Math.random() * size) + sizeMin,
      Math.floor(Math.random() * size) + sizeMin,
      i
    );

    if (i > 0) {
      if (
        rooms[0].x + rooms[0].w >= canvas.width ||
        rooms[0].x <= 0 ||
        rooms[0].y + rooms[0].h >= canvas.height ||
        rooms[0].y <= 0
      ) {
        rooms = [];
        createRooms();
        break;
      }

      for (var e = 0; e < rooms.length; e++) {
        collide = false;

        if (
          room.x <= rooms[e].x + rooms[e].w &&
          room.x + room.w >= rooms[e].x &&
          room.y <= rooms[e].y + rooms[e].h &&
          room.y + room.h >= rooms[e].y
        ) {
          collide = true;
          i--;
          break;
        } else if (
          room.x + room.w >= canvas.width ||
          room.x <= 0 ||
          room.y + room.h >= canvas.height ||
          room.y <= 0
        ) {
          collide = true;
          i--;
          break;
        }
      }
    }

    if (collide == false) {
      rooms.push(room);
    }
  }
}

Connecting Rooms

To connect the rooms we first find the horizontal distance and the vertical distance between the centre of the rooms. Once we have the vertical and horizontal distances we check whether or not any of the cells are in the horizontal corridor by check if they are at the appropriate row and in-between the rooms centre and the rooms centre plus the horizontal distance between rooms inside the cell object we then do the same thing for the vertical corridors and now we have functions that create corridors.


function hCorridor(x1, x2, y1, y2) {
  if (x1 > x2) {
    disX = x1 - x2;
    disX += 1;

    for (var i = 0; i < grid.length; i++) {
      grid[i].carveH(disX, x2, y2);
    }
  } else {
    disX = x2 - x1;
    disX += 1;
    for (var i = 0; i < grid.length; i++) {
      grid[i].carveH(disX, x1, y1);
    }
  }
}

function vCorridor(x1, x2, y1, y2) {
  var x;

  if (y1 > y2) {
    disY = y1 - y2;
    disY += 1;

    if (x2 + (disX - 1) > x1 + (disX - 1)) {
      x = x2;
    } else {
      x = x2 + (disX - 1);
    }

    for (var i = 0; i < grid.length; i++) {
      grid[i].carveV(disY, x, y2);
    }
  } else {
    disY = y2 - y1;
    disY += 1;

    if (x1 + (disX - 1) > x2 + (disX - 1)) {
      x = x1;
    } else {
      x = x1 + (disX - 1);
    }

    for (var i = 0; i < grid.length; i++) {
      grid[i].carveV(disY, x, y1);
    }
  }
}

we will also need to add this code to our createRooms function. Make sure it is inside the if statement "if(collide == false)" this will call the functions that create the hallways.

if (i > 0) {
  hCorridor(
    rooms[i - 1].center[0],
    room.center[0],
    rooms[i - 1].center[1],
    room.center[1]
  );
  vCorridor(
    rooms[i - 1].center[0],
    room.center[0],
    rooms[i - 1].center[1],
    room.center[1]
  );
}

Where to go from here

You've come a long way building your first procedurally generated dungeon level, and I'm hoping you've realized that PCG isn't some magical beast that you will never have a chance to slay.

We went over how to make a map using arrays & objects, and randomly place content around your dungeon level with simple random number generators. Next, we discovered a way to determine if your random placement made sense by checking for overlapping rooms. Lastly, we found a way to ensure that your player can reach every room in your dungeon.

The first four steps of our five step process are finished, which means that you have the building blocks of a great dungeon for your next game. The final step is down to you: you must iterate over what you learned to create more procedurally generated content for endless replay-ability.

If you would like more explained source code you can download it here


Me!

The_Coder

I love making games and creating pixel art you can see some of my work and read a bit more about me here.