Procedural Dungeon Generator
BSP-based dungeon generator that creates random room layouts with corridors, doors, and spawn points.
How to Use
Create floor and wall prefabs (cubes scaled to tile size)
Attach DungeonGenerator to a manager object
Assign floor and wall prefabs
Configure dungeon size, room size range, and split depth
Generates automatically on Start (or call Generate() manually)
Access rooms: generator.Rooms for room positions/sizes
Get spawn points: generator.GetRandomSpawnPoint()
Set seed for reproducible layouts (0 = random each time)
Features
- BSP (Binary Space Partitioning) algorithm for natural room distribution
- Configurable dungeon size, room size range, and split depth
- L-shaped corridors connecting room centers automatically
- Wall auto-generation around floors for clean boundaries
- Spawn points at room centers for player and enemy placement
- Seed-based generation for reproducible dungeon layouts
When to Use This
Perfect for roguelikes, dungeon crawlers, and RPGs that need random level generation each playthrough. Use for procedural content in games like Binding of Isaac-style room layouts or traditional dungeon crawls. Pair with A* Pathfinding for enemy navigation through the generated layout.
Common Mistakes
You must assign both floorPrefab and wallPrefab in the Inspector — null prefabs will generate the layout data but nothing visible. The tileSize must match your prefab dimensions; a mismatch causes gaps or overlaps. For large dungeons (100x100+), Instantiate can cause a frame spike — consider object pooling or async generation for smooth loading.
Source Code
using UnityEngine;
using System.Collections.Generic;
/// <summary>
/// BSP (Binary Space Partitioning) dungeon generator.
/// Creates random room layouts connected by corridors.
/// </summary>
public class DungeonGenerator : MonoBehaviour
{
[Header("Dungeon Size")]
[SerializeField] private int width = 60;
[SerializeField] private int height = 60;
[SerializeField] private float tileSize = 1f;
[Header("Rooms")]
[SerializeField] private int minRoomSize = 6;
[SerializeField] private int maxRoomSize = 15;
[SerializeField] private int maxSplitDepth = 5;
[Header("Corridors")]
[SerializeField] private int corridorWidth = 2;
[Header("Prefabs")]
[SerializeField] private GameObject floorPrefab;
[SerializeField] private GameObject wallPrefab;
[Header("Settings")]
[SerializeField] private bool generateOnStart = true;
[SerializeField] private int seed = 0; // 0 = random
private int[,] map; // 0=empty, 1=floor, 2=wall
private List<RectInt> rooms = new List<RectInt>();
private List<Vector2Int> spawnPoints = new List<Vector2Int>();
private Transform dungeonParent;
/// <summary>Generated rooms.</summary>
public IReadOnlyList<RectInt> Rooms => rooms;
/// <summary>Spawn point positions (centers of rooms).</summary>
public IReadOnlyList<Vector2Int> SpawnPoints => spawnPoints;
private void Start()
{
if (generateOnStart)
Generate();
}
/// <summary>
/// Generate a new dungeon layout.
/// </summary>
public void Generate()
{
if (seed != 0)
Random.InitState(seed);
else
Random.InitState(System.Environment.TickCount);
Clear();
map = new int[width, height];
rooms.Clear();
spawnPoints.Clear();
// BSP split
List<RectInt> partitions = new List<RectInt>();
BSPSplit(new RectInt(1, 1, width - 2, height - 2), maxSplitDepth, partitions);
// Create rooms within partitions
foreach (var partition in partitions)
{
CreateRoom(partition);
}
// Connect rooms with corridors
for (int i = 0; i < rooms.Count - 1; i++)
{
ConnectRooms(rooms[i], rooms[i + 1]);
}
// Add walls
AddWalls();
// Instantiate
BuildDungeon();
}
/// <summary>Clear the generated dungeon.</summary>
public void Clear()
{
if (dungeonParent != null)
Destroy(dungeonParent.gameObject);
}
private void BSPSplit(RectInt area, int depth, List<RectInt> result)
{
if (depth <= 0 || area.width < minRoomSize * 2 || area.height < minRoomSize * 2)
{
result.Add(area);
return;
}
bool splitHorizontal = Random.value > 0.5f;
if (area.width > area.height * 1.5f) splitHorizontal = false;
if (area.height > area.width * 1.5f) splitHorizontal = true;
if (splitHorizontal)
{
int split = Random.Range(area.y + minRoomSize, area.yMax - minRoomSize);
BSPSplit(new RectInt(area.x, area.y, area.width, split - area.y), depth - 1, result);
BSPSplit(new RectInt(area.x, split, area.width, area.yMax - split), depth - 1, result);
}
else
{
int split = Random.Range(area.x + minRoomSize, area.xMax - minRoomSize);
BSPSplit(new RectInt(area.x, area.y, split - area.x, area.height), depth - 1, result);
BSPSplit(new RectInt(split, area.y, area.xMax - split, area.height), depth - 1, result);
}
}
private void CreateRoom(RectInt partition)
{
int roomW = Random.Range(minRoomSize, Mathf.Min(maxRoomSize, partition.width));
int roomH = Random.Range(minRoomSize, Mathf.Min(maxRoomSize, partition.height));
int roomX = Random.Range(partition.x, partition.xMax - roomW);
int roomY = Random.Range(partition.y, partition.yMax - roomH);
RectInt room = new RectInt(roomX, roomY, roomW, roomH);
rooms.Add(room);
Vector2Int center = new Vector2Int(roomX + roomW / 2, roomY + roomH / 2);
spawnPoints.Add(center);
for (int x = room.x; x < room.xMax; x++)
for (int y = room.y; y < room.yMax; y++)
if (x >= 0 && x < width && y >= 0 && y < height)
map[x, y] = 1;
}
private void ConnectRooms(RectInt roomA, RectInt roomB)
{
Vector2Int a = new Vector2Int(roomA.x + roomA.width / 2, roomA.y + roomA.height / 2);
Vector2Int b = new Vector2Int(roomB.x + roomB.width / 2, roomB.y + roomB.height / 2);
// L-shaped corridor
if (Random.value > 0.5f)
{
CarveHorizontal(a.x, b.x, a.y);
CarveVertical(a.y, b.y, b.x);
}
else
{
CarveVertical(a.y, b.y, a.x);
CarveHorizontal(a.x, b.x, b.y);
}
}
private void CarveHorizontal(int x1, int x2, int y)
{
int start = Mathf.Min(x1, x2);
int end = Mathf.Max(x1, x2);
for (int x = start; x <= end; x++)
for (int w = 0; w < corridorWidth; w++)
if (x >= 0 && x < width && y + w >= 0 && y + w < height)
map[x, y + w] = 1;
}
private void CarveVertical(int y1, int y2, int x)
{
int start = Mathf.Min(y1, y2);
int end = Mathf.Max(y1, y2);
for (int y = start; y <= end; y++)
for (int w = 0; w < corridorWidth; w++)
if (x + w >= 0 && x + w < width && y >= 0 && y < height)
map[x + w, y] = 1;
}
private void AddWalls()
{
for (int x = 0; x < width; x++)
{
for (int y = 0; y < height; y++)
{
if (map[x, y] != 0) continue;
// Check if adjacent to floor
for (int dx = -1; dx <= 1; dx++)
{
for (int dy = -1; dy <= 1; dy++)
{
int nx = x + dx, ny = y + dy;
if (nx >= 0 && nx < width && ny >= 0 && ny < height && map[nx, ny] == 1)
{
map[x, y] = 2;
goto nextCell;
}
}
}
nextCell:;
}
}
}
private void BuildDungeon()
{
GameObject parent = new GameObject("Dungeon");
dungeonParent = parent.transform;
for (int x = 0; x < width; x++)
{
for (int y = 0; y < height; y++)
{
Vector3 pos = new Vector3(x * tileSize, 0f, y * tileSize);
if (map[x, y] == 1 && floorPrefab != null)
Instantiate(floorPrefab, pos, Quaternion.identity, dungeonParent);
else if (map[x, y] == 2 && wallPrefab != null)
Instantiate(wallPrefab, pos, Quaternion.identity, dungeonParent);
}
}
}
/// <summary>Get a random spawn point in the dungeon.</summary>
public Vector3 GetRandomSpawnPoint()
{
if (spawnPoints.Count == 0) return Vector3.zero;
Vector2Int p = spawnPoints[Random.Range(0, spawnPoints.Count)];
return new Vector3(p.x * tileSize, 0f, p.y * tileSize);
}
}