Search Unity

Resolved Help making a Metroidvania room system

Discussion in 'Scripting' started by Phoenix248, Oct 11, 2022.

  1. Phoenix248

    Phoenix248

    Joined:
    Sep 20, 2019
    Posts:
    52
    Hi everyone, I'm currently working on a Metroidvania style game, and after some research, I found that the best way to handle room transitioning is by creating a scene for each room and for performance improvement, when the player is in a room, it should load the scenes of nearby rooms offscreen and unload scenes far away.

    My questions are:

    1. Should I position the objects of each scene in the correct X and Y to match the nearby scenes, so when I load it, they will be at the correct position? Or should I position each object from point 0 on the X and Y, and tell unity where to create the scene in relation to the others?
    scenes in unity.PNG


    2. What is the most optimal way to identify which are the nearby scenes of a room in order to load/unload them?
    doorsAndRooms.PNG
     
  2. iLinaza

    iLinaza

    Joined:
    Feb 23, 2019
    Posts:
    23
    Hi!

    For both answers there could be a lot of options, but from my point of view I think I would go with the following design choices:

    Regarding your first question... I think i would go with a solution that don't involve placing rooms manually in the position that they should appear. Minor changes in room design in later development stages (moving a door higher or lower) would involve in a lot of repositioning in every other room. If your game design allows it, I would have the game know where the door connections are, an when a new level spawn I would programmatically position it making the doors position match.

    And about second one, I think I would store relevant level information of scenes in an ScriptableObject, including which other levels it is connected to. With this, after a level is loaded, you would know which levels should also be loaded and which unloaded.

    Hope it helps!
     
    Phoenix248 likes this.
  3. Phoenix248

    Phoenix248

    Joined:
    Sep 20, 2019
    Posts:
    52
    Thanks for the answer! Using Scriptable Objects seems to be the way to reference scenes that are connected. But I'm struggling to think on how to position the loaded scenes in the right spot.

    The process I thought until now was the following:

    1. Check the info in the Scriptable Object of the current scene in order to know which scenes are nearby.

    2. Unload scenes that are not nearby.

    3. Check if any of them are already loaded and load the remaining nearby scenes additively.

    4. Change the position of the objects of the loaded scenes in order to match the door positions.

    The problem is how do I identify which door in a loaded scence should connect to a door in the current scene, and after that move and align the tilemaps and other objects to where they should go. Any Ideas on how to implement that?
     
  4. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,108
    Can you explain why is that a problem?
    If you're having a hard time connecting two entities in your code then all of this might be well above your experience level.
     
  5. Phoenix248

    Phoenix248

    Joined:
    Sep 20, 2019
    Posts:
    52
    I can think of ways to connect them, like creating a door index in each room and use it to know which door should connect to the current room's door, but it doesn't feel very optimal.

    This system may be somewhat above my skill level, but thats why I'm asking it here, to know proper manners to build it, since I haven't found much information about it elsewhere.
     
    Last edited: Oct 11, 2022
  6. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,108
    Well, connecting the doors via some magical index is one way. Another way would be to use strings.

    You might want to consider whether this is something you're supposed to set up (and maintain) manually, or you just want to set it up so that the rest of the system finds its way during runtime.

    In both cases, you want your input to be human readable, so you can check what's what with your eyes. And because it is very easy to remap any initially cumbersome method to simpler integers on game start, but also because this is only occasionally used, I would personally go with strings.

    You may also think in terms of having an enum to identify the doors, because this offers two benefits: a) a nice drop down to select from, b) you can't make a typo. However standard enums in Unity are not a good practice, for a variety of reasons, and so you might use this custom enumeration thingy I've built instead, because it serializes to strings, while also offering everything nice about enums.

    You can also make this non-human-readable, but still user-friendly, by introducing a custom editor which simply shows you the connections somehow. You can then dragndrop and connect things somehow. Now this might not work, or there may be some complications on the way, if you truly want to split your game map into separate scenes. I've never worked with multiple scenes loaded at the same time, so I wouldn't know what the hurdles are.

    If I was you, I'd make a system that intelligently recycles and activates level chunks, based on player's position. This would likely require setting up trigger zones, aka air-locks, which would serve as in-game trackers of player intent. Once you go past such an airlock, the area behind it is slowly scavenged: i.e. toggled off, but not actively destroyed, to prevent garbage collection kicking in -- instead its assets are dissolved into a pool only to be reused by new content, yet to be encountered. This also allows you to have a backtracking player, where if a player would suddenly go back, you just revive the parts that were toggled off, without having to switch between states in memory.

    I would always strive for a seamless experience, both for the player, and for the game's internal states from the programming perspective, without any scene switching whatsoever, but that's just me.

    Of course, this last paragraph assumes a vast Metroidvania without any (concrete) level delineation. If the platformer was level based by design, I wouldn't care about it at all.
     
  7. Phoenix248

    Phoenix248

    Joined:
    Sep 20, 2019
    Posts:
    52
    I've found a reasonable solution. For anyone wondering how I did it:

    Created every room as a separate scene and positioned each of then in the correct X and Y respective to other scenes.

    Every scene has a Cinemachine Virtual Camera and a confiner which contains a polygon collider from a "Bound" gameObject.

    Every Bound object have a a script that checks if a empty gameObject attached to the player (pivot) entered the bound, then it calls the loading method in a room script.

    Every room has a reference of a "MetroidvaniaRoom" scriptable object. Each MetroidvaniaRoom contains the information of it's own name and a list of scriptable objects of nearby rooms

    Code (CSharp):
    1. public class Room : MonoBehaviour
    2. {
    3.     public MetroidvaniaRoom thisRoom;
    4.     public List<string> nearbyRoomsName = new List<string>();
    5.  
    6.     void Start()
    7.     {
    8.         if (!SceneHandler.sceneHandlerInstance.GetCurrentScenesLoaded().Contains(this.gameObject.scene.name))  
    9.             SceneHandler.sceneHandlerInstance.SetSceneAsLoaded(this.gameObject.scene.name);
    10.      
    11.         foreach (MetroidvaniaRoom room in thisRoom.roomsNearby)
    12.         {
    13.             nearbyRoomsName.Add(room.roomName);
    14.         }
    15.     }
    16.  
    17.    public void LoadNearbyLevels()
    18.    {
    19.         for (int i = 0; i < nearbyRoomsName.Count; ++i)
    20.         {
    21.             if (!SceneHandler.sceneHandlerInstance.GetCurrentScenesLoaded().Contains(nearbyRoomsName[i]))
    22.             {
    23.                 AsyncOperation sceneLoading = SceneManager.LoadSceneAsync(nearbyRoomsName[i], LoadSceneMode.Additive);
    24.                 SceneHandler.sceneHandlerInstance.SetSceneAsLoaded(nearbyRoomsName[i]);
    25.             }
    26.         }
    27.         UnloadFarLevels();
    28.     }
    29.    private void UnloadFarLevels()
    30.     {
    31.         for (int i = 0; i < SceneHandler.sceneHandlerInstance.GetCurrentScenesLoaded().Count; ++i)
    32.         {
    33.             if (!nearbyRoomsName.Contains(SceneHandler.sceneHandlerInstance.GetCurrentScenesLoaded()[i])
    34.                 && this.gameObject.scene.name != SceneHandler.sceneHandlerInstance.GetCurrentScenesLoaded()[i])
    35.             {
    36.                 AsyncOperation sceneUnloading = SceneManager.UnloadSceneAsync(SceneHandler.sceneHandlerInstance.GetCurrentScenesLoaded()[i]);
    37.                 SceneHandler.sceneHandlerInstance.SetSceneAsUnloaded(SceneHandler.sceneHandlerInstance.GetCurrentScenesLoaded()[i]);
    38.             }
    39.         }
    40.     }
    So, when you cross the bounds of a new room, it checks the nearby rooms of the entered room, loads the scenes that are not already loaded among the current scenes loaded, and unloads scenes not cointained in the neaby rooms list.

    Hope it helps.