Singletons are a concept of programming where only a single instance of a singleton class can exist during runtime. It's exceptionally useful when there really is only a single instance required. For example, a time morphing script, which controls the Unity time scale.
To ensure fast, more powerful and most importantly, safe use of singletons, the Singletons module contains a few base singleton classes for use.
Within DuskModules, Singletons is used by a variety of modules which provide a feature that really requires a single point of reference. Main examples are TimeControl, ScreenControl, SceneControl and SoundControl. Singletons has no dependencies.
The script Singleton.cs contains the base class for most singletons you would need. There is no need to have a launcher script instantiate a GameObject with it, as the singleton will create itself automatically. When the instance of any singleton is called upon, but it does not exist yet, it creates itself. Upon creation, it fires off protected virtual void Setup(), which can be overriden by inheriting classes to add custom setup code. After being created, any time the instance is requested, it simply returns the instance of what was created.
Because the singleton is setup as soon as it is needed, there can be no race conditions. It's impossible that the singleton doesn't exist yet when you need it, because it creates itself right before. This improves the safety of using singletons exceptionally, allowing you to program without fear of random null references depending on what script runs first. However, avoid referencing other singletons in your Setup method, as you can create infinite loops.
There are two variations of the script based singleton. There is public abstract class Singleton<I> where I is the class that extends it. And there is public abstract class Singleton<I, C> where C is the configuration asset used by this singleton.
A Script Singleton does not have any exposed public setting values for you to modify or tweak. Use custom Config assets for this purpose, which are part of the UtilityCollection module.
Any MonoBehaviour inheriting from ISingletonComponent is given the method void Setup();.
Within the Setup of your singleton implementation, you can add any ISingletonComponent to the singleton object by using gameObject.AddComponent <MySingletonComponent>();.
After adding all desired components, call base.Setup();. This will fire the setup of all added components in the order of their addition.
SingletonComponents are useful as they too will be fully setup before their singleton instance is used.
Each config can be given an editor button used to select and access the configuration file. Simply create the OpenConfig() method as shown in the example.
Let's say you have need of a singleton for a weather control script. It would keep track of the current weather, reading its config file for what weathers are available. It would contain a SingletonComponent called WeatherLight, to ensure the light is properly setup before being used by gameplay code. It would look something like this:
/// <summary> Controls gameplay weather </summary>
public class WeatherController : Singleton<WeatherController, WeatherConfig> {
/// <summary> Weather currently affecting gameplay </summary>
public Weather currentWeather;
/// <summary> Script controlling lighting </summary>
private WeatherLight light;
/// <summary> Sets up the singleton before being used </summary>
protected override void Setup() {
light = gameObject.AddComponent<WeatherLight>();
base.Setup();
currentWeather = config.startingWeather;
}
/// <summary> Changes weather randomly. </summary>
public void ChangeWeatherRandom() {
currentWeather = config.types.GetRandom();
}
}
/// <summary> Configuration file of Weather </summary>
public class WeatherConfig : SingletonConfig<WeatherConfig> {
public List<Weather> types;
public Weather startingWeather;
/// <summary> Opens the weather configuration file </summary>
[MenuItem("Window/Singletons/Weather Settings")]
public static void OpenConfig() {
OpenConfigFile();
}
}
Note that the SingletonComponent is added before base.Setup(), as the base calls the setup of each component. Also note that the config is only accessed after base.Setup(), as that's the moment config is set and ready for use.
In case you need a MonoBehaviour to be a singleton, while also placing it within the scene by hand, you need to use the SingletonBehaviour base MonoBehaviour or the ISingleton interface. Both are singletons that do not instantiate themselves automatically, and need to be placed within the scene by hand. Whether they DontDestroyOnLoad is fully optional.
SingletonBehaviour is automatically a singleton, providing whatever extends from it with the static instance field. The method protected void StayAlive() can be called in order to keep it active between scene loads. An implementation of SingletonBehaviour could look like:
public class MySingleton : SingletonBehaviour<MySingleton> {
}
Anything implementing the interface ISingleton must include the following instance getter manually:
public MySingleton instance {
get { return Singleton.Get<MySingleton>(); }
}
If you're using MonoBehaviour Singletons, it's recommended to create a prefab with the SingletonSetup.cs script attached. Add this prefab to every non-additive scene of your game. SingletonSetup contains a list of all Singletons to instantiate on awake, remembering that they have been created. When you load a different scene during runtime, the SingletonSetup prefab in that scene won't do anything, because it already did in the previous scene.
Using this, you can start the game from any scene without worrying about singletons not being present in the scene, while never worrying about duplicate singletons existing. Since the Singleton Setup is a prefab, you have only one place to modify when adding or removing singleton objects to the game.
The CoroutineRunner is a script singleton, able to run Coroutines given by other scripts.
Coroutines can normally only be executed by Monobehaviour scripts, preventing their use in non-monobehaviour objects such as ScriptableObjects. The CoroutineRunner can be called to run coroutines for these types of objects instead.
Don't forget to add using DuskModules.Singletons; to any script using the module.