Gatzby01
Gatzby01

Reputation: 3

Generating a Navigation Mesh at runtime for a 2d semi-procedural dungeon crawler(Unity)

For the past few months, I have been creating a 2D dungeon crawler similar to the Binding of Isaac and Enter the Gungeon, with a bit of inspiration from older Zelda games. Without getting into too much detail, the main scene which houses the player, UI, and some other objects such as a Game Controller, Room Controller, etc. When pressing play, the main scene gets populated with "rooms" which are their own seperate scenes. These scenes contain the tilemap for the room, door handlers to open and close the doors based on enemy state, and an ObjectRoomSpawner which deals with spawning the enemies/spawnables such as healthpots onto the room. Up until now, I've been using a simple moveTowards function for the enemy, but it makes the gameplay feel a bit boring.

My question is, how might I bake a nav mesh at runtime for my scenario? While it's not really generated procedrually, each instance of the game will have a different dungeon layout from a set number of rooms I've created then added to the RoomController. Everything I've tried so far hasn't worked out. The farthest I've managed to get done is generating the nav mesh for the first instance of each room, but any duplicate of that room throughout the dungeon doesn't have. Some of the things I've tried are creating a NavMesh GameObject in each 'room' scene then adding the modifier component to each tilemap under the grid for the room. I've also tried creating a GameObject in the main scene, and using the tutorial page Unity has on NavMesh baking at runtime, but to no avail. Hopefully someone can help point me in the right direction. Below is my Room and RoomController scripts.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Room : MonoBehaviour
{

    public int Width;
    public int Height;
    public int X;
    public int Y;
    
    private bool updatedDoors = false;

    public Room(int x, int y)
    {
        X = x;
        Y = y;
    }

    public Door leftDoor;
    public Door rightDoor;
    public Door topDoor;
    public Door bottomDoor;
    public GameObject replaceWallTop;
    public GameObject replaceWallBottom;
    public GameObject replaceWallLeft;
    public GameObject replaceWallRight;
    public List<Door> doors = new List<Door>();

    // Start is called before the first frame update
    void Start()
    {
        if (RoomController.instance == null)
        {
            Debug.Log("You pressed play in the wrong scene!");
            return;
        }

        Door[] ds = GetComponentsInChildren<Door>();
        foreach (Door d in ds)
        {
            doors.Add(d);
            switch (d.doorType)
            {
                case Door.DoorType.right:
                    rightDoor = d;
                    break;
                case Door.DoorType.left:
                    leftDoor = d;
                    break;
                case Door.DoorType.top:
                    topDoor = d;
                    break;
                case Door.DoorType.bottom:
                    bottomDoor = d;
                    break;
            }
        }

        RoomController.instance.RegisterRoom(this);
    }

    void Update()
    {
        if (name.Contains("End") && !updatedDoors)
        {
            RemoveUnconnectedDoors();
            updatedDoors = true;
        }
    }

    public void RemoveUnconnectedDoors()
    {
        Debug.Log("removing doors");
        foreach (Door door in doors)
        {
            switch (door.doorType)
            {
                case Door.DoorType.right:
                    if (GetRight() == null)
                    {
                        door.gameObject.SetActive(false);
                        replaceWallRight.SetActive(true);
                    }
                    break;
                case Door.DoorType.left:
                    if (GetLeft() == null)
                    {
                        door.gameObject.SetActive(false);
                        replaceWallLeft.SetActive(true);
                    }
                    break;
                case Door.DoorType.top:
                    if (GetTop() == null)
                    {
                        door.gameObject.SetActive(false);
                        replaceWallTop.SetActive(true);
                    }
                    break;
                case Door.DoorType.bottom:
                    if (GetBottom() == null)
                    {
                        door.gameObject.SetActive(false);
                        replaceWallBottom.SetActive(true);
                    }
                    break;
            }
        }
    }

    public Room GetRight()
    {
        if (RoomController.instance.DoesRoomExist(X + 1, Y))
        {
            return RoomController.instance.FindRoom(X + 1, Y);
        }
        return null;
    }
    public Room GetLeft()
    {
        if (RoomController.instance.DoesRoomExist(X - 1, Y))
        {
            return RoomController.instance.FindRoom(X - 1, Y);
        }
        return null;
    }
    public Room GetTop()
    {
        if (RoomController.instance.DoesRoomExist(X, Y + 1))
        {
            return RoomController.instance.FindRoom(X, Y + 1);
        }
        return null;
    }
    public Room GetBottom()
    {
        if (RoomController.instance.DoesRoomExist(X, Y - 1))
        {
            return RoomController.instance.FindRoom(X, Y - 1);
        }
        return null;
    }


    void OnDrawGizmos()
    {
        Gizmos.color = Color.red;
        Gizmos.DrawWireCube(transform.position, new Vector3(Width, Height, 0));
    }

    public Vector3 GetRoomCentre()
    {
        return new Vector3(X * Width, Y * Height);
    }

    void OnTriggerEnter2D(Collider2D other)
    {
        if (other.tag == "Player")
        {
            RoomController.instance.OnPlayerEnterRoom(this);
        }
    }
}

