Lets start by creating a new “WorldGen” folder inside the “World” folder.
Inside this folder we’ll create a new script “DungeonGenerator.cs” this class
is solely responsible for rendering a dungeon on our tilemap, nothing more.

I like to keep all my code seperated so its easy to expand on top later.
For our DungeonGenerator we’ll just make it a static class since we don’t need to keep any accessible information on it, it will just be called once to generate on the tilemap and return us maybe the Rooms collection for later use.
That gets me to the point, what dungeon are we generating and how will we do it?
Well since this roguelike series is expansive I don’t want to spend hours creating a massive dungeon, I just want something simple that you can later expand if you like.
So we are going to simply generate a few rooms in our tilemap and connect each room with some corridors and doorways.
Lets start with a barebones class for our dungeon generator that we can expand:
internal static class DungeonGenerator
{
// Sometimes our random position(s) won't work, so we need a few attempts
private const int MaxAttempts = 100;
// Do you want doors in every room or only a smaller percentage, 60% seems nice
private const int ChanceForDoorPlacement = 60;
public static void Generate(Tilemap tilemap, int maxRooms, int minRoomSize, int maxRoomSize, out List<Rectangle> rooms)
{
tilemap.Reset();
var random = ScreenContainer.Instance.Random;
rooms = [];
}
}
First its important to know that this Generate method might be used multiple times on the same tilemap, so its important to first do a reset. We will also be using an instance of a Random from our ScreenContainer (best to use one shared random, so when we later seed our world generation everything is the same.)
You can add this Random to our ScreenContainer.cs and initialize it inside the constructor:
public Random Random { get; }
public ScreenContainer()
{
if (_instance != null)
throw new Exception("Only one ScreenContainer instance can exist.");
_instance = this;
Random = new Random();
//and all the other code of previous articles..
}
Now lets start with generating some rooms of various sizes within our tilemap. Since our tilemap is a simple width * height grid we can take a random starting position while excluding our border tiles so we can properly place our walls later and make it look like none of the rooms are on the edge of the tilemap.
In our generate method lets continue:
public static void Generate(Tilemap tilemap, int maxRooms, int minRoomSize, int maxRoomSize, out List<Rectangle> rooms)
{
tilemap.Reset();
var random = ScreenContainer.Instance.Random;
rooms = [];
const int borderSize = 2;
// Generate rooms
for (int attempts = 0; attempts < MaxAttempts; attempts++)
{
if (rooms.Count == maxRooms) break;
int width = random.Next(minRoomSize, maxRoomSize);
int height = random.Next(minRoomSize, maxRoomSize);
// Exclude border tiles so we can set walls properly + keep an empty space as the border
int x = random.Next(borderSize, tilemap.Width - width - borderSize);
int y = random.Next(borderSize, tilemap.Height - height - borderSize);
Rectangle newRoom = new(x, y, width, height);
// Check for overlap with existing rooms
if (!rooms.Any(r => r.Intersects(newRoom)))
{
rooms.Add(newRoom);
CarveRoom(tilemap, newRoom);
}
}
}
What are we doing? Firstly we make sure to break from room generation when we are at our maximum rooms. Next we are taking a random position within our tilemap width/height excluding our border’s size. and also substracting the width and height of the room so they can never fall outside of the grid.
Then we make a new Rectangle which will represent our room the constructor works like (x, y, width, height)
Before we can add it to our rooms list we much check if this room already intersects/overlaps with any previously created rooms, if it doesn’t the room is valid and we can add it to our rooms list and start carving out the room.
To carve out the room we’ll simply set each tile inside the room as a ‘.’ floor character, give it a unique gray color and set obstruction type as open so it is walkable and see through.
private static void CarveRoom(Tilemap tilemap, Rectangle room)
{
for (int x = room.X; x < room.X + room.Width; x++)
{
for (int y = room.Y; y < room.Y + room.Height; y++)
{
// Set floor tile and make obstruction open
tilemap[x, y].Glyph = '.';
tilemap[x, y].Foreground = Color.Gray;
tilemap[x, y].Obstruction = ObstructionType.Open;
}
}
}
This will look something like:

