Obvious flaws or am I using this wrong? InstantiateAsync

So, the intended (or at least highly recommended) way of instantiating prefabs and other assets is using Addressables.InstantiateAsync(). This has the seemingly obvious drawback of having to rely on callbacks or async/await in order to run code on the newly instantiated object, but that’s fine for some situations. However I have two major issues with this API and system:

  • The .Complete callback, or the code after the await, runs on the frame after the object has been instantiated. This is a big deal. It means that if there is any code in the Update or LateUpdate methods on the instantiated object that relies on data being set by the caller of Instantiate, it just doesn’t work. This seems like such as massive oversight that I hope that I am somehow misunderstanding how this works.
    A simple example that throws a null exception:
var go = await assetReference.InstantiateAsync().Task;
var comp = go.GetComponent<MyComp>();
comp.Ref = new StringBuilder(); // just an example, imagine a reference to a scene object or something.

class MyComp
{
    public StringBuilder Ref;

    void Update()
    {
        Ref.Append("I expect Ref to be not null by now...");
    }
}

You can work around this by calling WaitForCompletion() and then assigning the data, but that negates any possible advantage of it being async.

  • What is the deal with the awful design of the ReleaseInstance API? On the surface it sounds great, let the system keep track of instance count and automatically load and unload. In practice, it’s awful.
    When you call ReleaseInstance is decrements the asset reference and destroys the object and all its child objects.
    The problem with this is that calling the usual Destroy leaves a dangling asset handle, a permanent memory leak. So what just replace all your calls to Destroy with ReleaseInstance? This may be fine for code you wrote and own but almost every Unity library in existance does not do this, they will just be calling Destroy(), and if that happens to be on an object that you created with Addressables, then you now have a memory leak.
  • But it gets worse.* Take the simple scenario of spawning two addressable prefabs, and making one the child of the other. Now call ReleaseInstance on the parent one. A single asset reference is released, but both objects are destroyed because one is a child of the other, again permanent memory leak. I find it hard to believe that this is intended behaviour and again I hope that I am doing something wrong. If the counter argument is that I should be keeping lists of every single instantiated object, I counter by asking what is the point of having ‘automatic’ reference tracking if I then have to do it myself.
    Another possible solution to this is to have a script on the spawned object that calls ReleaseInstance in its OnDestroy method. This works reliably as far as I can tell, and solves the previous two issues. Which begs the question, why is this not the default behaviour? Why do calls to the regular Destroy not automatically check if it is an addressable instance and release it? Clearly addressables keeps track of the instances internally, so surely this would be trivial?

Rant over. I still think that addressables is an upgrade over Resources in terms of memory management, but the API seems so poorly thought out or at least seems to not have been developed with real world projects in mind.

If order of initialisation operations matter for you in this case, it’s perhaps best to use the LoadAssetAsync API instead. The result of that operation you can instantiate once or as much as you want in the same way as you would instantiate any other prefab and perform initialisation operations on.

Loading the asset manually was my placeholder solution. I’ve since found that UniTask’s implementation of await on InstantiateAsync actually works as expected and allows it to have sensible behaviour.

Also, having done more research it seems like the await issue may be more of a general Unity await/async implementation issue, where the continuation after an asynchronous await will not be called until the next frame. Why said continuation is called after Update and not before, who knows, and I could not find any documentation explaining whether the execution order of continuations could be changed. I’ll keep using UniTask for now since that kind of thing is supported and documented…