and

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using System.Linq;

public class RoomInfo
{
    public string name;
    public int X;
    public int Y;
}

public class RoomController : MonoBehaviour
{

    public static RoomController instance;

    string currentWorldName = "Basement";

    RoomInfo currentLoadRoomData;

    Room currRoom;

    Queue<RoomInfo> loadRoomQueue = new Queue<RoomInfo>();

    public List<Room> loadedRooms = new List<Room>();

    bool isLoadingRoom = false;
    bool spawnedBossRoom = false;
    bool updatedRooms = false;

    void Awake()
    {
        instance = this;
    }

    void Start()
    {
        //LoadRoom("Start", 0, 0);
        //LoadRoom("Empty", 1, 0);
        //LoadRoom("Empty", -1, 0);
        //LoadRoom("Empty", 0, 1);
        //LoadRoom("Empty", 0, -1);
    }

    void Update()
    {
        UpdateRoomQueue();
    }

    void UpdateRoomQueue()
    {
        if (isLoadingRoom)
        {
            return;
        }

        if (loadRoomQueue.Count == 0)
        {
            if (!spawnedBossRoom)
            {
                StartCoroutine(SpawnBossRoom());
            }
            else if (spawnedBossRoom && !updatedRooms)
            {
                foreach (Room room in loadedRooms)
                {
                    room.RemoveUnconnectedDoors();
                }
                UpdateRooms();
                updatedRooms = true;
            }
            return;
        }

        currentLoadRoomData = loadRoomQueue.Dequeue();
        isLoadingRoom = true;

        StartCoroutine(LoadRoomRoutine(currentLoadRoomData));
    }

    IEnumerator SpawnBossRoom()
    {
        spawnedBossRoom = true;
        yield return new WaitForSeconds(0.5f);
        if (loadRoomQueue.Count == 0)
        {
            Room bossRoom = loadedRooms[loadedRooms.Count - 1];
            Room tempRoom = new Room(bossRoom.X, bossRoom.Y);
            Destroy(bossRoom.gameObject);
            var roomToRemove = loadedRooms.Single(r => r.X == tempRoom.X && r.Y == tempRoom.Y);
            loadedRooms.Remove(roomToRemove);
            LoadRoom("End", tempRoom.X, tempRoom.Y);
        }
    }

    public void LoadRoom(string name, int x, int y)
    {
        if (DoesRoomExist(x, y) == true)
        {
            return;
        }

        RoomInfo newRoomData = new RoomInfo();
        newRoomData.name = name;
        newRoomData.X = x;
        newRoomData.Y = y;

        loadRoomQueue.Enqueue(newRoomData);
    }

    IEnumerator LoadRoomRoutine(RoomInfo info)
    {
        string roomName = currentWorldName + info.name;

        AsyncOperation loadRoom = SceneManager.LoadSceneAsync(roomName, LoadSceneMode.Additive);

        while (loadRoom.isDone == false)
        {
            yield return null;
        }
    }

    public void RegisterRoom(Room room)
    {
        if (!DoesRoomExist(currentLoadRoomData.X, currentLoadRoomData.Y))
        {
            room.transform.position = new Vector3(
                currentLoadRoomData.X * room.Width,
                currentLoadRoomData.Y * room.Height,
                0
            );

            room.X = currentLoadRoomData.X;
            room.Y = currentLoadRoomData.Y;
            room.name = currentWorldName + "-" + currentLoadRoomData.name + " " + room.X + ", " + room.Y;
            room.transform.parent = transform;

            isLoadingRoom = false;

            if (loadedRooms.Count == 0)
            {
                CameraController.instance.currRoom = room;
            }

            loadedRooms.Add(room);
        }
        else
        {
            Destroy(room.gameObject);
            isLoadingRoom = false;
        }

    }

