In an ideal dev environment, we want to be able to drag any object into an empty scene, press play, and be able to test it by itself. Characters, UI, everything.
Turning Monobehaviours into singletons makes that dream unachievable. Countless times playing an empty scene with a simple character spits out a null reference because there’s no Gamecontroller.Instance.StartingHealth.
Inspired by Richard Fine’s GDC talk, a great way around this is to keep all this stuff on a scriptable object.
Here’s a simple GameSettings script, which inherits from our magic ScriptableSingleton class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
using UnityEngine; [CreateAssetMenu(menuName = "Game Settings")] public class GameSettings : ScriptableSingleton<GameSettings> { public int StartingHealth; public Color PrimaryColor; public Color HighlightColor; public float HeaderFontSize; public float BodyFontSize; public bool IsDebugMode; } |
On it, we keep normal, basic information we need across our game.
The magic is in the base class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
using UnityEngine; public abstract class ScriptableSingleton<T> : ScriptableObject where T: ScriptableObject { static T _instance; public static T Instance{ get{ if (!_instance) _instance = Resources.LoadAll<T>("").First(); if (!_instance) Debug.LogError(typeof(T) + " singleton hasn't been created yet. "); return _instance; } } } |
Here, we are inheriting from ScriptableObject instead of Monobehaviour so we can create a .asset file from the right-click menu. The where T: ScriptableObject is something we must have when creating a generic class. Info here. In this case, we’re just saying that children inheriting from this class are also ScriptableObjects but this could be anything else e.g. implements an interface.
The getter for the instance variable is not much different to classic Monobehaviour singletons. On line 10, if there’s no instance already, instead of instantiating one, we load an object of type <T> from the Resources folder in our Unity project and on line 12, we’re logging an error to remind ourselves to create one because we still couldn’t find one.
As long as we’ve created a GameSettings.asset in the Resources folder, we can call GameSettings.Instance.IsDebugMode even from an empty scene, and values changed during play mode are saved.