Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@dvddarias
Last active December 20, 2021 14:29
Show Gist options
  • Star 34 You must be signed in to star a gist
  • Fork 7 You must be signed in to fork a gist
  • Save dvddarias/7af26f6588bc61dfb43682cbc7ed889e to your computer and use it in GitHub Desktop.
Save dvddarias/7af26f6588bc61dfb43682cbc7ed889e to your computer and use it in GitHub Desktop.
Better Unity Singleton Class
/************************************************************
* Better Singleton by David Darias
* Use as you like - credit where due would be appreciated :D
* Licence: WTFPL V2, Dec 2014
* Tested on Unity v5.6.0 (should work on earlier versions)
* 03/02/2017 - v1.1
* **********************************************************/
using System;
using UnityEngine;
using SingletonScriptableObjectNamespace;
public class SingletonScriptableObject<T> : SingletonScriptableObjectNamespace.BehaviourScriptableObject where T : SingletonScriptableObjectNamespace.BehaviourScriptableObject
{
//Private reference to the scriptable object
private static T _instance;
private static bool _instantiated;
public static T Instance
{
get
{
if (_instantiated) return _instance;
var singletonName = typeof(T).Name;
//Look for the singleton on the resources folder
var assets = Resources.LoadAll<T>("");
if (assets.Length > 1) Debug.LogError("Found multiple " + singletonName + "s on the resources folder. It is a Singleton ScriptableObject, there should only be one.");
if (assets.Length == 0)
{
_instance = CreateInstance<T>();
Debug.LogError("Could not find a " + singletonName + " on the resources folder. It was created at runtime, therefore it will not be visible on the assets folder and it will not persist.");
}
else _instance = assets[0];
_instantiated = true;
//Create a new game object to use as proxy for all the MonoBehaviour methods
var baseObject = new GameObject(singletonName);
//Deactivate it before adding the proxy component. This avoids the execution of the Awake method when the the proxy component is added.
baseObject.SetActive(false);
//Add the proxy, set the instance as the parent and move to DontDestroyOnLoad scene
SingletonScriptableObjectNamespace.BehaviourProxy proxy = baseObject.AddComponent<SingletonScriptableObjectNamespace.BehaviourProxy>();
proxy.Parent = _instance;
Behaviour = proxy;
DontDestroyOnLoad(Behaviour.gameObject);
//Activate the proxy. This will trigger the MonoBehaviourAwake.
proxy.gameObject.SetActive(true);
return _instance;
}
}
//Use this reference to call MonoBehaviour specific methods (for example StartCoroutine)
protected static MonoBehaviour Behaviour;
public static void BuildSingletonInstance() { SingletonScriptableObjectNamespace.BehaviourScriptableObject i = Instance; }
private void OnDestroy(){
_instantiated = false;
_instance = null;
}
}
// Helper classes for the SingletonScriptableObject
namespace SingletonScriptableObjectNamespace
{
#if UNITY_EDITOR
//Empty custom editor to have cleaner UI on the editor.
using UnityEditor;
[CustomEditor(typeof(BehaviourProxy))]
public class BehaviourProxyEditor : Editor
{
public override void OnInspectorGUI(){}
}
#endif
public class BehaviourProxy : MonoBehaviour
{
public IBehaviour Parent;
public void Awake() { if (Parent != null) Parent.MonoBehaviourAwake(); }
public void Start() { if (Parent != null) Parent.Start(); }
public void Update() { if (Parent != null) Parent.Update(); }
public void FixedUpdate() { if (Parent != null) Parent.FixedUpdate(); }
}
public interface IBehaviour
{
void MonoBehaviourAwake();
void Start();
void Update();
void FixedUpdate();
}
public class BehaviourScriptableObject : ScriptableObject, IBehaviour
{
public void Awake() { ScriptableObjectAwake(); }
public virtual void ScriptableObjectAwake() { }
public virtual void MonoBehaviourAwake() { }
public virtual void Start() { }
public virtual void Update() { }
public virtual void FixedUpdate() { }
}
}
@dvddarias
Copy link
Author

dvddarias commented Jun 14, 2017

When it comes to the singleton pattern in Unity, I have always used the classic implementation, that is the one implemented using a MonoBehaviour as base class. In fact it is "officially" explained in this Unity video tutorial.

The main disadvantage is that this Singleton class only lives during run time. This has some drawbacks:

  • There is no way of directly editing the singleton fields other than changing the code.
  • No way to store a reference to other assets on the Singleton.
  • No way of setting the singleton as the destination of a Unity UI event. I end up using what i call "Proxy Components" that its sole propose is to have 1 line methods that call "GameManager.Instance.SomeGlobalMethod()".

There are implementations that try to solve this using ScriptableObjects as base class but lose the run time benefits of the MonoBehaviour. This implementation solves this problems by using a ScriptableObject as a base class and an associated MonoBehavior during run time:

  • It is an asset so you can edit its properties on the editor like any other Unity asset.
  • It plays nicely with the Unity serialization process.
  • You can assign references on the singleton to other assets from the editor (dependencies are injected through the editor).
  • Unity events can directly call methods on the Singleton.
  • Can call it from anywhere in your code using "SingletonClassName.Instance"
  • Has access to run time MonoBehaviour events and methods like: Update, Awake, Start, FixedUpdate, StartCoroutine, etc.

Here you have an example GameManager singleton class using the SingletonScriptableObject (with a lot of comments):

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

//this attribute is optional but recommended. It will allow you to create the singleton via the asset menu.
//you need to put the created singleton asset on the Resources folder.
[CreateAssetMenu(fileName = "GameManager", menuName = "Game Manager", order = 0)]
public class GameManager : SingletonScriptableObject<GameManager> {

    //any properties as usual
    public int Lives;
    public int Points;

    //optional (but recommended)
    //this method will run before the first scene is loaded. Initializing the singleton here
    //will allow it to be ready before any other GameObjects on every scene and will
    //will prevent the "initialization on first usage". 
    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
    public static void BeforeSceneLoad() { BuildSingletonInstance(); }

    //optional,
    //will run when the Singleton Scriptable Object is first created on the assets. 
    //Usually this happens on edit mode, not runtime. (the override keyword is mandatory for this to work)
    public override void ScriptableObjectAwake(){
        Debug.Log(GetType().Name + " created." );
    }

    //optional,
    //will run when the associated MonoBehavioir awakes. (the override keyword is mandatory for this to work)
    public override void MonoBehaviourAwake(){
        Debug.Log(GetType().Name + " behaviour awake." );

        //A coroutine example:
        //Singleton Objects do not have coroutines.
        //if you need to use coroutines use the atached MonoBehaviour
        Behaviour.StartCoroutine(SimpleCoroutine());
    }

    //any methods as usual
    private IEnumerator SimpleCoroutine(){
        while(true){
            Debug.Log(GetType().Name + " coroutine step." );
            yield return new WaitForSeconds(3);
        }
    }

    //optional,
    //Classic runtime Update method (the override keyword is mandatory for this to work).
    public override void Update(){

    }

    //optional,
    //Classic runtime FixedUpdate method (the override keyword is mandatory for this to work).
    public override void FixedUpdate(){

    }
}

/*
*  Notes:
*  - Remember that you have to create the singleton asset on edit mode before using it. You have to put it on the Resources folder and of course it should be only one. 
*  - Like other Unity Singleton this one is accessible anywhere in your code using the "Instance" property i.e: GameManager.Instance
*/

Any recommendation is of course welcome. ; )

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment