In LoadAssetsAsync<Foo>()Is there a reason to prefer the callback function over the Task completion?

Let’s say that I am interested in loading an asset, and I do so using Addressables.LoadAssetsAsync(“address”, OnLoaded);

private IEnumerator DoAllLoading(){
        // Obtain AsyncOperationHandle
        var op = Addressables.LoadAssetsAsync<GameObject>("AssetAddress", DidLoad);

        // Pause Until successful
        while (op.Status != AsyncOperationStatus.Succeeded)
        {
            yield return null;           
        }

        // Release The Asset, because we are done with it
        Addressables.Release(op);
}

private void DidLoad(GameObject result){
        // ... Do what you need to do here
}

Is there a practical reason to even use the DidLoad method in order to work on the results of the Load call? I’m confused as to why I would even need it, because:

  • If I simply yield null until op.Status is Succeeded, then I have the results after the end of the while loop.

  • op.Result should contain all the loaded items, after all.

  • If I do all my work in DidLoad, I would lose my reference to the AsyncOperationHandle

  • Eventually I will want to release that AsyncOperationHandle, no matter what. If I do everything in DidLoad then I might forget to store a reference to the handle for disposal later.

So since I really will want to hold onto that handle no matter what, why would I need to delegate any work at all to DidLoad?

Could we make it so that all the async functions work this way? I find this idiom to be very much more friendly than the callback function idiom.

You can periodically check the status of the operation, by detecting AsyncOperationHandle.Status to be either AsyncOperationStatus.Succeeded or AsyncOperationStatus.Failed, or register for a completed callback using the AsyncOperationHandle.Complete event, or use async-wait. They’re all fine with cons and pros.

The busy-wait approach requires coroutine.

The callback approach loses the context. A workaround is using anonymous methods / lambda.

var somecontext = ...;
Addressables.LoadAssetsAsync<GameObject>("address", result => {
    // you can still visit somecontext here.
});

The async-await approach requires scripting runtime version to “NET 4.x Equivalent”. Though async-await has many benefits, it’s still new and not the official way to handle async task yet.

public async Task LoadMyAsset()
{
    var asset = await Addressables.LoadAssetAsync<GameObject>("address").Task;
    // perhaps you should check gameObject != null as well, in case this happens in monobehaviour.
    if (asset != null)
    {
        // use asset
    }

}

For the later two, you don’t need to worry about releasing AsyncOperationHandle. Addressable stores a mapping from asset object to handle, when you call Addressables.Release(object), the related AsyncOperationHandle will be released.

The major design intention here is to provide more choices, so you can still use the system with/without coroutine, callback, and async-await.

2 Likes

I’ve shied away from async/await precisely because of how experimental it is in Unity, but that could be the best solution for me once it’s mature. If Addressables is able to deal with the Async handle internally while using await/async then that’s just a blessing.

The problem with Lambdas in this specific callback is that they create closures whenever you want to do anything useful, and I’ve gone to lengths to avoid writing code that needlessly allocates closures.

To the extent that Unity wants to promote performance by default, tools that necessitate closure creation work at cross purposes to this goal. @unity_bill :slight_smile:

I am having a lot of trouble understanding how to load addressables despite watching several Unity presentations and reading the (limited) documentation. Question regarding the anonymous methods:

If I use a method like that, how do I access the result and use it? I tried doing something like:

var op = Addressables.LoadAssetsAsync<Sprite>(address, result => {
               sprite = op.Result;
});

but intellisense tells me that op is an unassigned local variable. What am I missing?

Ok, so I didn’t understand how that worked. I would need to use result as opposed to op.Result. However, even after doing that, it didn’t work. I think that is because I am using LoadAssetsAsync when only trying to load a single asset. Is there a way I can do this for a single asset without losing context (like when using += delegate)?

AFAIK, the major complaint about async/await, is lacking a tight integration with GameObject lifecycle. Unlike coroutine which get cleaned after game object destroyed, async/await need either check gameobject != null after each await, or use CancellationTokenSource, which basically something similar.

2 Likes

I exclusively use that method to load single individual assets. It isn’t because you have only one. Maybe your issue is that you’re trying to access that sprite before the callback completes?

As far as I’m concerned the best way not to lose your context is the way I demonstrated in my first post, using a coroutine. It’s the only way you can both

  1. Have a context
  2. Not waste an allocation by making a closure lambda each time you load something

This isn’t really the point of the thread, but I do want to point out something. about LoadAssets. Class Addressables | Package Manager UI website

the interface is:
AsyncOperationHandle<IList> LoadAssetsAsync(object key, Action callback)

99% of the time, you’ll want to use the returned handle to process whatever it is you get back. The point of the callback is if you need to execute something per-result. So you could do something like:

LoadAssetsAsync(“Enemies”, SetEnemyHealth).Completed += OnEnemiesDoneLoading;

hope that clarifies things.

1 Like

@unity_bill how to properly handle exceptions (like InvalidKeyException) in the async/await variant of LoadAssetAsync (v1.1.5)? I tried to make something like this:

public async Task<T> LoadAssetAsync<T>(string name) where T : Object {          
    AsyncOperationHandle<T> handle = Addressables.LoadAssetAsync<T>(name);

    await handle.Task;      

    T result = null;

    if (handle.Status == AsyncOperationStatus.Succeeded) {
        result = handle.Result;
    } else {
        // Handler failure here
    }
    return result;
}

but if there is no asset with such name it just fails with the exception. I tried to wrap around it with try/catch but it didn’t help.

@iamarugin it’s a known bug. No exception shall raise when loading non-existing key.

4 Likes

Yes! This raising InvalidKeyException is frustrating me like hell, so what should we do?

I know I can first check if the key is valid by Addressables.LoadResourceLocationsAsync() first, but this will complicate the workflow which other programmers don’t like.

I find that it is because of the Complete() method in AsyncOperationBase.cs, when AsyncOperationStatus.Failed, it will throw the exception. I commented this out and everything works fine. But I don’t think this is how we suppose to do :frowning:

1 Like

I posted a new thread here:

Hope somebody can help me, thanks.