Managing scenes in Unity without pain or suffering

Have you ever had to think about how to make scene management in your project less painful? When you have a fairly simple game in which there are only a few scenes going one after the other, then, often, everything goes smoothly. But when the number of scenes grows and the transitions between them become more complicated - they can be loaded in a different order and the behavior of some of them should depend on the input parameters - the task becomes less trivial.



Below are several approaches to solving it that I have seen most often:



  • Files - when moving from one scene to another, all the necessary data is written to a JSON / XML file, and when the next scene is loaded, they are read back. At the very least, it is slow (speaking of reading and writing to a file), and the debugging process becomes less convenient.
  • A huge static class that handles all possible scene transitions. They are very similar to divine objects and quite often cause memory leaks, as well as pain in the lower back when a new developer tries to understand what is going on in this thousand lines of static code.
  • DontDestroyOnLoad GameObject - This approach is similar to the previous one, but the GameObject is presented in a scene with a bunch of links in the Inspector. In fact, this is one of those singletons that each of us has seen in most projects ...


I want to show you an approach that I have been using for years. It helps to make transitions more transparent for the developer, it becomes easier to understand where and what is happening, and also to debug.



In every scene I have SceneController. He is responsible for forwarding all necessary links and initializing key objects. In a sense, it can be considered the entry point of the scene. I use a class to represent arguments, SceneArgsand each scene has its own class that represents its arguments and inherits from it SceneArgs.



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


, , 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() {}
}


. , params object[] args. . , . , , — , , ( ) , , . , IDE , . params object[] args , , , . ( ), . where, SceneController.



, name buildIndex , LoadScene() LoadSceneAsync() Unity API. , SceneControllerAttribute, . , buildIndex , , , .



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

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


, MainMenu. , :



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



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


, ( , ). , . SceneManager. , , . . — . .



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;
    }
}


. OpenSceneWithArgs() (TController) , , (TArgs) , , (sceneArgs). , SceneManager , TController SceneControllerAttribute. , , TController. sceneArgs . - , TArgs IsNull true. , Unity API LoadSceneAsyn() , SceneControllerAttribute.



Awake(). , SceneController, TController SceneManager.GetArgs(), , , .



, SceneManager, . , . . !




All Articles