Search Unity

SceneManager.UnloadSceneAsync not Coroutine friendly?

Discussion in 'Scripting' started by Orion, Apr 8, 2017.

  1. Orion

    Orion

    Joined:
    Mar 31, 2008
    Posts:
    261
    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:

    Code (CSharp):
    1.         // Wait until loading screen is completely visible
    2.         while (!loadingScreen.Ready) yield return null;
    3.  
    4.         // Unload active scenes
    5.         foreach (var scene in scenesToUnload)
    6.         {
    7.             SceneManager.UnloadSceneAsync(scene.name);
    8.         }
    9.         GC.Collect();
    10.  
    11.         // Finish loading animation
    12.         yield return null;
    13.         yield return null;
    14.  
    15.         // Load new scenes
    16.         yield return StartCoroutine(LoadScenesAsync(scenesToLoad));
    17.         // etc.
     
    Nick62 likes this.
  2. wightwhale

    wightwhale

    Joined:
    Jul 28, 2011
    Posts:
    397
    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.
     
    Nick62 likes this.
  3. Orion

    Orion

    Joined:
    Mar 31, 2008
    Posts:
    261
    No, unfortunately not. So if you find anything out I'd still be interested.
     
  4. NoDumbQuestion

    NoDumbQuestion

    Joined:
    Nov 10, 2017
    Posts:
    186
    Here is my script that I attach to main player or Main Camera. Stuff that you throw into Dont destroy on load.
    Code (CSharp):
    1. public class SceneLoader : MonoBehaviour
    2. {
    3.  
    4.     // WARNING: This script singleton is special. Only Attach this to One player. Avoid multiple player instance in-game at one time
    5.     #region Singleton
    6.     private static SceneLoader _instance;
    7.     public static SceneLoader Instance
    8.     {
    9.         get
    10.         {
    11. #if UNITY_EDITOR
    12.             if (_instance != null) return _instance;
    13.             _instance = FindObjectOfType<SceneLoader>();
    14.  
    15.             if (_instance == null)
    16.             {
    17.                 _instance = new GameObject("SceneLoader").AddComponent<SceneLoader>();
    18.                 Debug.LogWarning("Instance Object not created or missing. Created temporary object: " + _instance.GetType().Name, _instance);
    19.             }
    20. #endif
    21.             return _instance;
    22.         }
    23.         set { _instance = value; }
    24.     }
    25.  
    26.     // This script is special
    27.     private void Awake()
    28.     {
    29.         if (_instance == null)
    30.             _instance = this;
    31.         else
    32.             DestroyImmediate(this.gameObject);
    33.         DontDestroyOnLoad(this);
    34.  
    35.         if (MainPlayer == null)
    36.             MainPlayer = this.gameObject;
    37.  
    38.     }
    39.     #endregion
    40.  
    41.     public static GameObject MainPlayer;
    42.  
    43.     public static void LoadSceneAddictive(string scene, Action OnLoadScene = null)
    44.     {
    45.         SceneLoader.Instance.StartCoroutine(LoadSceneAsync(scene, OnLoadScene));
    46.     }
    47.  
    48.     private static IEnumerator LoadSceneAsync(string scene, Action OnLoadScene = null)
    49.     {
    50.         yield return null;
    51.  
    52.         AsyncOperation ao = SceneManager.LoadSceneAsync(scene,LoadSceneMode.Additive);
    53.         ao.allowSceneActivation = false;
    54.  
    55.         while (!ao.isDone)
    56.         {
    57.             // [0, 0.9] > [0, 1]
    58.             float progress = Mathf.Clamp01(ao.progress / 0.9f);
    59.             // Loading completed
    60.             if (ao.progress == 0.9f)
    61.             {
    62.                 ao.allowSceneActivation = true;
    63.             }
    64.  
    65.             yield return null;
    66.         }
    67.         if (OnLoadScene != null)
    68.             OnLoadScene.Invoke();
    69.     }
    70.  
    71.     public static void UnloadSceneAddictive(Scene scene, Action OnLoadScene = null)
    72.     {
    73.         SceneLoader.Instance.StartCoroutine(UnloadSceneAsync(scene, OnLoadScene));
    74.     }
    75.  
    76.     private static IEnumerator UnloadSceneAsync(Scene scene, Action OnLoadScene = null)
    77.     {
    78.         yield return null;
    79.         AsyncOperation ao = SceneManager.UnloadSceneAsync(scene);
    80.      
    81.         while (!ao.isDone)
    82.         {
    83.             // [0, 0.9] > [0, 1]
    84.             float progress = Mathf.Clamp01(ao.progress / 0.9f);
    85.             // Loading completed
    86.             if (ao.progress == 0.9f)
    87.             {
    88.  
    89.             }
    90.  
    91.             yield return null;
    92.         }
    93.         if (OnLoadScene != null)
    94.             OnLoadScene.Invoke();
    95.     }
    96.  
    97. }
    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);
     
  5. Orion

    Orion

    Joined:
    Mar 31, 2008
    Posts:
    261
    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.
     
  6. NoDumbQuestion

    NoDumbQuestion

    Joined:
    Nov 10, 2017
    Posts:
    186
    You dont need temporary scene. Unity already have it. It called DontDestroyOnLoad
    https://docs.unity3d.com/ScriptReference/Object.DontDestroyOnLoad.html
    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.
     
  7. Orion

    Orion

    Joined:
    Mar 31, 2008
    Posts:
    261
    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.
     
  8. NoDumbQuestion

    NoDumbQuestion

    Joined:
    Nov 10, 2017
    Posts:
    186
    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.
     
  9. nsmith1024

    nsmith1024

    Joined:
    Mar 18, 2014
    Posts:
    870
    Has anybody figured this out, im having the same problem, crashes when unloading the scene using async.
     
  10. This is why you need to keep track of your coroutines and use StopCoroutine when you wind down a scene.
     
  11. nsmith1024

    nsmith1024

    Joined:
    Mar 18, 2014
    Posts:
    870
    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.