Additive Scene Loading And Awake

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:

IEnumerator LoadingCoroutine ()
    {

        var asyncOp = SceneManager.LoadSceneAsync ("Map", LoadSceneMode.Additive);
        asyncOp.allowSceneActivation = true;

        while (!asyncOp.isDone)
        {
            yield return null;
        }

        SceneManager.SetActiveScene (SceneManager.GetSceneByName ("Map"));
        sceneActivated?.Invoke (); //Note that for later
    }

Tried a SceneManager.sceneLoaded delegate, like this:

using UnityEngine.SceneManagement;
public class TEST_OnSceneLoaded : TEST_OnAwake
{
    protected override void Awake ()
    {
        SceneManager.sceneLoaded += SceneLoaded;
    }


    void SceneLoaded (Scene scene, LoadSceneMode mode)
    {
        SceneManager.sceneLoaded -= SceneLoaded;
        MakeTestObject ("OnSceneLoadedObject");
    }
}

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?

public class TEST_OnSceneActivated : TEST_OnAwake
{

    TEST_SceneActivation scenActivation;

    protected override void Awake ()
    {
        scenActivation = FindObjectOfType<TEST_SceneActivation> ();
        scenActivation.sceneActivated += SceneLoaded;
    }


    void SceneLoaded ()
    {
        scenActivation.sceneActivated -= SceneLoaded;
        MakeTestObject ("OnSceneActivatedObject");
    }
}

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?

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:

AsyncOperation op = SceneManager.LoadSceneAsync(sceneToLoad, LoadSceneMode.Additive);
      
            op.completed += (AsyncOperation o) => {
                SceneManager.SetActiveScene(SceneManager.GetSceneByName(sceneToLoad));
            }

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.

1 Like

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:

        AsyncOperation asyncOp = SceneManager.LoadSceneAsync ("Map", LoadSceneMode.Additive);
        asyncOp.completed += (AsyncOperation o) =>
        {
            SceneManager.SetActiveScene (SceneManager.GetSceneByName ("Map"));
            sceneActivated?.Invoke ();
        };

        asyncOp.allowSceneActivation = true;
        while (!asyncOp.isDone)
        {
            yield return null;
        }

But alas, only the stuff that relied on manual ‘sceneActivated’ delegate appeared where it should be.

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.

Ah, I see. I still had hope for a magic solution :slight_smile:

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.

1 Like

To be fair, the standard monobehaviour template says:

//Use this for initialization
void Start() {
}

Did you try it? I’m trying it right now, but for no success. What I have is

using UnityEngine;
using UnityEngine.SceneManagement;

public class Hacktivator_II : MonoBehaviour
{
    void Awake ()
    {
        SceneManager.SetActiveScene (SceneManager.GetSceneByName (TEST_SceneActivation.loadedSceneName));
    }
}

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 :eyes:

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.

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.

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™ script:

public class Hacktivator : MonoBehaviour
{
public GameObject commonParentObject;

void Awake ()
{
    YourSceneLoadingManager.sceneActive += SceneActivated ;
}


void SceneActivated ()
{
    YourSceneLoadingManager.sceneActive -= SceneActivated ;
    commonParentObject.SetActive (true);
}

}


- 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.
1 Like

Thanks for sharing this! I was worried what was causing my test to fail. This did the magic

1 Like