    public bool DoesRoomExist(int x, int y)
    {
        return loadedRooms.Find(item => item.X == x && item.Y == y) != null;
    }

    public Room FindRoom(int x, int y)
    {
        return loadedRooms.Find(item => item.X == x && item.Y == y);
    }

    public string GetRandomRoomName()
    {
        string[] possibleRooms = new string[] {
            "Empty",
            "Basic1",
            "Basic2",
            "Basic3"
        };

        return possibleRooms[Random.Range(0, possibleRooms.Length)];
    }

    public void OnPlayerEnterRoom(Room room)
    {
        CameraController.instance.currRoom = room;
        currRoom = room;

        StartCoroutine(RoomCoroutine());
    }

    public IEnumerator RoomCoroutine()
    {
        yield return new WaitForSeconds(0.2f);
        UpdateRooms();
    }

    public void UpdateRooms()
    {
        foreach (Room room in loadedRooms)
        {
            if (currRoom != room)
            {
                EnemyController[] enemies = room.GetComponentsInChildren<EnemyController>();
                if (enemies != null)
                {
                    foreach (EnemyController enemy in enemies)
                    {
                        enemy.notInRoom = true;
                       
                    }

                    foreach (Door door in room.GetComponentsInChildren<Door>())
                    {
                        door.doorCollider.SetActive(false);
                    }
                }
                else
                {
                    foreach (Door door in room.GetComponentsInChildren<Door>())
                    {
                        door.doorCollider.SetActive(false);
                    }
                }
            }
            else
            {
                EnemyController[] enemies = room.GetComponentsInChildren<EnemyController>();
                if (enemies.Length > 0)
                {
                    foreach (EnemyController enemy in enemies)
                    {
                        enemy.notInRoom = false;
                        Debug.Log("In room");
                    }

                    foreach (Door door in room.GetComponentsInChildren<Door>())
                    {
                        door.doorCollider.SetActive(true);
                    }
                }
                else
                {
                    foreach (Door door in room.GetComponentsInChildren<Door>())
                    {
                        door.doorCollider.SetActive(false);
                    }
                }
            }
        }
    }
}

This is my first time posting here so if the formatting looks off or anything else appears to be missing, please let me know!

Edit # 2

After some changes to my RoomController script and DungeonGenerator, I am now spawning the rooms as prefabs. However, they duplicate and overlap each other occasionally. Not every time will a room spawn in the start room, but at least one or two overlap throughout the dungeon. Here is the updated RoomController and my DungeonGenerator Script

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using System.Linq;

public class RoomInfo
{
   
    public int X;
    public int Y;
    public Room roomPrefab;
}

public class RoomController : MonoBehaviour
{

    public static RoomController instance;

    string currentWorldName = "Basement";

    RoomInfo currentLoadRoomData;

    Room currRoom;

    Queue<RoomInfo> loadRoomQueue = new Queue<RoomInfo>();

    public List<Room> loadedRooms = new List<Room>();

    bool isLoadingRoom = false;
    bool spawnedBossRoom = false;
    bool updatedRooms = false;

    void Awake()
    {
        instance = this;
    }

    void Start()
    {
        //LoadRoom("Start", 0, 0);
        //LoadRoom("Empty", 1, 0);
        //LoadRoom("Empty", -1, 0);
        //LoadRoom("Empty", 0, 1);
        //LoadRoom("Empty", 0, -1);
    }

    void Update()
    {
        UpdateRoomQueue();
    }

    // Manages the queue of rooms to be loaded, then loads one at a time
    void UpdateRoomQueue()
    {
        // If a room is already being loaded, don't do anything
        if (isLoadingRoom)
        {
            return;
        }
        // If there are no more rooms to load, spawn boss room and update the existing rooms
        if (loadRoomQueue.Count == 0)
        {
            // If the boss room hasn't spawned yet, spawn it
            if (!spawnedBossRoom)
            {
                StartCoroutine(SpawnBossRoom());
            }
            // If the boss rom has spawned and the rooms havn't been updated yet, update them
            else if (spawnedBossRoom && !updatedRooms)
            {
                // Remove unconnected doors from each room that don't lead to another room
                foreach (Room room in loadedRooms)
                {
                    room.RemoveUnconnectedDoors();
                }
                // Update the rooms and mark them as updated
                UpdateRooms();
                updatedRooms = true;
            }
            return;
        }
        // Dequeue the next room to be loaded
        currentLoadRoomData = loadRoomQueue.Dequeue();
        isLoadingRoom = true;
        // Start loading the room Asyncronously
        StartCoroutine(LoadRoomRoutine(currentLoadRoomData));
    }

