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. Feel free to ask any question and good luck with your experiments!
Top comments (6)
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 theMainMenuController
and maybe the inheritance fromMonoBehaviour
in theSceneController
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
inSceneController
class and appliance ofSceneControllerAttribute
toMainMenuController
. 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.
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.
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 ofSceneManager
, add a parameterbool additive
and the following to the method body: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:Inside my
MainMenuController
I of course callArgs.SomeInitializer()
and boom, subscene loaded up.Hi there, great post! Nice basis for passing dependencies between scenes.
By the way, I believe the code for
SceneManager.OpenSceneWithArgs
skips storing non-nullSceneArgs
instances in the dictionary, meaning loadedSceneController
instances will only receive uninitialized instances. Perhaps a fix: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!