Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. We have updated the language to the Editor Terms based on feedback from our employees and community. Learn more.
    Dismiss Notice

Additive Scene Loading And Awake

Discussion in 'Scripting' started by Soundwolf776, Apr 12, 2019.

  1. Soundwolf776

    Soundwolf776

    Joined:
    Mar 21, 2015
    Posts:
    17
    Game has a Main scene and a Map scene. I load Map scene additively to the Main one.

    Counterintuitively, but by Unity design, Awake() on Map scene scripts gets called before I can set Map scene as Active (via SceneManager.SetActiveScene()).
    Thus, all scripts that instantiate something, end up instantiating their stuff on Menu scene. Result: errors when Map scene eventually gets unloaded - because parts of its stuff got stuck on Main scene.

    For test purposes, the loading function looks like this:
    Code (CSharp):
    1. IEnumerator LoadingCoroutine ()
    2.     {
    3.  
    4.         var asyncOp = SceneManager.LoadSceneAsync ("Map", LoadSceneMode.Additive);
    5.         asyncOp.allowSceneActivation = true;
    6.  
    7.         while (!asyncOp.isDone)
    8.         {
    9.             yield return null;
    10.         }
    11.  
    12.         SceneManager.SetActiveScene (SceneManager.GetSceneByName ("Map"));
    13.         sceneActivated?.Invoke (); //Note that for later
    14.     }

    Tried a SceneManager.sceneLoaded delegate, like this:
    Code (CSharp):
    1. using UnityEngine.SceneManagement;
    2. public class TEST_OnSceneLoaded : TEST_OnAwake
    3. {
    4.     protected override void Awake ()
    5.     {
    6.         SceneManager.sceneLoaded += SceneLoaded;
    7.     }
    8.  
    9.  
    10.     void SceneLoaded (Scene scene, LoadSceneMode mode)
    11.     {
    12.         SceneManager.sceneLoaded -= SceneLoaded;
    13.         MakeTestObject ("OnSceneLoadedObject");
    14.     }
    15. }
    No luck - SceneLoaded gets called before Scene activation, resulting in the aforemented problem with misplaced objects.

    Tried a custom solution - remember a call to a custom 'sceneActivated' delegate in loading coroutine?
    Code (CSharp):
    1. public class TEST_OnSceneActivated : TEST_OnAwake
    2. {
    3.  
    4.     TEST_SceneActivation scenActivation;
    5.  
    6.     protected override void Awake ()
    7.     {
    8.         scenActivation = FindObjectOfType<TEST_SceneActivation> ();
    9.         scenActivation.sceneActivated += SceneLoaded;
    10.     }
    11.  
    12.  
    13.     void SceneLoaded ()
    14.     {
    15.         scenActivation.sceneActivated -= SceneLoaded;
    16.         MakeTestObject ("OnSceneActivatedObject");
    17.     }
    18. }
    This works as intended. Object is created on the Map scene.

    Now I gotta check and rewrite several hundred scripts (the joys of a complex first game, ahh), but that's not what bothers me.

    What bothers is: is this completely custom solution a correct one? I feel like I'm missing something obvious here. Isn't there some kind of simple, built-in solution? I mean, the cases where stuff wants to be instantiated on Awake() + project relies on additive scene loading must be pretty common - do they all have to do weird flexes like that?
     
  2. nilsdr

    nilsdr

    Joined:
    Oct 24, 2017
    Posts:
    374
    Hi,

    In our setup we load additive scenes from a control scene, which I think is similar to your setup. We also ran into this problem of objects instantiating in the wrong scene and solved it like this by responding to the completed event of the asyncoperation:

    Code (CSharp):
    1. AsyncOperation op = SceneManager.LoadSceneAsync(sceneToLoad, LoadSceneMode.Additive);
    2.      
    3.             op.completed += (AsyncOperation o) => {
    4.                 SceneManager.SetActiveScene(SceneManager.GetSceneByName(sceneToLoad));
    5.             }
    If your problem still persists, then I suppose the logical solution would be to move your initialization code from Awake to Start.

    Alternatively, set the scene to active from the Awake call before instantiating the objects which should appear in it, although this is a bit of a nasty solution.
     
    vozcn likes this.
  3. Soundwolf776

    Soundwolf776

    Joined:
    Mar 21, 2015
    Posts:
    17
    By 'solved', do you mean you managed to mark the scene as active before scene's objects got an Awake() call?

    I just tried this in my test project, modifying loader like this:
    Code (CSharp):
    1.  
    2.         AsyncOperation asyncOp = SceneManager.LoadSceneAsync ("Map", LoadSceneMode.Additive);
    3.         asyncOp.completed += (AsyncOperation o) =>
    4.         {
    5.             SceneManager.SetActiveScene (SceneManager.GetSceneByName ("Map"));
    6.             sceneActivated?.Invoke ();
    7.         };
    8.  
    9.         asyncOp.allowSceneActivation = true;
    10.         while (!asyncOp.isDone)
    11.         {
    12.             yield return null;
    13.         }
    But alas, only the stuff that relied on manual 'sceneActivated' delegate appeared where it should be.
     
  4. nilsdr

    nilsdr

    Joined:
    Oct 24, 2017
    Posts:
    374
    No, awake is called before the first frame I suppose, and the behaviour is by design (as you mentioned before).

    https://docs.unity3d.com/Manual/ExecutionOrder.html

    The correct place to instantiate objects is the Start method, as it runs before the first frame. If you really do want to instantiate from Awake, either put the SetActiveScene statement in the awake method before instantiating the object, or create an empty object in the scene root and parent them to that object.

    Last thing you could try is to reference Transform.root https://docs.unity3d.com/ScriptReference/Transform-root.html

    But I expect it will still instantiate in the wrong scene.
     
  5. Soundwolf776

    Soundwolf776

    Joined:
    Mar 21, 2015
    Posts:
    17
    Ah, I see. I still had hope for a magic solution :)

    It is crazy that such a critical bit of info is nowhere to be found in Unity guides. It's the kind of "Best Practice" that should be engraved into both Instantiate and ExecutionOrder manual pages.
     
    KwahuNashoba likes this.
  6. nilsdr

    nilsdr

    Joined:
    Oct 24, 2017
    Posts:
    374
    To be fair, the standard monobehaviour template says:
    Code (CSharp):
    1. //Use this for initialization
    2. void Start() {
    3. }
     
  7. Soundwolf776

    Soundwolf776

    Joined:
    Mar 21, 2015
    Posts:
    17
    Did you try it? I'm trying it right now, but for no success. What I have is
    Code (CSharp):
    1.  
    2. using UnityEngine;
    3. using UnityEngine.SceneManagement;
    4.  
    5. public class Hacktivator_II : MonoBehaviour
    6. {
    7.     void Awake ()
    8.     {
    9.         SceneManager.SetActiveScene (SceneManager.GetSceneByName (TEST_SceneActivation.loadedSceneName));
    10.     }
    11. }
    12.  
    and this script has top priority in Script Execution Order, so it should happen before any other Awake()s.
    But nope, "ArgumentException: SceneManager.SetActiveScene failed; scene 'Map' is not loaded and therefore cannot be set active". Seems like Awake() happens before the scene is marked as loaded o_O

    I have another solution to test now on an actual project, but it's a bit dirtier than this Hacktivator_II.

    I dunno. My logic, for example, was "ok, Start() is called only when the object is enabled, and I better do some heavy-lifting right away when scene loads, rather than wait when the object in question is activated, causing a lag spike right in the middle of gameplay. Thus, Awake()". It makes some sense, right? And big prefab instantiation is pretty much a definition of heavy-lifting, so I used it, like, everywhere.
     
  8. nilsdr

    nilsdr

    Joined:
    Oct 24, 2017
    Posts:
    374
    I thought I remembered the setactivescene on awake working, but I just tried it and it indeed doesnt, sorry my bad.

    I agree there are scenarios in which you want to load things before a gameobject is enabled, but I guess you'll have to do so a different way.

    Reading through the docs I see there is a 'OnLevelWasLoaded' callback, but it it appears to have been deprecated.
     
  9. Soundwolf776

    Soundwolf776

    Joined:
    Mar 21, 2015
    Posts:
    17
    And a very quick, very ugly, but actually sort-of-completely-valid-i-guess solution with a minimum of code.

    From the manual.
    So:
    • Take the additive scene.
    • Parent all it's native objects to a common parent object.
    • Disable just this CPO (thus the states of all children will be preserved)
    • Add an object with a Hacktivator(tm) script:
    • Code (CSharp):
      1.  
      2. public class Hacktivator : MonoBehaviour
      3. {
      4.     public GameObject commonParentObject;
      5.  
      6.     void Awake ()
      7.     {
      8.         YourSceneLoadingManager.sceneActive += SceneActivated ;
      9.     }
      10.  
      11.  
      12.     void SceneActivated ()
      13.     {
      14.         YourSceneLoadingManager.sceneActive -= SceneActivated ;
      15.         commonParentObject.SetActive (true);
      16.     }
      17. }
    • it basically receives a custom callback after the right scene was set active and enables the CPO.
    • Voila! All Awake()s happen in the correct scene and objects are instantiated where they belong.

    It's like the new scene's Awake() happens a few frames late. And as it happens for the entire scene at once, I guess it should work for most common cases. Works in my pretty busy game scene, at least.
     
    achimmihca likes this.
  10. unity_0DBB92696C21910327BA

    unity_0DBB92696C21910327BA

    Joined:
    Mar 31, 2022
    Posts:
    4
    Thanks for sharing this! I was worried what was causing my test to fail. This did the magic
     
    nilsdr likes this.