Preload multiple scenes at the same time and activate them on demand in Unity. [Unity, async, simultaneous]


Short answer

Currently we can’t do that in certain cases.

Long asnwer

AsyncOperation.allowSceneActivation tells us:

When allowSceneActivation is set to false then progress is stopped at 0.9. The isDone is then maintained at false. When allowSceneActivation is set to true isDone can complete. While isDone is false, the AsyncOperation queue is stalled. For example, if a LoadSceneAsync.allowSceneActivation is set to false, and another AsyncOperation (e.g. SceneManager.UnloadSceneAsync ) is initialized, the last operation will not be called before the first allowSceneActivation is set to true.

Which means that there is an order to load pre-loaded scenes. If we pre-load scenes (Scene) Main Game and (Scene) Sandbox in this order, we can’t activate (Scene) Sandbox until we activate (Scene) Main Game. If we first activate (Scene) Main Game it will activate without a problem if it was pre-loaded first, and following we can activate (Scene) Sandbox because the queue is empty.

In conclusion, right now we can only load scenes in specific order.

More: (ISSUE) Activating multiple scene at the same time via AsyncOperation.allowSceneActivation .

Solutions

  1. Use Addressables and make scenes as prefabs. Load/Unload instances of this prefab in a manner like you would load/unload scenes.

Code

Git: Preload multiple scenes at the same time and activate them on demand in Unity. (Unity, async, simultaneous)

I have tested this with both SceneManager and Addressables.

PreloadSceneManager.cs

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
using UnityEngine.ResourceManagement.ResourceProviders;
using UnityEngine.SceneManagement;

public class PreloadSceneManager : MonoBehaviour
{
    private Dictionary<string, AsyncOperation> _sceneName_sceneLoadAsyncOperation = new Dictionary<string, AsyncOperation>();

    public AsyncOperation PreloadSceneAsync(string sceneName, LoadSceneMode loadSceneMode)
    {
        if (this._sceneName_sceneLoadAsyncOperation.TryGetValue(key: sceneName, value: out AsyncOperation loadSceneAsyncOperation))
            return loadSceneAsyncOperation;

        loadSceneAsyncOperation = SceneManager.LoadSceneAsync(
            sceneName: sceneName,
            mode: loadSceneMode
        );

        loadSceneAsyncOperation.allowSceneActivation = false;

        this._sceneName_sceneLoadAsyncOperation.Add(
            key: sceneName,
            value: loadSceneAsyncOperation
        );
        
        return loadSceneAsyncOperation;
    }

    public AsyncOperation LoadSceneAsync(string sceneName, LoadSceneMode loadSceneMode)
    {
        if (this._sceneName_sceneLoadAsyncOperation.TryGetValue(key: sceneName, value: out AsyncOperation loadSceneAsyncOperation))
        {
            loadSceneAsyncOperation.allowSceneActivation = true;

            return loadSceneAsyncOperation;
        }
        
        loadSceneAsyncOperation = SceneManager.LoadSceneAsync(
            sceneName: sceneName,
            mode: loadSceneMode
        );
        
        return loadSceneAsyncOperation;
    }

    private Dictionary<string, AsyncOperationHandle<SceneInstance>> _sceneName_sceneLoadAsyncOperationHandle = new Dictionary<string, AsyncOperationHandle<SceneInstance>>();

    public AsyncOperationHandle<SceneInstance> AddressablesPreloadSceneAsync(string key, LoadSceneMode loadSceneMode)
    {
        if (this._sceneName_sceneLoadAsyncOperationHandle.TryGetValue(key: key, value: out AsyncOperationHandle<SceneInstance> loadSceneAsyncOperationHandle))
            return loadSceneAsyncOperationHandle;

        loadSceneAsyncOperationHandle = Addressables.LoadSceneAsync(
            key: key,
            loadMode: loadSceneMode,
            activateOnLoad: false
        );
        
        this._sceneName_sceneLoadAsyncOperationHandle.Add(
            key: key,
            value: loadSceneAsyncOperationHandle
        );

        return loadSceneAsyncOperationHandle;
    }

    public AsyncOperationHandle<SceneInstance> AddressablesLoadSceneAsync(string key, LoadSceneMode loadSceneMode)
    {
        if (this._sceneName_sceneLoadAsyncOperationHandle.TryGetValue(key: key, value: out AsyncOperationHandle<SceneInstance> loadSceneAsyncOperationHandle))
        {
            loadSceneAsyncOperationHandle.Result.ActivateAsync();

            return loadSceneAsyncOperationHandle;
        }

        loadSceneAsyncOperationHandle = Addressables.LoadSceneAsync(
            key: key,
            loadMode: loadSceneMode,
            activateOnLoad: true
        );

        return loadSceneAsyncOperationHandle;
    }

    private void Awake()
    {
        Object.DontDestroyOnLoad(target: this.gameObject);
    }
}

SceneLoader.cs

using System.Collections;
using UnityEngine;
using UnityEngine.ResourceManagement.AsyncOperations;
using UnityEngine.ResourceManagement.ResourceProviders;
using UnityEngine.SceneManagement;

public class SceneLoader : MonoBehaviour
{
    private IEnumerator LoadSceneAsyncProcess(string sceneName, AsyncOperation asyncOperation)
    {
        while (!asyncOperation.isDone)
        {
            Debug.Log($"(scene):{sceneName} (percent complete): {asyncOperation.progress}");

            yield return null;
        }
    }

    private IEnumerator LoadSceneAsyncProcess(string sceneName, AsyncOperationHandle<SceneInstance> asyncOperationHandle)
    {
        while (!asyncOperationHandle.IsDone)
        {
            Debug.Log($"(scene):{sceneName} (percent complete): {asyncOperationHandle.PercentComplete}");

            yield return null;
        }
    }

    (SerializeField) private PreloadSceneManager _preloadSceneManager;
    public PreloadSceneManager _PreloadSceneManager => this._preloadSceneManager;

    (SerializeField) private string _sceneNameOrKey = "(Scene) Preload Scene 1";
    public string _SceneNameOrKey => this._sceneNameOrKey;

    (SerializeField) private KeyCode _loadSceneKeyCode = KeyCode.Alpha1;
    public KeyCode _LoadSceneKeyCode => this._loadSceneKeyCode;

    (SerializeField) private LoadSceneMode _loadSceneMode = LoadSceneMode.Additive;
    public LoadSceneMode _LoadSceneMode => this._loadSceneMode;

    private void Update()
    {
        //if (Input.GetKeyDown(key: KeyCode.Return))
        //{
        //  // Start scene preloading.

        //  Debug.Log($"Preload (scene): {this._sceneName}");

        //  AsyncOperation asyncOperation = this._preloadSceneManager.PreloadSceneAsync(
        //      sceneName: this._sceneName,
        //      loadSceneMode: LoadSceneMode.Additive
        //  );

        //  this.StartCoroutine(
        //      routine: this.LoadSceneAsyncProcess(
        //          sceneName: this._sceneName,
        //          asyncOperation: asyncOperation
        //      )
        //  );
        //}
        
        //if (Input.GetKeyDown(key: this._loadSceneKeyCode))
        //{
        //  Debug.Log($"Load (scene): {this._sceneName}");

        //  AsyncOperation asyncOperation = this._preloadSceneManager.LoadSceneAsync(
        //      sceneName: this._sceneName,
        //      loadSceneMode: LoadSceneMode.Additive
        //  );
        //}

        if (Input.GetKeyDown(key: KeyCode.Return))
        {
            // Start scene preloading.

            Debug.Log($"Preload (scene): {this._sceneNameOrKey}");

            AsyncOperationHandle<SceneInstance> asyncOperationHandle = this._preloadSceneManager.AddressablesPreloadSceneAsync(
                key: this._sceneNameOrKey,
                loadSceneMode: this._loadSceneMode
            );

            this.StartCoroutine(
                routine: this.LoadSceneAsyncProcess(
                    sceneName: this._sceneNameOrKey,
                    asyncOperationHandle: asyncOperationHandle
                )
            );
        }
        
        if (Input.GetKeyDown(key: this._loadSceneKeyCode))
        {
            Debug.Log($"Load (scene): {this._sceneNameOrKey}");

            this._preloadSceneManager.AddressablesLoadSceneAsync(
                key: this._sceneNameOrKey,
                loadSceneMode: this._loadSceneMode
            );
        }
    }

    private void Awake()
    {
        Object.DontDestroyOnLoad(target: this.gameObject);
    }
}