1
\$\begingroup\$

I'm finding out that unity does not fully destroy all game objects on application quit. The result is that I have a number of async/await functionality that eventually runs and I can see output stream into the console long after the editor has been stopped.

Currently I'm manually disposing / destroying objects in OnApplicationQuit but this seems a bit silly.

Is there a more proper way to make sure all objects in the editor are fully destroyed on application quit ?

** EDIT **

How I'm using async code

public static async Task<Texture2D> DownloadTexture(string uri)
{
    using (
        UnityWebRequest uwr = UnityWebRequestTexture.GetTexture(uri, true)
    )
    {
        var asyncOp = uwr.SendWebRequest();

        if (WeatherTracker.instance == null)
        {
            uwr.Abort(); // todo: something related to this is crashing the editor while textures are still being download @jkr
            return null;
        }
        while (asyncOp.isDone == false)
        await Task.Delay(1000 / MainCamera.instance.MaxFrameRate);

        // return this.uwr.SendWebRequest();
        // Debug.Log($"download tex {z}/{x}/{y}");
        if (uwr.result != UnityWebRequest.Result.Success)
        {
            Debug.LogError($"{uwr.error}: {uri}");
            return null;
        }
        else
        {
            return DownloadHandlerTexture.GetContent(uwr);
        }
    }
}
\$\endgroup\$
8
  • 1
    \$\begingroup\$ You may be interested in How to abort async / await. \$\endgroup\$
    – DMGregory
    Commented Jan 18, 2022 at 21:30
  • \$\begingroup\$ @DMGregory I'm aborting my asyncOps in Destroy but Destroy is never called when you stop running your application via the editor (2021.2.3f1). My workaround for now is to trigger Destroy inside of OnApplicationQuit and even then SOME parts of my script continue to run. It's overkill but I've gone as far as telling coroutines and awaits to check for a running instance of the application before continuing. \$\endgroup\$
    – Jacksonkr
    Commented Jan 18, 2022 at 21:43
  • \$\begingroup\$ Do you mean OnDestroy()? I've just tested in Unity 2020.3.18f and verified that OnDestroy() is called as expected when exiting play mode in the editor. If you're not observing that in your version, that sounds like a bug to report to Unity. Can you edit your question to show a Minimal Complete Verifiable Example so we can use it to reproduce the problem and test potential fixes? \$\endgroup\$
    – DMGregory
    Commented Jan 18, 2022 at 21:53
  • \$\begingroup\$ I was caught up at some point using UnityEngine.Object.Destroy and after a while failed to differentiate that from MonoBehavior.OnDestroy. They are obviously different but my brain stopped working correctly. Thanks for being patient with me. \$\endgroup\$
    – Jacksonkr
    Commented Jan 18, 2022 at 22:00
  • 1
    \$\begingroup\$ @Evorlor I added an edit which contains code for an async texture download. Cheers~ \$\endgroup\$
    – Jacksonkr
    Commented Jan 25, 2022 at 20:49

2 Answers 2

1
\$\begingroup\$

The async / await construct is problematic in Unity. While coroutines are bound to the object which started them, async method executions are not. That means destroying objects won't stop them. They will still return to the method of the destroyed object and try to do stuff there, which will run into NullReferenceExceptions if you are lucky or cause all kinds of weird bugs if you are not. They will even survive leaving play mode and will then continue their work back in edit mode with potentially disastrous consequences for the scene you are editing.

It's therefore usually better to avoid them.

  • When performance doesn't matter enough to warrant multithreading, use Coroutines instead. A coroutine is always bound to a GameObject, and will be stopped automatically when that object gets destroyed.
  • When you want to parallelize complex calculations on multiple threads for performance reasons, use Jobs. While the job system is designed for entities, it can still be useful in projects which use classic game objects.

But if you really want to use an await for a long-running operation which might outlive the lifetime of the calling object, then you might want to put a check if (this == null) return; behind the async call to detect if the current object was destroyed in the meantime.

GameObject prefab = await LoadPrefabFromNetwork();
if (this == null) return; // Thanks for the effort, but I don't care anymore.
Instantiate(prefab);

When the asynchronous method itself does things with game objects internally, you might also want to use null-checks to confirm that none of those objects were destroyed in the meantime.

async void PutHatOn(GameObject target) {
    GameObject hat = await LoadHatPrefab();
    if (target != null) { // target might have been destroyed while loading
         Instantiate(hat, target.transform);
    }
}
\$\endgroup\$
1
\$\begingroup\$

I use a utility class for my async/await conditionals. I have slightly modified it for your use case (untested):

public static class TaskUtility
{
    private static readonly int DeltaTime;

    static TaskUtility()
    {
        DeltaTime = 1000 / MainCamera.instance.MaxFrameRate;
    }

    /// <summary>
    /// Whiles while the function is true
    /// </summary>
    /// <param name="condition">The function to check</param>
    public static async Task WaitWhile(Func<bool> condition)
    {
        while (condition() && Application.isPlaying)
        {
            await Task.Delay(DeltaTime);
        }
    }

    /// <summary>
    /// Waits until the function is true
    /// </summary>
    /// <param name="condition">The function to wait on</param>
    public static async Task WaitUntil(Func<bool> condition)
    {
        while (!condition() && Application.isPlaying)
        {
            await Task.Delay(DeltaTime);
        }
    }
}

Which would change your code to:

public static async Task<Texture2D> DownloadTexture(string uri)
{
    using (
        UnityWebRequest uwr = UnityWebRequestTexture.GetTexture(uri, true)
    )
    {
        var asyncOp = uwr.SendWebRequest();

        if (WeatherTracker.instance == null)
        {
            uwr.Abort(); // todo: something related to this is crashing the editor while textures are still being download @jkr
            return null;
        }

        await TaskUtility.WaitUntil(() => asyncOp.isDone);

        // return this.uwr.SendWebRequest();
        // Debug.Log($"download tex {z}/{x}/{y}");
        if (uwr.result != UnityWebRequest.Result.Success)
        {
            Debug.LogError($"{uwr.error}: {uri}");
            return null;
        }
        else
        {
            return DownloadHandlerTexture.GetContent(uwr);
        }
    }
}

This should effectively do the same thing as your code, except it pretends the condition is complete (which is a lie) if you press stop in the editor. (Note: It may behave differently on build.)

It is slightly hacky. Using cancellation tokens as commented by @DMGregory is probably the better way to go. But I have been using this method for a few years without issue. (But I think I will probably dehackify it, and update this answer when I do.)

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .