0
\$\begingroup\$

I have a scene with lots of lightweight objects and one that takes anywhere between 10 and 45 seconds to load.

At a high level, the Awake method does...

  • Query database
  • For each result do some (fairly heavy) calculations
  • Populate a list
  • Use that list to generate game objects

[Only the last step of which requires the main thread]

I have another scene to display a loading screen. It has a UI panel with a shader that shows an animation.

The Loading screen starts a coroutine to switch level...

private IEnumerator LoadNewScene() {
    AsyncOperation async = SceneManager.LoadSceneAsync("GalaxyMap", LoadSceneMode.Single);
    while (!async.isDone) {
        yield return null;
    }
}

At which point, the animation freezes for 10-45 seconds while the heavy object loads.

In an attempt to make the animation CPU-independent, it's coded as a shader using the built-in _Time property.

I assume this is because my heavy object's Awake() is running on Unity's main thread and thus is blocking updates to the shader.

If so, I can move a lot of the heavy lifting onto a background thread, but I can't work out how to release the thread back to Unity temporarily.

The Awake() method isn't async, and any attempt to create a Task and wait for it will just block the same thread.

Conversely, I could have a completion callback for when the load is done. That would allow me to release the main thread sooner, however, Unity would think my scene had finished loading prematurely and remove the loading screen.

How can I either:

  • Release control of the thrad intermittently so Unity can refresh the UI
  • Explicitly tell Unity when it should consider a scene "loaded" and ready for display

?

\$\endgroup\$
3
  • \$\begingroup\$ Have you consulted How to not freeze the main thread in Unity? \$\endgroup\$
    – DMGregory
    Nov 8 '20 at 2:47
  • \$\begingroup\$ @DMGregory Not explicitly, however, I don't believe there's anything new there? I've already mentioned using threads (and the reference to await was re: the C# job system). I've identified work that can run on another thread (and I can handle dispatching/invoking to get the UI thread to do something afterwards). Where I'm struggling is to get Unity to think "Level still loading" until that process completes if it's not on the main thread (eg SceneManager.LoadSceneAsync won't wait for a Coroutine) \$\endgroup\$
    – Basic
    Nov 8 '20 at 9:52
  • \$\begingroup\$ The example there shows using a member variable to communicate between the threads. So, you set a member variable to a specific value at the end of the thread, and until you read that value on your main thread poll, you keep displaying your waiting state. \$\endgroup\$
    – DMGregory
    Nov 8 '20 at 11:13
0
\$\begingroup\$

As mentioned in the comments, you can use an ordinary variable to pass around information about when the generation is done. Here's one way to do it:

public class GalaxyMap : MonoBehaviour {

    static bool IsGenerationComplete { get; private set; }
    public static void PrepareToGenerate() { IsGenerationComplete = false; }

    public UnityEvent OnGenerationCompleted;

    IEnumerator Start() {
        // Do the bulk of your work on another thread.
        var generatorThread = new Thread(DoBackgroundThreadWork);
        generatorThread.Start();

        // Keep ticking this Start method on the main thread
        // until our threadsafe work is done.
        while (generatorThread.ThreadState != ThreadState.Stopped) {
            yield return null;
        }

        // Now apply any final touch-ups that have to be done on the main thread.
        DoMainThreadWork();

        // Signal to any scripts that were waiting for the load to finish
        // that they can start doing their thing now.
        if (OnGenerationCompleted != null) 
            OnGenerationCompleted.Invoke();

        // Signal that we can hide the loading animation.
        IsGenerationComplete = true;
    }
    
    // Define your worker methods down here...
}

Your LoadNewScene function would then look something like this, with two waits: one for loading the scene file itself, and a second wait for the generation.

private IEnumerator LoadNewScene() {

    GalaxyMap.PrepareToGenerate();
    AsyncOperation async = SceneManager.LoadSceneAsync("GalaxyMap", LoadSceneMode.Single);

    // Wait while the scene file is loaded.
    while (!async.isDone) {
        yield return null;
    }

    // Continue waiting while the generation is in progress.
    while (!GalaxyMap.IsGenerationComplete) {
        yield return null;
    }

    // Now hide the loading animation and show the generated scene.
}

If you had other scripts that were starting to do stuff in their own Awake or Start or Update/etc. that need to happen after generation finishes, you should make them subscribe to the OnGenerationCompleted event instead to control when those behaviours kick off.

\$\endgroup\$
5
  • \$\begingroup\$ I'll give it a try, but won't Unity consider the scene loaded by the time I exit the first loop? What's to prevent it trying to render objects for the next 30+ seconds while we wait for loading to complete? Edit: Ah! Didn't realise start could be a coroutine itself. \$\endgroup\$
    – Basic
    Nov 8 '20 at 12:47
  • \$\begingroup\$ That's in your control. You can choose to leave a black card in front of the scene for those 30 seconds, or start with the renderers disabled and enable them when you want to. \$\endgroup\$
    – DMGregory
    Nov 8 '20 at 13:06
  • \$\begingroup\$ I appreciate the response, but "rewrite every script in your scene to use some new "CanRun" flag" doesn't seem like the right answer. It completely blows separation of concerns our of the water and leaves me with a loading screen that's opinionated about what it's loading (and scripts that need to behave differently depending on how the scene is launched). Not to mention all the hassle of having to recreate Unity's startup ordering in my own code because Unity would be off running scripts before they should be started. \$\endgroup\$
    – Basic
    Nov 9 '20 at 16:36
  • \$\begingroup\$ That's fine. Just add those objects additively in a second loading call after the procedural generation is completed. You can remove the opinionation by making a ScriptableObject that represents the sequence of actions required to construct and load into your complex generated scene, and your loading script can accept that as an argument and just iterate through it. But that's a different problem than "avoid blocking the main thread so the loading screen can animate" so I recommend posting it as a new question with more details about the content of your scene so we can give concrete answers. \$\endgroup\$
    – DMGregory
    Nov 9 '20 at 16:43
  • \$\begingroup\$ I think I'm making headway using the allowSceneActivation flag on Async scene loads, so I'll see if I can do it the right way™ first. If not, I'll definitely post another Q, thanks. \$\endgroup\$
    – Basic
    Nov 9 '20 at 17:00

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.