The next part will be creating tunnels between our rooms, adding walls around our rooms and tunnels and adding random doors to our rooms to make it feel more like a dungeon.
In our generate method underneath the for loop of our room building attempts lets add a new loop to carve tunnels between the rooms:
// Connect rooms with tunnels
for (int i = 1; i < rooms.Count; i++)
{
Rectangle roomA = rooms[i - 1];
Rectangle roomB = rooms[i];
Point centerA = new(roomA.X + roomA.Width / 2, roomA.Y + roomA.Height / 2);
Point centerB = new(roomB.X + roomB.Width / 2, roomB.Y + roomB.Height / 2);
CarveTunnel(tilemap, centerA, centerB);
}
So what is happening, we are looping over all our created rooms starting at room 1,
We take the previous room (roomA) and the current room (roomB)
and we calculate the center of both rooms. We then carve a tunnel from the center of roomA to the center of roomB.
How do we carve the tunnel?
private static void CarveTunnel(Tilemap tilemap, Point start, Point end)
{
Point current = start;
while (current.X != end.X)
{
// Set floor tile and make obstruction open
tilemap[current.X, current.Y].Glyph = '.';
tilemap[current.X, current.Y].Foreground = Color.Gray;
tilemap[current.X, current.Y].Obstruction = ObstructionType.Open;
current = new Point(current.X + (current.X < end.X ? 1 : -1), current.Y);
}
while (current.Y != end.Y)
{
// Set floor tile and make obstruction open
tilemap[current.X, current.Y].Glyph = '.'; // Set floor tile
tilemap[current.X, current.Y].Foreground = Color.Gray;
tilemap[current.X, current.Y].Obstruction = ObstructionType.Open;
current = new Point(current.X, current.Y + (current.Y < end.Y ? 1 : -1));
}
}
We carve until our centerA x matches centerB x and also until
centerA y matches centerB y
Each point in the tilemap while carving changes to a floor tile respecting the same tile data as the previous room carve, setting to unique gray color, obstruction open and glyph to ‘.’ floor.
This ends up looking like this:

Now we have rooms that are connected to eachother, but we have no walls. Lets fix that by adding walls!
Before we can do this, we must first add another useful extension to our Extensions.cs to help us get our neighboring points.
/// <summary>
/// Gets the neighboring points based on the given point.
/// </summary>
/// <param name="point"></param>
/// <param name="includeDiagonals"></param>
/// <returns></returns>
internal static IEnumerable<Point> GetNeighborPoints(this Point point, bool includeDiagonals)
{
return (includeDiagonals ? _diagonalDirections : _directionalDirections).Select(d => new Point(point.X + d.X, point.Y + d.Y));
}
private static readonly Point[] _directionalDirections =
[
new Point(-1, 0), new Point(1, 0), new Point(0, -1), new Point(0, 1)
];
private static readonly Point[] _diagonalDirections =
[
new Point(-1, 0), new Point(1, 0), new Point(0, -1), new Point(0, 1),
new Point(-1, -1), new Point(-1, 1), new Point(1, -1), new Point(1, 1)
];
We can now get all our neighbor points on a given Point this will be useful to create our walls. The directional and diagonal directions are mappings that should never change, hence we define them on a global level as to not re-initialize the arrays for every call to the extension we do.
Now onto wall building, lets create a new method that will do just that:
private static void AddWalls(Tilemap tilemap)
{
for (int x=0; x < tilemap.Width; x++)
{
for (int y = 0; y < tilemap.Height; y++)
{
// Check if the current tile is a floor
if (tilemap[x, y].Glyph == '.')
{
// Get neighbors of the current tile
var neighbors = new Point(x, y).GetNeighborPoints(true);
foreach (var neighbor in neighbors)
{
// If a neighbor is inbounds and not a floor, set it as a wall
if (tilemap.InBounds(neighbor.X, neighbor.Y) && tilemap[neighbor.X, neighbor.Y].Glyph != '.')
{
// Set wall glyph and make obstruction fully blocked
tilemap[neighbor.X, neighbor.Y].Glyph = '#';
tilemap[neighbor.X, neighbor.Y].Foreground = Color.SandyBrown;
tilemap[neighbor.X, neighbor.Y].Obstruction = ObstructionType.FullyBlocked;
}
}
}
}
}
}
We are now looping over each tile in the tilemap, checking if the tile is a ‘.’ floor tile.
Then for that tile we get all its 8 neighbors (diagonal included) and we check for all the tiles that are InBounds of the tilemap and are not a floor we simply change those tiles to a “wall” tile reflected with the ‘#’ character, a SandyBrown color and obstruction type will be FullyBlocked.
Lets also call this method in our Generate method in the DungeonGenerator.cs right under the for loop for our room tunnel carving.
AddWalls(tilemap);
This looks like:

Now all that is left is to place some doors, I decided that a bunch of random doors is more realistic in a dungeon, then a door for every single room. So I put a randomization of 60% on it. You can always put it to 100 if you like a door in every room by changing the const field at the top of the DungeonGenerator class. Lets make a method similar to AddWalls but now for doors.
private static void AddDoors(Tilemap tilemap, List<Rectangle> rooms)
{
foreach (var room in rooms)
{
var wallPositions = room.Expand(1, 1).PerimeterPositions();
// Check if tile is a floor and if both horizontal / vertical neighbors are a match with eachother
foreach (var position in wallPositions)
{
if (!tilemap.InBounds(position.X, position.Y)) continue;
// If tile is a floor
if (tilemap[position.X, position.Y].Glyph == '.')
{
// Get directional neighbors
var north = tilemap[position.X, position.Y + 1, false];
var south = tilemap[position.X, position.Y - 1, false];
var east = tilemap[position.X + 1, position.Y, false];
var west = tilemap[position.X - 1, position.Y, false];
if ((north?.Glyph == '.' && south?.Glyph == '.' && east?.Glyph == '#' && west?.Glyph == '#') ||
(north?.Glyph == '#' && south?.Glyph == '#' && east?.Glyph == '.' && west?.Glyph == '.'))
{
// % chance to place a door
if (ScreenContainer.Instance.Random.Next(100) < ChanceForDoorPlacement)
{
tilemap[position.X, position.Y].Glyph = '+';
tilemap[position.X, position.Y].Foreground = Color.OrangeRed;
tilemap[position.X, position.Y].Obstruction = ObstructionType.VisionBlocked;
}
}
}
}
}
}
The principal is the same, but instead of looping over the entire map we will just loop over each room we defined, expand the room 1×1 in size, and take the border positions (perimeter) these reflect the positions of the walls of the room.
Then we just need to check for all the tiles in the border that are a ‘.’ floor type.
And they must match the following criteria:
- Both north and south must be either: wall ‘#’ tiles, or floor ‘.’ tiles (but not a mix).
- Both east and west must be the opposite of north and south.
- Center tile must a floor and part of the room’s perimeter
So for example both cases here are valid door tiles:
- The top red square matches both east and west being floor and north and south being wall, the center is a floor and part of the room perimeter.
- The bottom red square matches both east and west being wall and north and south being floor, the center is a floor and part of the room perimeter.

Underneath our AddWalls(tilemap) method call we’ll add our AddDoors(tilemap, rooms) method call.
The generate method will end up looking like this in the end:
public static void Generate(Tilemap tilemap, int maxRooms, int minRoomSize, int maxRoomSize, out List<Rectangle> rooms)
{
tilemap.Reset();
var random = ScreenContainer.Instance.Random;
rooms = [];
const int borderSize = 2;
// Generate rooms
for (int attempts = 0; attempts < MaxAttempts; attempts++)
{
if (rooms.Count == maxRooms) break;
int width = random.Next(minRoomSize, maxRoomSize);
int height = random.Next(minRoomSize, maxRoomSize);
// Exclude border tiles so we can set walls properly + keep an empty space as the border
int x = random.Next(borderSize, tilemap.Width - width - borderSize);
int y = random.Next(borderSize, tilemap.Height - height - borderSize);
Rectangle newRoom = new(x, y, width, height);
// Check for overlap with existing rooms
if (!rooms.Any(r => r.Intersects(newRoom)))
{
rooms.Add(newRoom);
CarveRoom(tilemap, newRoom);
}
}
// Connect rooms with tunnels
for (int i = 1; i < rooms.Count; i++)
{
Rectangle roomA = rooms[i - 1];
Rectangle roomB = rooms[i];
CarveTunnel(tilemap, roomA.Center, roomB.Center);
}
AddWalls(tilemap);
AddDoors(tilemap, rooms);
}
And thats randomized doors implemented now too:

That is the gist of the dungeon generator, now we just need to hook it up in our generation process of the world. We do that in our WorldScreen.cs
In the Generate method of our WorldScreen.cs
public void Generate()
{
DungeonGenerator.Generate(Tilemap, 10, 4, 10, out _);
}
We can replace our ugly blue background Fill method, with our DungeonGenerator Generate method call. And we can specify the sizes for our rooms and the max rooms we want.
We pass our tilemap, and all the calculations will happen directly on the tilemap and be reflected automatically in the WorldScreen.
Note: For now i’ve discared the out property because we don’t yet have a use for the rooms, but we will need it later when spawning entities and the player inside the rooms.
Give it a go, and run the game and marvel at your cool dungeon!
Checkout the next article to continue with the series!
You can find all the code for this series down in the repository here:
https://github.com/Ven0maus/Code2DTutorials/tree/Tutorials/Roguelike
