Skip to content

Instantly share code, notes, and snippets.

@lordlycastle
Last active May 7, 2024 14:41
Show Gist options
  • Save lordlycastle/64821426eedf222e66218f70e2498757 to your computer and use it in GitHub Desktop.
Save lordlycastle/64821426eedf222e66218f70e2498757 to your computer and use it in GitHub Desktop.
Abstract singleton class for Unity.
using System.Linq;
using UnityEngine;
/// <summary>
/// Abstract singleton class for Unity. Updated with possible errors:
/// - Doesn't create a new singleton if it is in scene.
/// - Singleton has an initialize method which is called on creation, finding one from scene, or awake. Which makes sure initialization always happen before using it.
/// - It doesn't create a new single if you try to access it OnDestroy (e.g. game quit). Returns null instead.
/// </summary>
/// <typeparam name="TSingleton">Type of singleton. (Same as class inheriting from it.)</typeparam>
public abstract class BaseSingleton<TSingleton> : MonoBehaviour
where TSingleton : BaseSingleton<TSingleton>
{
/// <summary>
/// A flag which is set to true when Application.quitting event is fired.
/// Used to stop singleton from spawning if it is used OnDestroy.
/// </summary>
private static bool _applicationIsQuitting = false;
/// <summary>
/// Has initialization code been called?
/// </summary>
private bool _initialized = false;
/// <summary>
/// Backing field of the instance.
/// </summary>
private static TSingleton _instance;
/// <summary>
/// Static property to access singleton. Creates a new one if it doesn't exist.
/// </summary>
public static TSingleton Instance
{
get
{
if (_applicationIsQuitting) return null;
if (_instance != null) return _instance;
// Try to find a singleton in scene.
var foundSingletons = Resources.FindObjectsOfTypeAll<TSingleton>().ToList();
if (foundSingletons.Count != 0) // is not (null or empty)
{
// choose one that has already initialized.
_instance = foundSingletons.FirstOrDefault(singleton => singleton._initialized);
// if none have initialised then choose first one.
if (_instance == null) _instance = foundSingletons[0];
// destroy th rest.
foundSingletons.Remove(_instance);
if (foundSingletons.Count > 0)
{
Debug.LogWarning($"Found more than one singleton of type: {_instance.GetType()} when searching in scene. Make sure scene only has one instance. Removing them.");
for (var idx = foundSingletons.Count - 1; idx >= 1; idx--) Destroy(foundSingletons[idx]);
}
}
else
{
// otherwise create one.
var singleton = new GameObject("Singleton").AddComponent<TSingleton>();
_instance = singleton.GetComponent<TSingleton>();
Debug.Log($"Singleton of type: {nameof(TSingleton)} was created on the fly.");
}
// initialize singleton if it hasn't been initialized.
if (_instance._initialized) return _instance;
_instance.Initialize();
_instance._initialized = true;
return _instance;
}
}
/// <summary>
/// Should we mark this singleton object as don't destroy on load.
/// <remarks>It is best to keep a singleton on one game object as it may mark the game object as don't destroy on load (Unity can be weird).</remarks>
/// </summary>
public bool dontDestroyOnLoad = false;
/// <summary>
/// Sets up the instance field. And names the game object.
/// </summary>
protected virtual void Awake()
{
if (_initialized == false)
{
Initialize();
_initialized = true;
}
}
/// <summary>
/// Needs to be overwritten by child class. Give it a descriptive name with "Singleton" suffix.
/// Return null or empty string to not overwrite name in scene.
/// </summary>
/// <returns>Singleton game object name.</returns>
protected abstract string GetSingletonName();
/// <summary>
/// Initialize is called in Awake and on creation. You should put any code that should run before this singleton is usable.
/// Since it can create an instance on the fly, it's is not guaranteed that Awake will run before you access it.
/// But this will run just after it is created on fly or on Awake.
/// <remarks>Call base on override.</remarks>
/// </summary>
protected virtual void Initialize()
{
if (_instance == null) _instance = this as TSingleton;
else if (_instance != this)
{
Debug.LogWarning($"Singleton already exits of type {GetType().Name}. Destroying.");
Destroy(this);
return;
}
if (dontDestroyOnLoad) DontDestroyOnLoad(this);
if (!string.IsNullOrEmpty(GetSingletonName()))
gameObject.name = GetSingletonName();
_initialized = true;
}
/// <summary>
/// We need this method because we were getting following error:
/// 'Some objects were not cleaned up when closing the scene. (Did you spawn new GameObjects from OnDestroy?)'
/// Which is caused because objects try to access destroyed singleton OnDisable (which is called last on quit) and since it doesn't exists, it create a new instance.
/// </summary>
private void OnApplicationQuit() => _applicationIsQuitting = true;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment