0
\$\begingroup\$

I have an audio manager that plays background music for a game I'm making in Unity. Since the music persists between some scenes, I've set up a means of not destroying the audio manager between all scenes and just changing the track whenever the scene changes to one that has a different track. I've accomplished this by implementing my audio manager on the main menu screen, but none of the subsequent levels, since I don't want multiple managers. However, when I launch a scene from Unity, I have a difficult time debugging with no audio manager. Is there a way to instantiate an audio manager if there is none in a scene? Thanks!

\$\endgroup\$

2 Answers 2

2
\$\begingroup\$

To make Marunia's suggestion more explicit, you can use a "lazily-initialized" singleton pattern to spawn your audio manager just-in-time when someone asks for it, even if it wasn't in the scene to begin with. That could look like this:

public class PersistentManager<T> : MonoBehaviour where T : PersistentManager<T> {

    static T _instance;
    public static T Instance { 
        get
        {
            if (_instance == null) {
                _instance = FindObjectOfType<T>();
                if (_instance == null) {
                    _instance = (new GameObject(typeof(T).Name)).AddComponent<T>();
                }
            }
            return Instance;
        }
    }


    void Awake() {
        if (_instance != null) {
            // We've loaded back to a scene that already includes this
            // object! Destroy ourselves to let the existing instance stay.
            Destroy(gameObject);
            return;
        }

        DontDestroyOnLoad(gameObject);
        _instance = (T)this;
    }
}

Then you can declare your audio manager class as

public class AudioManager : PersistentManager<AudioManager>

and other scripts could get (or create on demand) a reference to the solitary instance of it and access its properties and methods like so:

AudioManager.Instance.DoAThing();

There are a couple of problems with this lazy spawning approach though:

  • When we can't find a matching manager in the scene, we spawn a new one with default settings. That will prevent a null reference exception, but it won't play music you'd assigned to your manager in the inspector in your Menu scene or in a prefab. If the manager depends on child objects to function, those won't be spawned by this script either.

    We could partially address this by searching the assets folder for a matching prefab, and instantiating that, or hard-coding a path to load it from. But that still leaves another issue:

  • Your manager won't exist until someone asks for it. That might not be a problem for managers whose only job is to service requests, but if the manager is supposed to be doing something passively in the background - like playing music - that background activity won't start until the first time the manager is requested. So that could create a weird silence at the start of your level, or force you to put unnecessary AudioManager.Instance calls in places just to kickstart the manager if it's absent.

Here's another strategy that addresses these problems. We'll make a ScriptableObject asset that keeps track of what managers we want spawned in any scene we load:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

// Split out the part that we only use for in-editor testing.
#if UNITY_EDITOR
using UnityEditor;

[CreateAssetMenu(fileName = "ManagerSpawner.asset", menuName = "Utilty/Manager Spawner")]
public partial class ManagerSpawner : ScriptableObject
{
    // Uncheck to stop auto-spawning. 
    //This lets you switch between different sets of objects for testing.
    [SerializeField] bool enabled = true;

    // Fill this with references to the prefabs you want to spawn.
    [SerializeField] PersistInAllScenes[] objectsToSpawn;

    // When we enter play mode, spawn the desired managers.
    [RuntimeInitializeOnLoadMethod]
    static void SpawnManagers() {
        if (_alreadySpawned) return;

        string[] guids = AssetDatabase.FindAssets($"t:{typeof(ManagerSpawner).Name}");
        foreach (var guid in guids)
        {
            string path = AssetDatabase.GUIDToAssetPath(guids[0]);
            var asset = AssetDatabase.LoadAssetAtPath<ManagerSpawner>(path);
            if (asset.enabled) {
                foreach(var item in asset.objectsToSpawn) {
                    Instantiate(item);
                }
            }
        }
    }
}
#endif

// The rest is just a stub to turn it off when needed.
public partial class ManagerSpawner : ScriptableObject
{
    static bool _alreadySpawned = false;

    public static void CancelSpawn() { _alreadySpawned = true; }
}

Now in your Assets > Create menu, you can create a Manager Spawner asset in your project folder, and use the inspector to load it up with references to the prefabs that should be spawned in every scene.

On those prefabs, you just need a simple script:

public class PersistInAllScenes : MonoBehaviour
{
    void Awake() {
        // Usual persistence path: DontDestroyOnLoad.
        DontDestroyOnLoad(gameObject);

        // Tell the manager spawner it doesn't need to do anything.
        // (In case we loaded the menu scene where these objects already live.)
        ManagerSpawner.CancelSpawn();
    }
}

This turns off the spawn-on-start-up behaviour if you entered play mode in a scene that already contained your persistent objects.

By using [RuntimeInitializeOnLoadMethod], we ensure these managers are present as we enter play mode, not delayed until the first request. And since we're loading them from prefabs, they'll come in with any inspector setup or dependencies they need to do their work. And we managed to do that without an expensive search through all prefabs, or hard-coding any asset paths. You can have multiple different versions of your manager prefabs, or multiple different sets of managers, and swap between them during testing by explicitly controlling which ones should be spawned.

\$\endgroup\$
1
\$\begingroup\$

Is there a way to instantiate an audio manager if there is none in a scene?

Yes - you can check if your audio manager already exists by calling Object.FindObjectOfType(). You could then do something like this:

var audioManager = Object.FindObjectOfType<YourAudioManager>();
if(audioManager == null)
{
    // Instantiate your audioManager
}

Another way would be to use some static reference e.g., a Singleton pattern to track your audio manager instance.

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .