Advanced scenes management in Unity
Tags: unity3d, gamedev, csharp
Published: 2020-08-03 11:33:34

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`.

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

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

``` cs
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.

``` cs
[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:

``` cs
public sealed class MainMenuArgs : SceneArgs
{
    // args' properties
}
```

``` cs
[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.

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