advanced Utilities

Procedural Dungeon Generator

BSP-based dungeon generator that creates random room layouts with corridors, doors, and spawn points.

Unity 2022.3+ · 4.0 KB · DungeonGenerator.cs

How to Use

1

Create floor and wall prefabs (cubes scaled to tile size)

2

Attach DungeonGenerator to a manager object

3

Assign floor and wall prefabs

4

Configure dungeon size, room size range, and split depth

5

Generates automatically on Start (or call Generate() manually)

6

Access rooms: generator.Rooms for room positions/sizes

7

Get spawn points: generator.GetRandomSpawnPoint()

8

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

DungeonGenerator.cs
C#
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);
    }
}