Code/Patterns/Singleton/Singleton.cs

Generic Singleton Component base class for Sandbox game objects. It sets and clears a static Instance of the derived type, prevents multiple runtime instances by destroying duplicates, logs warnings in the editor when multiple exist, and participates in hotload state tracking via IHotloadManaged.

ReflectionEmbedded Secret
using System.Collections.Generic;
using Sandbox;
using Sandbox.Diagnostics;

namespace WackyLib.Patterns;

/// <summary>
/// Represents a Singleton component. Which is a component that we can access through an instance, and only one can be allowed in a scene at once.
/// In-game, duplicate instances are automatically destroyed. In the editor (with ExecuteInEditor), multiple instances are permitted to coexist
/// for tooling purposes, but a warning is logged, as only one instance will survive when the game runs (the latest awakened component).
/// </summary>
public abstract class Singleton<T> : Component, IHotloadManaged where T : Singleton<T>
{
#nullable enable
#pragma warning disable SB3000
	// ReSharper disable once MemberCanBePrivate.Global
	public static T? Instance { get; private set; }
#pragma warning restore SB3000
	
	private readonly Logger Log = new( "Singleton" );
	
	protected override void OnAwake()
	{
		// We're running ExecuteInEditor, which means we should ignore instances.
		if ( ExecutingInEditor() )
		{
			Log.Info( $"OnAwake called in editor with ExecuteInEditor, creating once." );
			
			if ( Active )
			{
				if ( Instance.IsValid() && Instance != this )
				{
					Log.Warning( $"Multiple {typeof(T)} instances detected in the scene! Only one will be used in-game." );
				}
				
				Instance = (T)this;
			}
			
			return;
		}
		
		if ( Instance.IsValid() )
		{
			Log.Warning( $"Singleton tried to initialize another {typeof(T)}!" );
			Destroy();
			return;
		}
		
		if ( Active )
		{
			Instance = (T)this;
		}
	}
	
	protected override void OnDestroy()
	{
		if ( Instance == this )
		{
			Instance = null;
		}
	}
	
	void IHotloadManaged.Destroyed( Dictionary<string, object> state )
	{
		state["IsActive"] = Instance == this;
	}
	
	void IHotloadManaged.Created( IReadOnlyDictionary<string, object> state )
	{
		if ( state.GetValueOrDefault( "IsActive" ) is true )
		{
			Instance = (T)this;
		}
	}
	
	private bool ExecutingInEditor()
	{
		if ( !Game.IsEditor )
		{
			return false;
		}
		
		var type = GetType();
		return typeof(ExecuteInEditor).IsAssignableFrom( type );
	}
}