1
\$\begingroup\$

Singletons can be very tricky to work with in Unity, especially when you dont want to give up on Monobehaviour features.

Most examples I've seen make use of Awake to initialise, but this requires careful managing of script execution order, here is my attempt at a "lazy instantiated" (kind-of) singleton that moves all the boilerplate to a parent class which can be inherited from.

    /// <summary>
    /// Singleton base class. <see cref="T"/> must be the type of the child inheriting from this class.
    /// </summary>
    /// <typeparam name="T">The class *inheriting* from this class that will be a singleton</typeparam>
    public abstract class Singleton<T> : MonoBehaviour where T : Singleton<T>
    {
        private static T instance;
        
        /// <summary>
        /// The <see cref="Singleton{T}"/> instance of this class.
        /// </summary>
        public static T Instance
        {
            get
            {
                if (!InstanceExists)
                {
                    TryInitialiseSingleton();
                }
                return instance;
            }
        }
    
        /// <summary>
        /// If an instance of this singleton currently exists
        /// </summary>
        public static bool InstanceExists => instance != null;
        
        /// <summary>
        /// Finds all singletons to ensure theres only one, assigns us to it, dontDestroysUs
        /// </summary>
        private static void TryInitialiseSingleton()
        {
            var singleton = FindSingleton();
            SetSingleton(singleton);
            DontDestroyOnLoad(singleton.transform.root.gameObject);
        }
    
        /// <summary>
        /// Finds the singletons in the scene, if only one exists then that's us so return it, otherwise throw
        /// </summary>
        /// <returns></returns>
        /// <exception cref="Singleton{T}.SingletonDoesNotExistException"></exception>
        /// <exception cref="Singleton{T}.MultipleSingletonInSceneException"></exception>
        private static T FindSingleton()
        {
            var singletons = FindObjectsOfType<T>();
            if (singletons.Length == 0)
                throw new SingletonDoesNotExistException();
            if (singletons.Length > 1)
                throw new MultipleSingletonInSceneException(singletons);
    
            return singletons.First();
        }
    
        /// <summary>
        /// Set the found singleton as the instance
        /// </summary>
        /// <param name="thisSingleton">Reference to the found object</param>
        private static void SetSingleton([NotNull]T thisSingleton)
        {
            if (thisSingleton == null) 
                throw new ArgumentNullException(nameof(thisSingleton), "Cannot set singleton as singleton may not exist");
            
            if (thisSingleton.GetType() != typeof(T)) // this should never happen
                throw new Exception(
                    $"Singletons can only reference their own types, {typeof(Singleton<T>)} cannot be used to template typeof {thisSingleton.GetType()}");
    
            if (instance != null && instance != thisSingleton)
                throw new MultipleSingletonInSceneException(thisSingleton, instance);
    
            instance = thisSingleton;
        }
    
        
        [Serializable]
        protected class SingletonDoesNotExistException : Exception
        {
            public static string DoesNotExistMessage => $"{typeof(Singleton<T>)} is required by a script, but does not exist in scene \"{SceneManager.GetActiveScene().name}\".";
            public SingletonDoesNotExistException():base(DoesNotExistMessage) { }
        } 
        
        [Serializable]
        protected class MultipleSingletonInSceneException : Exception
        {
            public static string MultipleInSceneMessage(IEnumerable<T> singletons)
            {
                var message =
                    $"{typeof(Singleton<T>)} is a singleton, but multiple copies exist in the scene {SceneManager.GetActiveScene().name}: ";
                var singletonNames = string.Join(",", singletons.Select(s => s.name));
                return message +  singletonNames;
            }
    
            public MultipleSingletonInSceneException(params T[] singletons):base(MultipleInSceneMessage(singletons)) { }
        }
    }

Any thoughts or suggestions are very welcome. Singleton<T> where T : Singleton<T> feels like a hack to me, is this going to backfire at all?

\$\endgroup\$
2
  • \$\begingroup\$ Looks fine to me. Possible enhancement is in the case where none are found, to create a GameObject and instantiate an instance and attach it and make that the singleton instance. Then, forgetting to add it to your scene before hitting Play doesn't waste your time having to stop and fix it and replaying. \$\endgroup\$
    – Tenfour04
    Aug 24, 2022 at 1:56
  • \$\begingroup\$ It does NOT require careful managing script execution order. It just requires DISCIPLINE in that during Awake() all you ever do is get and assign references but you DO NOT call any methods on them in Awake(). Calling methods is deferred to OnEnable() or Start(). Then you won't have any script exec order issues, it's as simple as that, and it's best practice anyway (I think even the manual mentions that). Btw this Singleton is overengineered. If you have to FindObjects to get the singleton, you don't know whether or where it exists which indicates a structural problem! \$\endgroup\$
    – CodeSmile
    Feb 22 at 9:55

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Browse other questions tagged or ask your own question.