Search Unity

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

Discussion in 'Addressables' started by RecursiveFrog, Jun 28, 2019.

  1. RecursiveFrog

    RecursiveFrog

    Joined:
    Mar 7, 2011
    Posts:
    350
    Let's say that I am interested in loading an asset, and I do so using Addressables.LoadAssetsAsync<GameObject>("address", OnLoaded);

    Code (CSharp):
    1.  
    2. private IEnumerator DoAllLoading(){
    3.         // Obtain AsyncOperationHandle
    4.         var op = Addressables.LoadAssetsAsync<GameObject>("AssetAddress", DidLoad);
    5.  
    6.         // Pause Until successful
    7.         while (op.Status != AsyncOperationStatus.Succeeded)
    8.         {
    9.             yield return null;          
    10.         }
    11.  
    12.         // Release The Asset, because we are done with it
    13.         Addressables.Release(op);
    14. }
    15.  
    16. private void DidLoad(GameObject result){
    17.         // ... Do what you need to do here
    18. }
    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<GameObject>
    • 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.
     
  2. Favo-Yang

    Favo-Yang

    Joined:
    Apr 4, 2011
    Posts:
    464
    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.

    Code (CSharp):
    1. var somecontext = ...;
    2. Addressables.LoadAssetsAsync<GameObject>("address", result => {
    3.     // you can still visit somecontext here.
    4. });
    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.

    Code (CSharp):
    1. public async Task LoadMyAsset()
    2. {
    3.     var asset = await Addressables.LoadAssetAsync<GameObject>("address").Task;
    4.     // perhaps you should check gameObject != null as well, in case this happens in monobehaviour.
    5.     if (asset != null)
    6.     {
    7.         // use asset
    8.     }
    9.  
    10. }
    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.
     
    Deleted User and Colin_MacLeod like this.
  3. RecursiveFrog

    RecursiveFrog

    Joined:
    Mar 7, 2011
    Posts:
    350
    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 :)
     
    Last edited: Jun 30, 2019
  4. lifetree

    lifetree

    Joined:
    Mar 28, 2014
    Posts:
    49
    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:
    Code (CSharp):
    1. var op = Addressables.LoadAssetsAsync<Sprite>(address, result => {
    2.                sprite = op.Result;
    3. });
    but intellisense tells me that op is an unassigned local variable. What am I missing?
     
  5. lifetree

    lifetree

    Joined:
    Mar 28, 2014
    Posts:
    49
    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)?
     
  6. Favo-Yang

    Favo-Yang

    Joined:
    Apr 4, 2011
    Posts:
    464
    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.
     
    Prodigga and RecursiveFrog like this.
  7. RecursiveFrog

    RecursiveFrog

    Joined:
    Mar 7, 2011
    Posts:
    350
    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
     
  8. unity_bill

    unity_bill

    Joined:
    Apr 11, 2017
    Posts:
    1,053
    This isn't really the point of the thread, but I do want to point out something. about LoadAssets. https://docs.unity3d.com/Packages/c...etsAsync__1_System_Object_System_Action___0__


    the interface is:
    AsyncOperationHandle<IList<TObject>> LoadAssetsAsync<TObject>(object key, Action<TObject> 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<GameObject>("Enemies", SetEnemyHealth).Completed += OnEnemiesDoneLoading;

    hope that clarifies things.
     
    DevilCult likes this.
  9. iamarugin

    iamarugin

    Joined:
    Dec 17, 2014
    Posts:
    883
    @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:

    Code (CSharp):
    1.  
    2. public async Task<T> LoadAssetAsync<T>(string name) where T : Object {          
    3.     AsyncOperationHandle<T> handle = Addressables.LoadAssetAsync<T>(name);
    4.  
    5.     await handle.Task;      
    6.  
    7.     T result = null;
    8.  
    9.     if (handle.Status == AsyncOperationStatus.Succeeded) {
    10.         result = handle.Result;
    11.     } else {
    12.         // Handler failure here
    13.     }
    14.     return result;
    15. }
    16.  
    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.
     
  10. Favo-Yang

    Favo-Yang

    Joined:
    Apr 4, 2011
    Posts:
    464
    @iamarugin it's a known bug. No exception shall raise when loading non-existing key.
     
  11. Philip_Zhang

    Philip_Zhang

    Joined:
    Feb 15, 2017
    Posts:
    25
    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 :(
     
    Xarbrough likes this.
  12. Philip_Zhang

    Philip_Zhang

    Joined:
    Feb 15, 2017
    Posts:
    25