Ahh, Singletons... probably one of the most debated game design patterns in Unity development. A quick Google search often reveals two camps. Those who swear by it, and those who avoid it like the plague.
Not that long ago, I was in the latter camp. But ever since I ran into some issues, I'm switching back to the other side. Now, how do you use persistent and non-persistent Singletons effectively in Unity?
Let’s first briefly go over what a Singleton is for those unfamiliar with the concept.
There can only be one!
A Unity Singleton is a script and a game design pattern that enforces a single active instance at runtime. In other words, only one instance of that system can exist while the game is running.
In Unity, Singletons are commonly used for systems such as audio, saving, or game state. It provides a clear and easy way for other scripts to access that system without having to manually pass references around.
For example, if your player script needs to play a sound when the player jumps, it can access the AudioManager Singleton directly with a single line of code. The problem (and the argument) is that you create a dependency, if you remove or modify the singleton - your code will break.
Two types of Singletons
There are two types of Singletons: persistent Singletons (those that 'live' in every scene), and non-persistent Singletons (those that get instantiated once the scene loads, and destroyed when the scene unloads - aka scene-bound).
When you just started out in Unity, you probably learned to implement a Singleton along these lines of code (using an AudioManager as an example):
Non-persistent Singleton example:
using UnityEngine;
public class AudioManager : MonoBehaviour
{
public static AudioManager Instance;
void Awake()
{
Instance = this;
}
public void PlayJumpSound()
{
// cached reference to an AudioSource and clip go here
}
}
Once the above code is attached to a game-object in Unity, you've created a simple non-persistent Singleton. A class that can be globally accessed by any other script by the following example:
using UnityEngine;
public class Player : MonoBehaviour
{
private void Jump()
{
AudioManager.Instance.PlayJumpSound();
}
}
Now, this Singleton is scene-bound (non-persistent), which means it only gets instantiated when the scene initializes, and destroyed when the scene is no longer active.
If you want to make this Singleton persistent, all you have to do is add a little adjustment in the Awake method.
using UnityEngine;
public class AudioManager : MonoBehaviour
{
public static AudioManager Instance;
private void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
DontDestroyOnLoad(gameObject);
}
public void PlayJumpSound()
{
// cached reference to an AudioSource and clip go here
}
}
What this does is it reads the Awake method and does a couple of checks before the Singleton is marked as persistent. If the instance is already instantiated and the instance is already this, then it simply destroys the instance, and sets the instance to be this script. Right after it ensures that the instance does not get destroyed on load (this actually makes the script persistent).
Now, even when scenes change, the script will still be instantiated and won't get destroyed. This is the basic implementation of this game design pattern.
The more robust and scalable version of this is the inheritance of the Singleton class.
The following script I use myself in every single game that requires a Singleton (persistent or non persistent), and yes, it's just one abstract script:
using UnityEngine;
public abstract class Singleton : MonoBehaviour where T : MonoBehaviour
{
public static T Instance { get; protected set; }
protected virtual void Awake()
{
Instance = this as T;
}
protected virtual void OnApplicationQuit()
{
Instance = null;
Destroy(gameObject);
}
}
public abstract class SingletonPersistent : Singleton where T : MonoBehaviour
{
protected override void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this as T;
DontDestroyOnLoad(gameObject);
}
}
As you can see, it's one single abstract class that takes care both persistent and non-persistent Singletons.
Once you have this script in your Utilities folder, you can start implementing any script that needs to be a singleton in your project thanks to its static level. For instance, a non-persistent AudioManager would look like this:
using UnityEngine;
public class AudioManager : Singleton
{
// no need for the Awake method - it's already handled by the base class.
}
If you want the AudioManager to persist in all scenes, you can simply change the Singleton to SingletonPersistent
Then it would look like:
using UnityEngine;
public class AudioManager : SingletonPersistent
{
// no need for the Awake method - it's already handled by the base class.
// if you need to add certain things to the Awake method you can use:
// protected override void Awake()
// base.Awake();
}
So, there you go, one script that takes care of all your Singleton needs!
When to use Singletons (and when not)
This question is debatable, and it really hinges on the game architecture of your Unity Game Development project as a whole.
In my case, I use Singletons if I'm absolutely sure I only need one script to handle one thing. For instance, it makes sense to me to use one Audio Manger in your game (or one UI Manager). It gets tricky if you have a Player script as a Singleton. If your game features only one player, then it makes sense in a way to make this a Singleton. But, what if later on you decide to implement a co-op feature. Then you'd need another player script, or modify the Player Singleton script in such a way that it can be used as a second or a third player (perhaps with implementing a public enumerator). But doing so will likely turn into a mess of spaghetti code, and it's best to be avoided.
Only make scripts Singletons that need to pass information to do something with the incoming code. For example, my AudioManager only passes through audio information. It doesn't hold anything else - not even audio clips. The same can go for the UI, when the game gets paused, all the UI needs to do is show the UI menu. Just use a Singleton like UIManager.Instance.OnPause(); Then, when the game gets paused you know exactly what is being called.
These things get harder to backtrack if you use an observer pattern. It may look cleaner, and you've indeed decoupled scripts from one another, but try to debug it if you come back to your project at a later time and forgot which script raised the event.
Hopefully this shone another light on the debate :P. Again, don't overengineer things, and keep it simple!
Of course, this is just my opinion regarding the implementation of the Singleton pattern in Unity. If you know of a more efficient way, or a totally different way, please let everyone know in the comment section below.
print("Happy coding!");

No comments:
Post a Comment