Singletons between scenes

If I want to use something like a game manager, whatever it has a singleton, how do I put it in another scene with it working properly? make it a DontDestroyOnLoad object? turning it into a prefab?

Yes, you often want to DontDestroyOnLoad it.

The other option is to prefab it and reinstantiate. That’s viable.

However, it’s good practice that whichever you do, to have all your scenes support directly Playing from the Editor. You really do not want to need to load a sequence of scenes for everything to be set up right. Major timesuck. So if using DontDestroyOnLoad, don’t forget to destroy whatever other singleton that tries to initialize itself in each new scene.

The best singleton game objects are the ones you don’t have to add into a scene yourself. They should be able to generate themselves via code, or pull themselves out of Resources/Addressables (or any other manner that works) and instance themselves, in both cases marking themselves as DontDestroyOnLoad to persist between scene loads.

Though definitely consider if any the data you care about in the singleton can be replaced by scriptable objects.

I’ll take it one step further:

If you drag a ā€œsingleton-yā€ script into a scene by hand, you have almost certainly created a defective singleton.

Instead, try this approach:

Simple Singleton (UnitySingleton):

Some super-simple Singleton examples to take and modify:

Simple Unity3D Singleton (no predefined data):

Unity3D Singleton with a Prefab (or a ScriptableObject) used for predefined data:

These are pure-code solutions, DO NOT put anything into any scene, just access it via .Instance

Alternately you could start one up with a RuntimeInitializeOnLoad attribute.

The above solutions can be modified to additively load a scene instead, BUT scenes do not load until end of frame, which means your static factory cannot return the instance that will be in the to-be-loaded scene. This is a minor limitation that is simple to work around.

If it is a GameManager, when the game is over, make a function in that singleton that Destroys itself so the next time you access it you get a fresh one, something like:

public void DestroyThyself()
{
   Destroy(gameObject);
   Instance = null;    // because destroy doesn't happen until end of frame
}

There are also lots of Youtube tutorials on the concepts involved in making a suitable GameManager, which obviously depends a lot on what your game might need.

OR just make a custom ScriptableObject that has the shared fields you want for the duration of many scenes, and drag references to that one ScriptableObject instance into everything that needs it. It scales up to a certain point.

If you really insist on a barebones C# singleton, here’s a highlander (there can only be one):

And finally there’s always just a simple ā€œstatic locatorā€ pattern you can use on MonoBehaviour-derived classes, just to give global access to them during their lifecycle.

WARNING: this does NOT control their uniqueness.

WARNING: this does NOT control their lifecycle.

public static MyClass Instance { get; private set; }

void OnEnable()
{
  Instance = this;
}
void OnDisable()
{
  Instance = null;     // keep everybody honest when we're not around
}

Anyone can get at it via MyClass.Instance., but only while it exists.

Hello,

it’s very simple and probably not the most acurate way of doing it. so it’s just what i do.
I have a scene ā€œloaderā€ that is only loaded once.
I have a a game manager object (game object). This object has manager script attached to it.(monobehaviour)
script calls don’t destroy on load and stores itself in a public static variable called instance on Awake call. then load main menu scene or other scene for the game
then the manager is accecible via Manager.instance. from any script.

This is close to my approach.

So I don’t use a Singleton, but I use a Service Locator pattern… at the end of the day though it’s the same thing with just a couple added bells and whistles.

With that said my approach is that I have a couple parts.

Game manager - the core singleton/service that is intended to be loaded for the game and other singletons/services can branch off of. I often attach it to a prefab in the project (maybe even resources folder and is the only thing I put in Resources) so that way it can be configured at editor time.

StartupScript/BootScene - in my boot scene (where I’ll have things like our logo and what not) is a StartupScript that initializes the core game manager and loads the scene that comes after the boot scene.

DebugStartup script - this script has EDITOR_ONLY wrapped around its logic, is attached to a GameObject that is flagged as EditorOnly, and prefabbed out. You can then drag this into any scene and on start of the game it acts as the StartupScript for you if and only if the game hasn’t previously been initialized.

An example of the Game service:

    public sealed class Game : ServiceComponent<Game>, ISceneLoadedGlobalHandler, IOnSpawnGlobalHandler
    {

        #region Singleton Access

        private static Game _instance;
        private static System.Action<Game> _onInitializationCompleteCallback;
        private static System.Action<Game> _onBeforeInitializeCallback;

        public static bool Initialized { get { return _instance != null; } }

        public static bool Init(System.Action<Game> onComplete = null, System.Action<Game> onBeforeInitializeCallback = null)
        {
            if (_instance != null) return false;

            _instance = Services.Get<Game>();
            if (_instance != null) return false;

            _onInitializationCompleteCallback = onComplete;
            _onBeforeInitializeCallback = onBeforeInitializeCallback;
            try
            {
                _instance = Services.LoadResourceService<Game>("Wreckage.GameController");
                return true;
            }
            catch
            {
                _onBeforeInitializeCallback = null;
                return false;
            }
        }

        #endregion

        #region Fields

        [SerializeField]
        [Range(0f, 1f)]
        private float _defaultVolume = 1f;

        [SerializeField, UnityEngine.Serialization.FormerlySerializedAs("_vehiclePrefabs")]
        [ReorderableArray]
        private List<WreckageVehicle> _playerVehiclePrefabs = new List<WreckageVehicle>();
        [SerializeField]
        [ReorderableArray]
        private List<WreckageVehicle> _otherVehiclePrefabs = new List<WreckageVehicle>();

        [System.NonSerialized]
        private List<WreckageVehicle> _allVehicles;
        [System.NonSerialized]
        private HashSet<UnityEngine.Object> _pauseTokens = new HashSet<UnityEngine.Object>();
        [System.NonSerialized]
        private HashSet<UnityEngine.Object> _stallTokens = new HashSet<UnityEngine.Object>();

        #endregion

        #region CONSTRUCTOR

        public Game() : base(Services.AutoRegisterOption.RegisterAndPersist, Services.MultipleServiceResolutionOption.UnregisterSelf, Services.UnregisterResolutionOption.DestroyGameObject) { }

        protected override void OnServiceRegistered(System.Type serviceTypeRegisteredAs)
        {
            _ = OnServiceRegisteredAsync(serviceTypeRegisteredAs);
        }

        private async UniTaskVoid OnServiceRegisteredAsync(System.Type serviceTypeRegisteredAs)
        {
            _instance = this;
            if (!GameLoop.Initialized) GameLoop.Init();

            if (_onBeforeInitializeCallback != null)
            {
                var d = _onBeforeInitializeCallback;
                _onBeforeInitializeCallback = null;
                d(this);
            }

            //################
            // Initialize Services - this should always happen first, any given service shouldn't rely on accessing other services on creation
            //########
            {
                //create services
                var sceneManager = Services.Create<ISceneManager, SPSceneManager>(true);
                Messaging.RegisterGlobal<ISceneLoadedGlobalHandler>(this);

                Services.GetOrLoadResourceService<IInputManager, WreckageInputManager>("Wreckage.InputManager", true);

                Messaging.RegisterGlobal<IOnSpawnGlobalHandler>(this);
            }

            await UniTask.Yield(); //let all auto registering services do their thing

            this.LoadPlayerPrefs();

            await UniTask.WaitForEndOfFrame(this);

            Debug.Log("GAME SETTINGS INITIALIZED");

            if (_onInitializationCompleteCallback != null)
            {
                var d = _onInitializationCompleteCallback;
                _onInitializationCompleteCallback = null;
                d(this);
            }
        }

        #endregion

//SNIP.......

The Game prefab that is loaded by Initalize:
9611087--1363538--upload_2024-1-29_13-6-20.png
(snip other services attached here)

Startup script for in the bootscene:

    public class GameStartup : SPComponent
    {

        [SerializeField]
        private SPEvent _onLoaded = new SPEvent();

        protected override void Start()
        {
            base.Start();

            if (!Game.Initialized)
            {
                Game.Init(this.OnGameInitialized);
            }
            else
            {
                this.OnGameInitialized(Game.Settings);
            }
        }

        private void OnGameInitialized(Game game)
        {
            _onLoaded.ActivateTrigger(this, null);
        }

    }

Note that this just triggers an SPEvent after initialized (similar to a UnityEvent). Then through the editor we setup animations and the sort to load to the next scene:
9611087--1363541--upload_2024-1-29_13-7-59.png

DebugStartup - prefabbed out and dropped in any scene intended for gameplay:

    [DefaultExecutionOrder(DebugStartup.DEFAULT_EXECUTION_ORDER)]
    public class DebugStartup : SPComponent
    {

        public const int DEFAULT_EXECUTION_ORDER = -31979;

#if UNITY_EDITOR

        #region Fields

        [SerializeField]
        public bool StallGameObjectsOnStart = true;

        [SerializeReference]
        [SerializeRefPicker(typeof(LoadSceneOptions), AllowNull = true, AlwaysExpanded = true, DisplayBox = true)]
        public LoadSceneOptions LoadSceneOptions = new DebugStartupLoadOptions();

        #endregion

        protected override void Awake()
        {
            base.Awake();

            var module = this.GetComponent<BaseInputModule>();
            if (module) Destroy(module);
            var evsys = this.GetComponent<EventSystem>();
            if (evsys) Destroy(evsys);

            if (!Game.Initialized)
            {
                Debug.Log("DEBUG INITIALIZING GAME");

                var scene = SceneManager.GetActiveScene();
                var rootgos = new List<GameObject>();
                if (this.StallGameObjectsOnStart)
                {
                    foreach (var obj in scene.GetRootGameObjects())
                    {
                        if (obj.activeSelf)
                        {
                            rootgos.Add(obj);
                            obj.SetActive(false);
                        }
                    }
                }

                Game.Init((g) =>
                {
                    this.OnGameInitialized(rootgos);
                });
            }
        }

        private void OnGameInitialized(List<GameObject> rootgos)
        {
            if (this.LoadSceneOptions is DebugStartupLoadOptions dopts)
            {
                dopts = (DebugStartupLoadOptions)dopts.Clone();
                dopts.rootgos = rootgos;
                Services.Get<ISceneManager>()?.LoadScene(dopts);
            }
            else if (this.LoadSceneOptions != null)
            {
                Services.Get<ISceneManager>()?.LoadScene(this.LoadSceneOptions.Clone());
            }
            else if (rootgos?.Count > 0)
            {
                foreach (var obj in rootgos)
                {
                    obj.SetActive(true);
                }
            }
        }

        #region Special Types

        [System.Serializable]
        private class DebugStartupLoadOptions : LoadSceneOptions
        {

            [SerializeField]
            private SessionDisplayMode _displayMode;
            [SerializeField]
            private SessionMode _sessionMode;

            [System.NonSerialized]
            public List<GameObject> rootgos;

            protected override void DoBegin(ISceneManager manager)
            {
                _ = this.DoBeingAsync(manager);
            }

            private async UniTaskVoid DoBeingAsync(ISceneManager manager)
            {
                if (Services.Get(out WreckageSessionController ctrl))
                {
                    ctrl.ConfigureSession(_displayMode, _sessionMode);
                }

                await UniTask.Yield();
                foreach (var obj in rootgos)
                {
                    obj.SetActive(true);
                }
            }
        }

        #endregion

#endif

    }

9611087--1363547--upload_2024-1-29_13-9-36.png

Note that this one gets a little weird. There’s extra bells and whistles in here for my project.

Since scenes can be loaded into different modes (this is a car combat game that can be single player, local multiplayer, online multiplayer [incomplete], and has various combat modes)… I have a serializedreference to an initialization script on start of the screen that does the special lifting to put the game in that specific mode. I can easily write other ā€˜LoadOptions’ to have other custom initializations if I want. It’s basically my boiler plate way of doing this.

Oh and DebugStartup is set to run with an execution order that’s really super early so that it can freeze/disable all scene objects while the ā€˜Game’ service initializes.

…

At the end of the day though, it really is what you described.