    IEnumerator SpawnBossRoom()
    {
        // Set the flag to indicate that a boss room has been spawned
        spawnedBossRoom = true;

        // Wait for a short duration before updating the flag
        yield return new WaitForSeconds(0.5f);

        // Check that there are no more rooms to load
        if (loadRoomQueue.Count == 0)
        {
            // Get the last room in the list of loaded rooms and find the existing instance of the boss room
            Room bossRoom = loadedRooms[loadedRooms.Count - 1];
            Room tempRoom = bossRoom.gameObject.GetComponent<Room>();

            // Destroy the boss room game object and remove it from the list of loaded rooms
            Destroy(bossRoom.gameObject);
            loadedRooms.Remove(bossRoom);

            // Load the end room at the coords of the temp room
            //LoadRoom(bossRoomPrefab, tempRoom.X, tempRoom.Y);
        }
    }

    // This method loads a new room if it does not already exist at the given location
    public void LoadRoom(Room roomPrefab, int x, int y)
    {
        // Checks if a room already exists at the given location, and returns if it does
        if (DoesRoomExist(x, y))
        {
            return;
        }

        // Creates a new RoomInfo object with the given name and position on the dungeon grid
        RoomInfo newRoomData = new RoomInfo();
        newRoomData.roomPrefab = roomPrefab;
        newRoomData.X = x;
        newRoomData.Y = y;

        // Adds the new RoomInfo object to the load room queue
        loadRoomQueue.Enqueue(newRoomData);
    }

    // Couroutine that loads a room Asyncronously
    IEnumerator LoadRoomRoutine(RoomInfo info)
    {
        // Instantiate the room prefab
        Room room = Instantiate(info.roomPrefab);

        // Set the room's position based on its grid coordinates and size
        room.transform.position = new Vector3(
            info.X * room.Width,
            info.Y * room.Height,
            0
        );
        Debug.Log("Room width = " + info.X + ", and Room height = " + info.Y);
        // Set the room's coordinates and name
        room.X = info.X;
        room.Y = info.Y;
        room.name = currentWorldName + "-" + info.roomPrefab.name + " " + room.X + ", " + room.Y;

        // Set the room's parent and add it to the list of loaded rooms
        room.transform.parent = transform;
        loadedRooms.Add(room);

        // If this is the first room loaded, set it as the current Camera View.
        if (loadedRooms.Count == 1)
        {
            CameraController.instance.currRoom = room;
        }

        // Yield null to wait for the end of the frame before finishing the coroutine
        yield return null;
    }

    // This function registers a new room in the current world(Basement) and makes it part of the game. Takes the most recent room loaded
    public void RegisterRoom(Room room)
    {
        // Set the room's loading status to false
        isLoadingRoom = false;

        // Set the room's parent and add it to the list of loaded rooms
        room.transform.parent = transform;
        loadedRooms.Add(room);

        // Set the room as the current Camera View if it is the first room loaded
        if (loadedRooms.Count == 1)
        {
            CameraController.instance.currRoom = room;
        }

        // Set the room's name based on the prefab name and grid coordinates
        room.name = currentWorldName + "-" + room.name + " " + room.X + ", " + room.Y;
    }

    // Checks if a room with given x and y coords already exists in the loadedRooms
    public bool DoesRoomExist(int x, int y)
    {
        // Using the Find meothd, we search for a room object with matching x and y values
        // If a match is found, we return true, if not, false.
        return loadedRooms.Find(item => item.X == x && item.Y == y) != null;
    }

    // Checks if a room exists with given x and y coords.
    public Room FindRoom(int x, int y)
    {
        // Use the Find method again to search for a room that matches the given coords
        // If a match is found, return the Room Object
        // Else the method returns null
        return loadedRooms.Find(item => item.X == x && item.Y == y);
    }

    
    public string GetRandomRoomName()
    {
        // Define an array of possible room names
        string[] possibleRooms = new string[] {
            "Empty",
            "Basic1",
            "Basic2",
            "Basic3"
        };
        // Then return a random room name from the possibleRooms array
        return possibleRooms[Random.Range(0, possibleRooms.Length)];
    }

