Coroutines, LoadSceneAsync and Unity Execution Order

I thought I understood Unity’s execution order and this lesson made me question everything I thought I knew.

There are a few things Sam said that I was unable to substantiate with any Unity documentation so I’m wondering if I just misunderstood what Sam was trying to say here.

  1. At 3:04 I understand him saying that coroutines run after all Awakes but before all Starts but Unity docs show that all the Coroutine stuff happens after Update (and thus well after Start). What nuance am I misunderstanding in Sam’s explanation?

  2. At 6:10 I understand him saying that SceneManager.LoadSceneAsync() runs after all Awakes but before all Starts. But I can’t find any of that info in the Unity docs on LoadSceneAsync(). But that doesn’t even seem to make sense since you could be calling LoadSceneAsync during Update() and well after Awake and Start(). What’s Sam trying to say here???

  3. So now that has me wondering when do the Awake() and Start() methods in the scene to be loaded by LoadSceneAsync() get called? Since LoadSceneAsync happens in the backgound, it sounds like Awake and Start for the new scene get called in parallel with Update() methods running on the current scene. But this again sounds inconsistent with what I thought I heard Sam say.

What am I missing? I feel like I misunderstood something really basic here.

These order of executions are specific for what’s happening within the Portal’s coroutine, not coroutines in general.

When the scene loads via LoadSceneAsync(), control is returned to the calling coroutine once all of the Awake() methods have been called. However, at this point, the Start() methods have NOT been called.
Then the Portal Script calls Load(), which means that RestoreState() is called next.
Finally, after the next yield return statement, Start() and Update() are finally called on the new object.

Here’s a snapshot of the Player Health script with all of the Life Cycle methods set with a Debug.Log() for clarity.
image
This snapshot was taken immediately after transitioning through a portal filtered for just the player.
The first CaptureState() is immediately before LoadSceneAsync()
The Awake() and OnEnable() methods are called during the creation of the objects in LoadSceneAsync, by the time control returns to the coroutine, both of these methods have been called.
Next, RestoreState is called, followed immediately by CaptureState (those are the Load() and Save() method scalled in the portal.
Finally, after Load() has been called, when the next yield return statement is executed in the coroutine, the system runs Start() and Update().

1 Like

Ah OK. I rewatched it a few times and with this context it makes sense.

Thanks so much. Brian I’d be lost without you. You have stitch together several Unity docs to understand this is is indeed the case. Is there something official in Unity docs that documents this behavior?

Regarding coroutines in general. If I have the following code:

  • do you know if lines 16, 21 and 22 guaranteed to be executed sequentially with nothing in between (besides whatever is happening inside of “StartCoroutine”)? I’ve seen some debates on Unity forums over these types of questions.
  • When does line 23 execute? Does it execute immediate after 22 and then it performs a “yield return”? Or does it perform a yield return, waits for its turn and then execute what is in line 23? Not sure even if I’m using the correct phrasing to form the question.
14        private void SomeRandomMethod()
15        {
16            StartCoroutine(LoadLastScene());
17        }
18
19        private IEnumerator LoadLastScene()
20        {
21            Fader fader = FindObjectOfType<Fader>();
22            fader.FadeOutImmediate();
23            yield return GetComponent<SavingSystem>().LoadLastScene(defaultSaveFile);
24            yield return fader.FadeIn(fadeInTime);
25        }

Minor: what is in your Debug.Log that gets it to print the method you’re inside so nicely? I suspect there might be something within reflection but couldn’t find it.

Not really, or at least I haven’t found it. There is stuff on the order of execution, but what’s happening here in Portal is a special case.

I learned it by Debug.Logging the gollygeewillikers out of the code, and by helping an infinite number of students through errors in their scene transitions.

Here’s a quick way to test this:
Put a Debug.Log before and after the StartCoroutine() call, and put a Debug.Log right after fader.FadeOutImmediate.
Your output should be
“Before”
“Faded”
“After”

The yield return statement will put the instruction in the queue, and it should fire on the next frame. You can test this by adding a Debug.Log in LoadLastScene, which should appear after the “After” statement.

This is called String Interpolation. With this, you can put any variable into the string by enclosing it in brackets. This lets you compose a string once without resorting to "this " + gameObject.name + “is a constructed string”. Instead you can use $“this {gameObject} is an interpolated string”; You don’t need the .name within interpolation because if it’s an object, they system will generally use the ToString() of the object automagically (and gameObject.ToString() is the same thing as gameObject.name).

OK Thanks. That’s what I’ve been trying to do. The Unity docs could be much more clear on these timing nuances.

Thanks. I actually use string interpolation all the time. My question was a bit different but based on your response, I’m guessing there’s no convenient method inside System.Reflection and you had to put a few things together with string interpolation. I wish Unity had a method to the effect of Debug.LogWhereIAmInTheCode()

You can make your own Log that can do it. This one matches Unity’s log signature a bit

using System.Runtime.CompilerServices;
using UnityEngine;

public static class DebugEx
{
    public static void Log(object message, [CallerMemberName] string caller = "", [CallerFilePath] string callerFile = "", [CallerLineNumber] int callerLineNumber = 0)
        => Debug.Log($"{caller} ({callerFile}:{callerLineNumber}): {message}");
}

It’s quite verbose, though.

void Update()
{
    DebugEx.Log("This is a test");
}

prints
Update (C:\full\path\to\your\script.cs:5): This is a test

2 Likes

This topic was automatically closed 24 hours after the last reply. New replies are no longer allowed.

Privacy & Terms