Unity singleton
First of all, please remember that Singletons are generally not recommended due to various obvious disadvantages:
- Hard to reason about code
- Encourage coupling
- Potential concurrency issue
However, in game development it's super useful to have singleton for many types of global system, including but not limited to:
- Sound system
- Time manager
- Localization
- Tutorial
- Login
- Global Notifier
In this article we will look at some options of Singleton implementation we have in Unity to see what problems they're trying to solve and what assumptions they made.
Requirements
Let's first review some requirements for a useful singleton system in Unity.
Singleton class types
- MonoBehavior based: These types of singletons extends MonoBehavior and shows themselves in the scene hierarchy so that you can look at their exposed members. Another reason is you want them to have an
Update
function to do something useful in it. - Non MonoBehavior based: Traditional singletons in any OO languages. These singletons don't need to be inspected in the scene nor do they need
Update
. We will not focus on these types of singletons.
Singleton lifecycle types
- Has dependencies: These singletons depend on other system or singletons to exist before they can start
- No dependencies: These singletons can be created anytime without waiting for any other entities.
Singleton configurability types
- Has customized parameters: This is a bit overlap with
Has dependencies
singletons. These singletons might: 1. Depend on parameters provided by another system, such as the FileSystem to load certian parameters from disk. OR 2. Need customized parameters or linked prefab. They might be in a prefab themselves such as a LevelManager. - No customized parameters: These singletons behave the same all the time.
Solutions
Fortunately there're already some solutions written in unity3d so we can review and improve them as we wish.
AManagerClass
AManagerClass is a standard implementation which supports MonoBehavior-based singletons. In the code, we refer to the singleton like this:
AManager.instance.Foo ();
The instance
function implementation is as below:
public static AManager instance { get { if (s_Instance == null) { // This is where the magic happens. // FindObjectOfType(...) returns the first AManager object in the scene. s_Instance = FindObjectOfType(typeof (AManager)) as AManager; } // If it is still null, create a new instance if (s_Instance == null) { GameObject obj = new GameObject("AManager"); s_Instance = obj.AddComponent(typeof (AManager)) as AManager; Debug.Log ("Could not locate an AManager object. \ AManager was Generated Automaticly."); } return s_Instance; } }
There are several problems with AManager
:
- We have to duplicate it for all singleton class
- It's slow due to the 2 checks
Surely we can do better!
AutoSingletonManager
- AutoSingletonManager solves the code duplication problem of
AManager
Here's the main code
public abstract class AutoSingletonManager : MonoBehaviour { } public abstract class AutoSingletonManager<T> : AutoSingletonManager where T : AutoSingletonManager { private static bool Compare<T>(T x, T y) where T : class { return x == y; } #region Singleton private static T _instance = default(T); public static T Instance { get { if (!Compare<T>(default(T), _instance)) return _instance; InitInstance(true); return _instance; } } #endregion public void Awake() { InitInstance(false); } public static void InitInstance(bool shouldInitManager) { Type thisType = typeof (T); _instance = FindObjectOfType<T>(); if (Compare<T>(default(T), _instance)) { _instance = new GameObject(thisType.Name).AddComponent<T>(); } //Won't call InitManager from Awake if (shouldInitManager) { (_instance as AutoSingletonManager<T>).InitManager(); } } public virtual void InitManager() { } }
The code uses recursive generic definition. This allow us to reuse code by inheriting from the singleton class as well as from MonoBehaviour
. Note that we don't have to make AutoSingletonManager<T>
inherits from AutoSingletonManager
, instead we can make it inherit from our own class which in turn inherits from MonoBehavior.
It supports initializing the instance from Awake
so maybe it can be used by adding directly it to an object in the scene. However, InitManager
is not called in Awake
so if you call AutoSingletonManager.Instance
after it already awakes then InitManager will never be called.
Another more subtle draw back is it uses an unnecessary comparison if (!Compare<T>(default(T), _instance)) return _instance;
in the Instance
property, so it's not optimal if you call Instance
in a Update
loop or something similar.
Surely we can do better than this.
Secure UnitySingleton
SecureUnitySingleton is the last one of the three singleton implementation in unity3d. It has a very clear idea about the usecases of singletons. There are 3 main supported usecases:
- Exists In Scene: Searches the current scene for an object with the singleton component attached to it.
- Loaded From Resources: This type creates an instance of a prefab with the singleton component attached to it from a Resources folder.
- Create on New GameObject: This type creates a new GameObject and attaches a new instance of the singleton component to it.
There are several issues with approach:
- Even though the code is quite functional, it still suffers the performance penalty from checking too many things in the
Instance
property. - Lack of lifecyle methods. Even though it provides a static
DeleteInstance
method, there's no other methods to execute when the instance is created. You might think thatAwake
can be used for that purpose, but it not explicit enough thatAwake
is called immediately afterInstance
is called. Another possible issue isAwake
will not be called when the prefab is disabled, which should never be desirable but can happen due to a mistake from the developers.
This is quite good but surely we can do better.
Discussions
First of all, we realize that non of the above methods take into account concurrency issue. If we care about concurrency we can do something like this:
public class MySingleton ... { static MySingleton _instance = null; static object singletonLock = new object (); public static MySingleton Instance { get { if (_instance != null) { return; } lock (singletonLock) { if (_instance != null) { return _instance; } var singletonGameObject = new GameObject ("MySingleton"); GameObject.DontDestroyOnLoad (singletonGameObject); _instance = singletonGameObject.AddComponent< MySingleton > (); ... other initialization code } return _instance; } }
By using a simple double lock pattern, we seemingly solve the "concurrency problem". The question is: Do we need to?. Why you have the concurrency problem with singleton? It's because you want to lazy load
the singleton, i.e. only create it when it is used for the first time. But that's exactly what we want to avoid if we want to achieve smooth gameplay. So what you should do instead is just create the bloody singleton at the beginning and forget about it. Remember, preload
is better than lazy load
.
Secondly, I've already mentioned the check for null is redundant if we know we only call the singleton after it's been initialized. In the usecases listed by SecureSingleton
, we see that the singleton is either created by code or already exists in the scene as a GameObject
which is supposed to be configured by designers. In either case, the code to initialize the scene should be a good place for calling the initialization code of the singleton and there's no need for making it sooner.
So what will happen if the Instance
property is called before you initialize the singleton? Obviously an exception will be thrown and this is an ideal case for using Unity Assert because you will have a nice message in development mode but you can also turn it off in production build.
public class GameSingleton<T> : MyBaseBehaviour where T : GameSingleton<T> { ... public static T Instance { get { Assert.IsNotNull (instance); return instance; } }
Note that MyBaseBehaviour
class is a class derived from MonoBehaviour
and is customized specifically for our game. For instance, it might have general code for handling Update
or OnDestroy
.
All that is left now is writing a function CreateInstance
for creating the singleton. This function should be called once only at the exact place where you want it. This means we should design it in such a way to discourage it from being called multiple times randomly by some careless developers. Calling CreateInstance
twice should throw an exception. This is better than mindlessly guard the function in the name of "defensive programming". It will look something like this:
public static T CreateInstance() { Assert.IsNull (instance); GameObject go = new GameObject(typeof(T).Name); instance = go.AddComponent<T>(); instance.OnCreated(); DontDestroyOnLoad(go); return instance; } protected virtual void OnCreated() { }
Here we have a virtual function OnCreated
waiting to be overriden in subclass. To destroy the singleton, we don't have to use fancy static function DeleteInstance
, instead we can just get the singleton gameobject and destroy it like this:
GameObject.Destroy (MySingleton.Instance.gameObject);
When the OnDestroy
function is called, we will set instance
variable to null then allow subclass to do cleanup themselves. The reason why we don't use DeleteInstance
is because we don't want developers who use the API to think there's anything special about destroying the singleton than just destroying the container object.
Finally, we discuss feature destroy if exists in scene
. In my opinion, this feature accounts for a very small proportion of our usecases. Most of the time we have a singleton already exists in scene, or in some prefab is because we want to use Unity to store game data. But that's should not be the main way to deal with game data. We'd better use off-unity format to store game data, like text or database. If we need to store linked resources we can use Scriptable Object. The point is to not rely too much on data stored in prefab or scene just because you want designers to change them easily, they can also change text file or database much more easily than firing up Unity. Also, merging prefab is harder than merging text data.
If you really want to use avoid duplicate singleton in scene, we can simply use this piece of code from SecureUnitySingleton
:
protected virtual void Awake() { if (InstanceExists && instance != this) Destroy(gameObject); }
Implementation
After we've considered different aspect of singletons and what our real usecases are, we finalize our own version Unity singleton as follows
using UnityEngine; using System; using UnityEngine.Assertions; public class SimpleSingleton<T> : BaseMonoBehaviour where T : SimpleSingleton<T> { private static T instance; public static T Instance { get { Assert.IsNotNull (instance, "Instance is null. Please call CreateInstance first!"); return instance; } } public static T CreateInstance () { if (instance != null) { Assert.IsNull (instance, "Instance is not null. Please call CreateInstance once only"); } GameObject go = new GameObject (typeof(T).Name); instance = go.AddComponent<T> (); instance.OnCreated (); return instance; } protected virtual void OnDestroy () { instance = null; } protected virtual void OnCreated () { } }
Then remember to call CreateInstance
when necessary and in appropriate order.
The code with a sample scene is available on github at https://github.com/minhhh/unity-singleton
Regarding the Has Customized Parameters
singletons, there are some implementations of this type using Prefab to set the parameters. This method is not recommended. Instead, we can use ScriptableObject to store the parameter and let the Singleton read from these ScriptableObject. This method will make all the settings centralized in a single location.
TODO: Create SceneSingleton
Comments