Rogue-lite: procedural dungeon

Rogue-lite: procedural dungeon

When creating a procedural map you need to think about how you are going to organize your map layout and how much of it will be procedurally. In my case I wanted something simple but with enough randomisation to make each run feel different. So I thought of making my map a simple grid with rooms and corridors. The only requirement was to have the ability to deal with different room size. I kept the shape of the rooms rectangle with 3 different diffent size and aspect ratio for simplicity. The main reason for this choice is that the corridors could connect easily horizontally or vertically.

procedural-room-gif

Once I made these decisions I started breaking down the map into abstract pieces.

  • grid: the entire map
  • block: a block of the grid, contains one room and 4 edges either room or corridor, all blocks have the same dimensions.
  • room: a room with four openings (up, down, left, right), can be different size.
  • corridor: a corridor connecting a room to the edge of its block, must match the room size.
  • Wall: a wall that closes a rooms opening

procedural-room-gif

Now that I had abstracted the map layout I started to think about how I could program the procedural creation. The first thing to take into account is that each stage has a starting point and an end point, which must be connected. Preferably we would like the end point to be far from the start point. The other point to take into account is that we would like to have multiple paths, which may or may not lead to the end. We would like to have different room types and the procedurally created paths should be independent from the room types.

Taking all of this into account I made a function that sperates the building process in sections.

//Note the function is an IEnumerator so that it can be called asynchronously
public IEnumerator StartBuilding()
{

    yield return null;
    CheckStageSettings();

    yield return null;
    Setup();

    yield return null;
    PositionRooms();

    yield return null;
    AssignRooms();

    yield return null;
    DrawStage();

}

The CheckStageSettings looks at a Scriptable Object that contains some configuration parameters for the build creating. For example the number of rooms, the probability of finding some type of room, the grid size (not always squared), etc. This gives me the ability to set up different configuration of map layout for each stage of the game. At this point we also check that the parameters are valid.

The Setup function simply initializes variables using the parameters from the stage settings object. And sets up a starting point on the grid.

The PositionRooms function recursively expands the map by adding adjacent rooms to the existing rooms. This is all done using probability.

private void PositionRooms()
{
    //0 no room
    //1 room

    AddRoomAt(startRoomIndex);

    int roomsToAdd = stageSettings.numOfRooms - 1;

    int iterations = 0;
    while (roomsToAdd > 0)
    {
        Vector2Int randomRoomIndex = roomsIndexes[Random.Range(0, roomsIndexes.Count)];

        Vector2Int[] availableNeighbors = AvailableNeighbors(randomRoomIndex,false);

        if(availableNeighbors.Length != 0)
        {
            AddRoomAt(availableNeighbors[Random.Range(0, availableNeighbors.Length)]);
            roomsToAdd -= 1;
        }

        if (iterations >= 50 * stageSettings.numOfRooms)
        {
            Debug.Log("Could Not Find Neighbor");
            break;
        }
        iterations++;
    } 
    
}

The AssignRooms function is where the magic happens. We start by assigning the start room to the start index and we set the room as connected. We set its neighbours as room to be connected and start the while loop. In the loop we go through the unconnected neighbours, we assign them a room type and we connect them to at least one connected room. We check again the unconnected neighbours of all the connected rooms and we loop. Once we reach to the last room to be connected, we set its type to the end room. And just like that we have layed out the map.

private void AssignRooms()
{

    Debug.Log("Assign Rooms");

    List<Vector2Int> curToConnect = new List<Vector2Int>();
    List<Vector2Int> nextToConnect = new List<Vector2Int>();

    RoomObject startingRoom = stageSettings.environementSettings.GetRoom(
        RoomType.StartingRoom, stageSettings.roomSizeLimit);
    RoomObject endingRoom = stageSettings.environementSettings.GetRoom(
        RoomType.DoorToNextStage, stageSettings.roomSizeLimit);

    roomGrid[startRoomIndex.x, startRoomIndex.y] = new Room(startRoomIndex,startingRoom, true);

    curToConnect.AddRange(AvailableNeighbors(startRoomIndex, true));

    int roomsCount = 1;
    int iterations = 0;
    while (roomsCount < stageSettings.numOfRooms)
    {
        for (int i = 0; i < curToConnect.Count; i++)
        {
            Vector2Int index = curToConnect[i];

            Debug.Log("yo before test null");
            Debug.Log("index: " + index.x + " " + index.y);
            if (roomGrid[index.x, index.y] != null) { continue; }
            Debug.Log("yo after test null");

            float maxRD = stageSettings.roomRdTreasure + stageSettings.roomRdDefeatAll;
            RoomType randomRoomType = Random.Range(0, maxRD) > stageSettings.roomRdTreasure ?
                RoomType.DefeatAllEnemies : RoomType.TreasureRoom;


            RoomObject roomObject = stageSettings.environementSettings.GetRoom(
                randomRoomType, stageSettings.roomSizeLimit);

            if (roomsCount == stageSettings.numOfRooms - 1)
            {
                //Overwrite
                roomObject = endingRoom;
            }
                

            Room room = new Room(index, roomObject);
            roomGrid[index.x, index.y] = room;
            roomsCount++;
            Debug.Log("room added: "+roomsCount);

            Vector2Int[] availableNeighbors = AvailableNeighbors(index, true);
            Vector2Int[] notConnectedNeighbors = Connected(availableNeighbors,false);
            Vector2Int[] connectedNeighbors = Connected(availableNeighbors, true);

            nextToConnect.AddRange(notConnectedNeighbors);

            //Connect room to one (or more?) neighbors that are connected to the starting Room
            SleepingPenguinz.RandomSP.ShuffleArray(connectedNeighbors);
            for (int j = 0; j < connectedNeighbors.Length; j++)
            {
                Vector2Int neighborIndex = connectedNeighbors[j];
                Room neighborRoom = roomGrid[neighborIndex.x, neighborIndex.y];

                room.ConnectWith(neighborRoom);
                //The break prevents from connecting the room with all its available connected neighbours.
                if( Random.Range(0f,1f) >= magicRandomConnection)
                {
                    break;
                }
            }
        }

        iterations++;
        if (roomsCount < stageSettings.numOfRooms && (nextToConnect.Count == 0 || iterations >= 50 ))
        {
            Debug.LogError("Early exit");
            break;
        }
        
        curToConnect = new List<Vector2Int>();
        curToConnect.AddRange(nextToConnect);
        nextToConnect = new List<Vector2Int>();

    }
}

All that is left is spawn the rooms, walls and corridors and we do so using the DrawStage function. The rooms, walls and corridors are prefabs that are referenced by a scriptable object. The scriptable object contains not only the prefab but also information on the room/wall/corridor such as size, direction, type, etc…

Once the procedural dungeon is built we can spawn our player in the start room and play.

Author face

Santiago Rubio (Sangemdoko)

A electronics and information engineer who works on game development in his free time. He created Sleeping Penguinz to publish the games he makes with his friends and familly.

Recent post