    // When the player enters the room, move the camera to that room. This is legacy code.
    public void OnPlayerEnterRoom(Room room)
    {
        CameraController.instance.currRoom = room;
        currRoom = room;

        StartCoroutine(RoomCoroutine());
    }
    
    // Time waited before loading another room and unpdating the queue
    public IEnumerator RoomCoroutine()
    {
        yield return new WaitForSeconds(0.2f);
        UpdateRooms();
    }

    public void UpdateRooms()
    {
        // First loop through all the loaded rooms
        foreach (Room room in loadedRooms)
        {
            // If the current room isn't the current room being displayed
            if (currRoom != room)
            {
                // Deactivate all the enemies in the room and their colliders
                EnemyController[] enemies = room.GetComponentsInChildren<EnemyController>();
                if (enemies != null)
                {
                    foreach (EnemyController enemy in enemies)
                    {
                        enemy.notInRoom = true;
                       
                    }

                    foreach (Door door in room.GetComponentsInChildren<Door>())
                    {
                        door.doorCollider.SetActive(false);
                    }
                }
                // Deactive all the doors inside the room
                else
                {
                    foreach (Door door in room.GetComponentsInChildren<Door>())
                    {
                        door.doorCollider.SetActive(false);
                    }
                }
            }
            // If the current room is the current room being displayed
            else
            {
                // Activate all enemies in the room and their colliders
                EnemyController[] enemies = room.GetComponentsInChildren<EnemyController>();
                if (enemies.Length > 0)
                {
                    foreach (EnemyController enemy in enemies)
                    {
                        enemy.notInRoom = false;
                        Debug.Log("In room");
                    }

                    foreach (Door door in room.GetComponentsInChildren<Door>())
                    {
                        door.doorCollider.SetActive(true);
                    }
                }
                // Deactivate all the doors in the room
                else
                {
                    foreach (Door door in room.GetComponentsInChildren<Door>())
                    {
                        door.doorCollider.SetActive(false);
                    }
                }
            }
        }
    }
}

And the DungeonGenerator

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class DungeonGenerator : MonoBehaviour
{
    public DungeonGenerationData dungeonGenerationData;
    private List<Vector2Int> dungeonRooms;
    public Room startingRoomPrefab;
    public Room bossRoomPrefab;
    public Room[] roomPrefabs;
    private void Start()
    {
        dungeonRooms = DungeonCrawlerController.GenerateDungeon(dungeonGenerationData);
        SpawnRooms(dungeonRooms);
    }

    private void SpawnRooms(IEnumerable<Vector2Int> rooms)
    {
        // Load the starting room
        RoomController.instance.LoadRoom(startingRoomPrefab, 0, 0);
        List<Vector2Int> dungeonRooms = new List<Vector2Int>();
        int count = 0;
        // Loop through the list of room locations and load each room
        foreach (Vector2Int roomLocation in rooms)
        {
            if (count == 0)
            {
                // Get a random room prefab from the list
                int randomIndex2 = Random.Range(0, roomPrefabs.Length);
                Room randomRoomPrefab2 = roomPrefabs[randomIndex2];

                // Load the room prefab at the given location
                RoomController.instance.LoadRoom(randomRoomPrefab2, roomLocation.x, roomLocation.y);
                Debug.Log("Room spawned at x = " + roomLocation.x + ", and at y = " + roomLocation.y);
                count++;
                continue;
            }
            if (dungeonRooms.Contains(roomLocation))
            {
                continue;
            }
            // Get a random room prefab from the list
            int randomIndex = Random.Range(0, roomPrefabs.Length);
            Room randomRoomPrefab = roomPrefabs[randomIndex];

            // Load the room prefab at the given location
            RoomController.instance.LoadRoom(randomRoomPrefab, roomLocation.x, roomLocation.y);
            Debug.Log("Room spawned at x = " + roomLocation.x + ", and at y = " + roomLocation.y);

            count++;
            dungeonRooms.Add(roomLocation);
        }
    }
}

Upvotes: 0

Views: 250

Answers (1)

uwuna
uwuna

Reputation: 26

Loading each room as a new scene is not a good idea. As you can see here in the Unity docs, NavMeshes on different scenes do not connect unless you manually create a NavMeshLink: https://docs.unity3d.com/Manual/nav-AdditiveLoading.html

So I would suggest to simply make each room a Prefab and Instantiate them in the right places, then bake a single NavMesh for the whole scene. Or bake a NavMesh for each Prefab if you do not need for them to be connected together.

Upvotes: 1

Related Questions