SceneManager.UnloadSceneAsync not Coroutine friendly?

Since Unity 5.5 SceneManager.UnloadScene is marked obsolete and UnloadSceneAsync should be used instead.

I noticed, however, that when using this, more or less randomly my coroutines throw errors for any component, monobehaviour or gameobject they may be referencing.

Obviously objects are being deleted in a more or less random fashion, while all coroutines happily continue running and then complain when objects (even their own transform at times!) go missing.

I don’t think many people are actually unloading scenes. But nonetheless, did anyone encounter this? Is there an easier way than to add checks to all coroutines after every yield statement if all references are still valid (or we’re not in a loading process)?

Here’s what my loading looks like:

        // Wait until loading screen is completely visible
        while (!loadingScreen.Ready) yield return null;

        // Unload active scenes
        foreach (var scene in scenesToUnload)
        {
            SceneManager.UnloadSceneAsync(scene.name);
        }
        GC.Collect();

        // Finish loading animation
        yield return null;
        yield return null;

        // Load new scenes
        yield return StartCoroutine(LoadScenesAsync(scenesToLoad));
        // etc.
1 Like

Did you figure this out? I’m having trouble unloading async myself. It’s possible you can only have one load or unload call active at a time.

1 Like

No, unfortunately not. So if you find anything out I’d still be interested.

Here is my script that I attach to main player or Main Camera. Stuff that you throw into Dont destroy on load.

public class SceneLoader : MonoBehaviour
{

    // WARNING: This script singleton is special. Only Attach this to One player. Avoid multiple player instance in-game at one time
    #region Singleton
    private static SceneLoader _instance;
    public static SceneLoader Instance
    {
        get
        {
#if UNITY_EDITOR
            if (_instance != null) return _instance;
            _instance = FindObjectOfType<SceneLoader>();

            if (_instance == null)
            {
                _instance = new GameObject("SceneLoader").AddComponent<SceneLoader>();
                Debug.LogWarning("Instance Object not created or missing. Created temporary object: " + _instance.GetType().Name, _instance);
            }
#endif
            return _instance;
        }
        set { _instance = value; }
    }

    // This script is special
    private void Awake()
    {
        if (_instance == null)
            _instance = this;
        else
            DestroyImmediate(this.gameObject);
        DontDestroyOnLoad(this);

        if (MainPlayer == null)
            MainPlayer = this.gameObject;

    }
    #endregion

    public static GameObject MainPlayer;

    public static void LoadSceneAddictive(string scene, Action OnLoadScene = null)
    {
        SceneLoader.Instance.StartCoroutine(LoadSceneAsync(scene, OnLoadScene));
    }

    private static IEnumerator LoadSceneAsync(string scene, Action OnLoadScene = null)
    {
        yield return null;

        AsyncOperation ao = SceneManager.LoadSceneAsync(scene,LoadSceneMode.Additive);
        ao.allowSceneActivation = false;

        while (!ao.isDone)
        {
            // [0, 0.9] > [0, 1]
            float progress = Mathf.Clamp01(ao.progress / 0.9f);
            // Loading completed
            if (ao.progress == 0.9f)
            {
                ao.allowSceneActivation = true;
            }

            yield return null;
        }
        if (OnLoadScene != null)
            OnLoadScene.Invoke();
    }

    public static void UnloadSceneAddictive(Scene scene, Action OnLoadScene = null)
    {
        SceneLoader.Instance.StartCoroutine(UnloadSceneAsync(scene, OnLoadScene));
    }

    private static IEnumerator UnloadSceneAsync(Scene scene, Action OnLoadScene = null)
    {
        yield return null;
        AsyncOperation ao = SceneManager.UnloadSceneAsync(scene);
      
        while (!ao.isDone)
        {
            // [0, 0.9] > [0, 1]
            float progress = Mathf.Clamp01(ao.progress / 0.9f);
            // Loading completed
            if (ao.progress == 0.9f)
            {

            }

            yield return null;
        }
        if (OnLoadScene != null)
            OnLoadScene.Invoke();
    }

}

I would normally use:

SceneLoader.LoadSceneAsync(“My 2nd scene”);

It add another scene to your main scene. Then i move my character transform into it. When leaving that area, i just have to:

SceneLoader.UnloadSceneAsync(MyObjectIn2ndScene.Scene);

Do I get this right?

At end of scene / level A:

  • Load temporary scene
  • Move objects with coroutines there
  • Unload scene A
  • Load scene B
  • Unload temporary scene

Sounds like it could work, but how do you deal with references to objects in scene A when it’s being unloaded? You’d still need the extra checks in coroutines… also, the temporary scene is also unloaded async, so it could again happen in the middle of a coroutine, right?

Or maybe I didn’t understand it correctly.

I am thinking now, though, that maybe setting timescale to 0 could prevent any coroutines from running during the loading process and thus removing the issue that various objects are unloaded in random order…

Alternatively, a custom coroutine system could be built (or bought, whatever) that allows execution of coroutines to be paused. I think I still have an advanced coroutine system lying around somewhere that added some whistles and bells like that.

You dont need temporary scene. Unity already have it. It called DontDestroyOnLoad

It prevent all object from being destroyed when loading new scene or destroying old scene.

And my practice would be like this. No coroutines needed.

Scene 1: Main menu. Open scene up.
Move stuff to DontDestroyOnLoad:

  • Main Camera + lighting if needed
  • My Player Character (if you have one)
  • My Loading Canvas (overlay, only show loading word)

Open Scene 2(Level 1):

  • Tell my loading canvas show Black screen with loading text.
  • Call loading scene addictive. Add a new scene on top menu (or you could call LoadSceneMode.Single).
  • UnloadAsync my menu scene.
  • When done, tell my Loading canvas to go back to alpha 0 and show the game (Disable the main camera in DontDestroyOnLoad if you have another camera in scene).

And so on, but what happen if I open my Menu Scene again? I will have to 2 camera, 2 main character yes?
But I my Singleton script destroy all overlap GameObject that have same script SceneLoader attach as component. Therefore you will always have 1 exist camera and 1 character when reopen menu scene.

Thanks for the explanation. It seems to me that we are trying to solve different problems.

My problem is that I have a scene with lots of objects using coroutines that reference other objects. I don’t want any of these objects to remain in the scene.

Let’s say object A refers to object B and has a coroutine where the reference is used.

When I just unload the scene, both objects see destroyed before object A’s coroutine gets to run again.

When I unload the scene async, this can happen: object B gets destroyed. Next frame, object A runs its coroutine, but B is already missing, so there is an exception.

And this can happen randomly, to any object with reference to another in this scene that uses a coroutine. And to prevent it, I’d have to check the references after every yield in every coroutine.

I would just have 1 MonoBehaviour control all coroutine that ever created. So I can stop it all at the same time.
Check null exception for every new coroutine sound like really cumbersome.

Otherwise, I would add 2 scene addictive on top another then wait for all Data transfer(or Gameobject transfer) finish then Unload not needed scene.

Has anybody figured this out, im having the same problem, crashes when unloading the scene using async.

This is why you need to keep track of your coroutines and use StopCoroutine when you wind down a scene.

Thanks, im also wondering why it doesnt crash when run in the editor, or built as a stand alone windows app, but the exact same code crashes in WebGL tho.