loading...

Advanced scenes management in Unity

flatmango profile image Oleh Zahorodnii Updated on ・4 min read

Have you ever had to think about how to achieve a less painful transition between scenes? If you have a simple game with few scenes that just go one by one, then usually everything goes well. But when the number of scenes becomes larger and they can be loaded in a different order — while their behavior may depend on the input parameters — scene management becomes less trivial.

Here are some popular approaches to scene management with parameters:

  • JSON files — when transitioning from one scene to another, the necessary data is written to JSON/XML file and then read out when the next scene is loaded. Well, at least it is slow (write and read) and sometimes difficult to debug.
  • Huge static class that takes care of all kinds of scene-to-scene transitions and handles initializations for all cases. These objects are very similar to god-objects and often cause memory leaks and low back pain when a new developer tries to figure out what's going on in this thousand of lines of static code.
  • DontDestroyOnLoad GameObject — similar to previous approach, but also with an GameObject in scene with dozens of references in Inspector. It usually looks like one of those huge singletons that each of us has seen in almost every project...

I would like to show you the approach that I've been using for years. It makes things easier to debug, more transparent and understandable what's going on with all that scenes' transitions.

I have a SceneController in every scene. It is responsible for key objects initializations and passing necessary references. You can think of it as an entry point for current scene. The SceneArgs class is used to represent scene arguments. Each scene has its own class representing arguments, which inherits from SceneArgs.

public abstract class SceneArgs
{
    public bool IsNull { get; private set; }
}

Each scene has its own controller class, which inherits from SceneController.

public abstract class SceneController<TController, TArgs> : MonoBehaviour
        where TController :  SceneController<TController, TArgs>
        where TArgs : SceneArgs, new()
{
    protected TArgs Args { get; private set; }


    private void Awake()
    {
        Args = SceneManager.GetArgs<Tcontroller, TArgs>();

        OnAwake();
    }

    protected virtual void OnAwake() {}
}

The reason why I use a separate class for scene arguments is very simple. Initially, the scene load method took arguments as params object[] args array. It was an unified way to load any scene and pass the necessary arguments to it. When the scene controller took control, it parsed this array of objects retrieving all the arguments. But in addition to boxing, there was another problem here — it wasn't obvious to anyone, except the developer of this controller, what types of parameters and in what order should be passed there so that there would be no casting errors. When we write a method or function, we indicate the order and types of arguments to be passed in signature. But with params object[] args we just see an array of objects and to understand what the order and types of arguments the developer has to look at controller code every time to see how the arguments are parsed. I wanted to keep the scenes loading unified (one method to load any scene) and with strong typing of the arguments for each scene. So, this is what generic constraints of SceneController do.

As we know, in order to load a scene using Unity API you need to pass the name or buildIndex of the scene each time the method is called. To avoid this, I use the custom attribute SceneControllerAttribute to bind the controller to specific scene. The only reason I chose the scene name instead of buildIndex is it change much less frequently, based on my experience.

[AttributeUsage(AttributeTargets.Class)]
public sealed class SceneControllerAttribute : Attribute
{
    public string SceneName { get; private set; }


    public SceneControllerAttribute(string name)
    {
        SceneName = name;
    }
}

Let's say we have a MainMenu scene, then we'll have the following scripts for it:

public sealed class MainMenuArgs : SceneArgs
{
    // args' properties
}
[SceneControllerAttribute("MainMenu")]
public sealed class MainMenuController : SceneController<MainMenuController, MainMenuArgs>
{
    protected override void OnAwake()
    {
        // scene initialization
    }
}

And that's it. There is only one thing left. Scenes are managed using tiny static class called SceneManager. It was very important to keep it as small and simple as possible so that is doesn't turn into another god-object with a bunch of dependencies. Its only purpose is to transfer control (along with arguments) form the controller of one scene to the controller of another. All subsequent actions should be performed by the next controller itself.

public static class SceneManager
{
    private static readonly Dictionary<Type,  SceneArgs> args;


    static SceneManager()
    {
        args = new Dictionary<Type,  SceneArgs>();
    }

    private static T GetAttribute<T>(Type type) where T : Attribute
    {
        object[] attributes = type.GetCustomAttributes(true);

        foreach (object attribute in attributes)
            if (attribute is T targetAttribute)
                return targetAttribute;

        return null;
    }

    public static AsyncOperation OpenSceneWithArgs<TController, TArgs>(TArgs sceneArgs)
        where TController   : SceneController<TController, TArgs>
        where TArgs         :  SceneArgs, new()
    {
        Type                type        = typeof(TController);
        SceneControllerAttribute    attribute   = GetAttribute<SceneControllerAttribute>(type);

        if (attribute == null)
            throw new NullReferenceException($"You're trying to load scene controller without {nameof(SceneControllerAttribute)}");

        string sceneName = attribute.SceneName;

        if (sceneArgs == null)
            args.Add(type, new TArgs { IsNull = true });

        return UnityEngine.SceneManagement.SceneManager.LoadSceneAsync(sceneName);
    }

    public static TArgs GetArgs<TController, TArgs>()
        where TController   : SceneController<TController, TArgs>
        where TArgs         :  SceneArgs, new()
    {
        Type type = typeof(TController);

        if (!args.ContainsKey(type) || args[type] == null)
            return new TArgs { IsNull = true };

        TArgs sceneArgs = (TArgs)args[type];

        args.Remove(type);

        return sceneArgs;
    }
}

Let me explain what's going on here. When you call OpenSceneWithArgs() you pass the type of scene controller (TController), type of arguments (TArgs) and arguments themselves (sceneArgs). First of all SceneManager checks if TController has the SceneControllerAttribute. This is important because it determines which scene TController is binded to. We then add the passed sceneArgs to the arguments dictionary. If no arguments were passed, then we create a new instance of TArgs and set IsNull property to true. If everything went smoothly the Unity API is called to load scene with the name provided by SceneControllerAttribute.

The next scene will be loaded and Awake() method of TController will be called. Then, as you saw in SceneController code, TController calls SceneManager.GetArgs() method to get its arguments from arguments dictionary and performs the necessary scene initialization.

Just try this approach in one of your pet projects (even a small one) and you'll see how much more convenient and transparent scene management will be. Fill free to ask any question and good luck with your experiments!

Posted on by:

Discussion

pic
Editor guide
 

Hi! Welcome to dev.to!

Excellent post and well explained. I'm not sure, but I think some things are missing in the code snippets like adding the SceneControllerAttribute to the MainMenuController and maybe the inheritance from MonoBehaviour in the SceneController abstract class.

I am definitely going to try it and tweak it a bit :)

As a side note, IMHO I think this could be problematic if you are working in a team with a game designer that has no development knowledge since these configs will be defined at the code level. He could not create these controllers and scene args by himself to create new scenes.

Thank you for sharing.

 

Hi Alberto!
Thanks for your feedback!

Sure, there should be inheritance from MonoBehavour in SceneController class and appliance of SceneControllerAttribute to MainMenuController. I had to rewrite the article few times for some reasons and missed these things. I’ll fix that.

As for your side note. I totally agree, there should be some work around to give game designers ability to configure these scenes loading args. I never thought about it before cause that never was a problem in my projects. :)

P.S. Fill free to contact me for any questions or ideas of how to improve it.