DEV Community

Oleh Zahorodnii
Oleh Zahorodnii

Posted on • Updated on

Advanced scenes management in Unity

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; }
}
Enter fullscreen mode Exit fullscreen mode

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() {}
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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

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

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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. Feel free to ask any question and good luck with your experiments!

Top comments (6)

Collapse
 
albertofdzm profile image
Alberto Fernandez Medina • Edited

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.

Collapse
 
flatmango profile image
Oleh Zahorodnii • Edited

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. Feel free to contact me for any questions or ideas of how to improve it.

Collapse
 
amosbatista profile image
Amós Batista

Hello, hope you turn back here and read it anyway.

Thank you so much for build this sollution. It's so versatile and easy to use.

Best regards.

Collapse
 
krummja profile image
Jonathan Crum • Edited

So I just ran across this while researching solutions for scene management in code, and I have to say this is really slick! I actually want to offer a small addition that I find extremely handy:

In the OpenSceneWithArgs method of SceneManager, add a parameter bool additive and the following to the method body:

LoadSceneMode mode = additive ? LoadSceneMode.Additive : LoadSceneMode.Single;
return UnityEngine.SceneManagement.SceneManager.LoadSceneAsync(sceneName, mode);
Enter fullscreen mode Exit fullscreen mode

Then, in any given SceneController subclass, you can call subscenes, leaving the main scene to be a master controller and additively loading additional scenes as needed:

public sealed class MainMenuArgs : SceneArgs
{
    TestSceneArgs testSceneArgs = new TestSceneArgs();
    SceneManager.OpenSceneWithArgs<TestSceneController, TestSceneArgs>(testSceneArgs, true);
}
Enter fullscreen mode Exit fullscreen mode

Inside my MainMenuController I of course call Args.SomeInitializer() and boom, subscene loaded up.

Collapse
 
rahilp profile image
Rahil Patel • Edited

Hi there, great post! Nice basis for passing dependencies between scenes.

By the way, I believe the code for SceneManager.OpenSceneWithArgs skips storing non-null SceneArgs instances in the dictionary, meaning loaded SceneController instances will only receive uninitialized instances. Perhaps a fix:

args.Add(type, sceneArgs ?? new TArgs { IsNull = true });
Enter fullscreen mode Exit fullscreen mode
Collapse
 
spawncamper profile image
spawncamper

Hello Oleh! Thank you for an excellent post.
I am relatively new to Unity and C#. Much of the code that you explained in this post is above my head (abstract classes, private classes, protected classes, getters, setters and so on).

Would you recommend a starting point where I can pick up all the necessary tools in order to be able to implement a system such as the one that you described above?
Thank you!