Asynchronous operation handles
Many tasks in the Addressables need to load or download information before they can return a result. To avoid blocking program execution, Addressables implements such tasks as asynchronous operations.
In contrast to a synchronous operation, which doesn’t return control until the result is available, an asynchronous operation returns control to the calling method almost immediately. However, the results might not be available until some time in the future.
When you call a method, such as LoadAssetAsync
, it doesn't return the loaded assets directly. Instead, it returns an AsyncOperationHandle
object, which you can use to access the loaded assets when they become available.
You can use the following techniques to wait for the results of an asynchronous operation while allowing other scripts to continue processing:
Note
You can block the current thread to wait for the completion of an asynchronous operation. Doing so can introduce performance problems and frame rate hitches. Refer to Using operations synchronously for more information.
Release AsyncOperationHandle instances
Methods like LoadAssetsAsync
return AsyncOperationHandle
instances that give the results of the operation and a way to release both the results and the operation object itself.
You must keep the handle object for as long as you want to use the results. Depending on the situation, that might be one frame, until the end of a level, or even the lifetime of the application. Use the Addressables.Release
method to release operation handles and any associated Addressable assets.
Releasing an operation handle decrements the reference count of any assets loaded by the operation and invalidates the operation handle object itself. Refer to Memory management for more information about reference counting in the Addressables system.
If you don’t need to use the results of an operation beyond a limited scope, you can release the handles immediately. Some Addressables methods, such as UnloadSceneAsync
allow you to automatically release the operation handle when it's complete.
If an operation is unsuccessful, you should still release the operation handle. Addressables releases any assets that it loaded during a failed operation, but releasing the handle still clears the handle’s instance data. Some methods which load multiple assets, like LoadAssetsAsync
, give you the option to either keep any assets that it loaded, or to fail and release everything if any part of the load operation failed.
Coroutine and IEnumerator operation handling
AsyncOperationHandle
implements the IEnumerator
interface and continues iteration until the operation is complete.
In a coroutine, you can yield the operation handle to wait for the next iteration. When complete, the execution flow continues to the following statements. You can implement the MonoBehaviour Start
method as a coroutine, which is a good way to have a GameObject load and instantiate the assets it needs.
The following script loads a prefab as a child of its GameObject using a Start
method in a coroutine. It yields the AsyncOperationHandle
until the operation finishes and then uses the same handle to instantiate the prefab.
using System.Collections;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
internal class LoadWithIEnumerator : MonoBehaviour
{
public string address;
AsyncOperationHandle<GameObject> opHandle;
public IEnumerator Start()
{
opHandle = Addressables.LoadAssetAsync<GameObject>(address);
// yielding when already done still waits until the next frame
// so don't yield if done.
if (!opHandle.IsDone)
yield return opHandle;
if (opHandle.Status == AsyncOperationStatus.Succeeded)
{
Instantiate(opHandle.Result, transform);
}
else
{
opHandle.Release();
}
}
void OnDestroy()
{
opHandle.Release();
}
}
You can't cancel Addressables.LoadAssetsAsync
once started. However, releasing the handle before it has finished decrements the handle reference count and automatically releases it when the load is complete.
Refer to the Unity User Manual documentation on Coroutines for more information.
Group operations in a coroutine
To perform several operations before moving on to the next step in your game logic, such as to load prefabs and other assets before you start a level, you can combine them with a single call to the Addressables.LoadAssetsAsync
method, if all the operations load assets.
The AsyncOperationHandle
for this method works the same as LoadAssetAsync
. You can yield the handle in a coroutine to wait until all the assets in the operation load. You can also pass a callback method to LoadAssetsAsync
and the operation calls that method when it finishes loading a specific asset. Refer to Loading multiple assets for an example.
You can also use the ResourceManager.CreateGenericGroupOperation
to create a group operation that completes when all its members finish.
Event based operation handling
You can add a delegate function to the Completed
event of an AsyncOperationHandle
. The operation calls the delegate function when it's finished.
The following script performs the same function as the example in coroutine and IEnumerator operation handling, but uses an event delegate instead of a coroutine:
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
internal class LoadWithEvent : MonoBehaviour
{
public string address;
AsyncOperationHandle<GameObject> opHandle;
void Start()
{
// Create operation
opHandle = Addressables.LoadAssetAsync<GameObject>(address);
// Add event handler
opHandle.Completed += Operation_Completed;
}
private void Operation_Completed(AsyncOperationHandle<GameObject> obj)
{
if (obj.Status == AsyncOperationStatus.Succeeded)
{
Instantiate(obj.Result, transform);
}
else
{
obj.Release();
}
}
void OnDestroy()
{
opHandle.Release();
}
}
The handle instance passed to the event delegate is the same as that returned by the original method call. You can use either to access the results and status of the operation and to release the operation handle and loaded assets.
Task-based operation handling
AsyncOperationHandle
provides a Task
object that you can use with the C# async
and await
keywords to sequence code that calls asynchronous methods and handles the results.
The following example loads Addressable assets using a list of keys. The differences between this task-based approach and the coroutine or event-based approaches are in the signature of the calling method. This method must include the async
keyword and use of the await
keyword with the operation handle’s Task
property. The calling method, Start
in this case, suspends operation while the task finishes. Execution then resumes and the example instantiates all the loaded prefabs in a grid pattern.
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
internal class LoadWithTask : MonoBehaviour
{
// Label or address strings to load
public List<string> keys = new List<string>() {"characters", "animals"};
// Operation handle used to load and release assets
AsyncOperationHandle<IList<GameObject>> loadHandle;
public async void Start()
{
loadHandle = Addressables.LoadAssetsAsync<GameObject>(
keys, // Either a single key or a List of keys
addressable =>
{
// Called for every loaded asset
Debug.Log(addressable.name);
}, Addressables.MergeMode.Union, // How to combine multiple labels
false); // Whether to fail if any asset fails to load
// Wait for the operation to finish in the background
await loadHandle.Task;
// Instantiate the results
float x = 0, z = 0;
foreach (var addressable in loadHandle.Result)
{
if (addressable != null)
{
Instantiate<GameObject>(addressable,
new Vector3(x++ * 2.0f, 0, z * 2.0f),
Quaternion.identity,
transform); // make child of this object
if (x > 9)
{
x = 0;
z++;
}
}
}
}
private void OnDestroy()
{
loadHandle.Release();
// Release all the loaded assets associated with loadHandle
// Note that if you do not make loaded addressables a child of this object,
// then you will need to devise another way of releasing the handle when
// all the individual addressables are destroyed.
}
}
Important
The AsyncOperationHandle.Task
property isn't available on the WebGL platform, which doesn't support multitasking.
When you use Task
-based operation handling, you can use the C# Task
class methods such as WhenAll
to control which operations you run in parallel and which you want to run in sequence. The following example illustrates how to wait for more than one operation to finish before moving onto the next task:
// Load the Prefabs
var prefabOpHandle = Addressables.LoadAssetsAsync<GameObject>(
keys, null, Addressables.MergeMode.Union, false);
// Load a Scene additively
var sceneOpHandle
= Addressables.LoadSceneAsync(nextScene,
UnityEngine.SceneManagement.LoadSceneMode.Additive);
await System.Threading.Tasks.Task.WhenAll(prefabOpHandle.Task, sceneOpHandle.Task);
Use operations synchronously
You can wait for an operation to finish without yielding, waiting for an event, or using async await
by calling an operation’s WaitForCompletion
method. This method blocks the current program execution thread while it waits for the operation to finish before continuing in the current scope.
Avoid calling WaitForCompletion
on operations that can take a significant amount of time, such as those that must download data. Calling WaitForCompletion
can cause frame hitches and interrupt UI responsiveness.
The following example loads a prefab asset by address, waits for the operation to complete, and then instantiates the prefab:
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
internal class LoadSynchronously : MonoBehaviour
{
public string address;
AsyncOperationHandle<GameObject> opHandle;
void Start()
{
opHandle = Addressables.LoadAssetAsync<GameObject>(address);
opHandle.WaitForCompletion(); // Returns when operation is complete
if (opHandle.Status == AsyncOperationStatus.Succeeded)
{
Instantiate(opHandle.Result, transform);
}
else
{
opHandle.Release();
}
}
void OnDestroy()
{
opHandle.Release();
}
}
Custom operations
To create a custom operation, extend the AsyncOperationBase
class and override its virtual methods.
You can pass the derived operation to the ResourceManager.StartOperation
method to start the operation and receive an AsyncOperationHandle
struct. The ResourceManager
registers operations started this way.
Execute a custom operation
The ResourceManager
invokes the AsyncOperationBase.Execute
method for the custom operation once the optional dependent operation completes.
Completion handling
When the custom operation completes, call AsyncOperationBase.Complete
on the custom operation object. You can call this in the Execute
method or defer it to outside the call. AsyncOperationBase.Complete
notifies the ResourceManager
that the operation has finished. The ResourceManager
invokes the associated AsyncOperationHandle.Completed
events for the relevant instances of the custom operation.
Terminate the custom operation
ResourceManager
invokes the AsyncOperationBase.Destroy
method for your custom operation when the operation AsyncOperationBase.ReferenceCount
reaches zero. AsyncOperationBase.ReferenceCount
is decreased when the AsyncOperationHandle
that references it is released using Addressables.Release
or when AsyncOperationBase.DecrementReferenceCount
is called by a custom operation internally. AsyncOperationBase.Destroy
is where you should release any memory or resources associated with your custom operation.
Typed and typeless operation handles
Most Addressables
methods that start an operation return a generic AsyncOperationHandle<T>
struct, allowing type safety for the AsyncOperationHandle.Completed
event and for the AsyncOperationHandle.Result
object. You can also use a non-generic AsyncOperationHandle
struct and convert between the two handle types as desired.
A runtime exception happens if you try to cast a non-generic handle to a generic handle of a wrong type. For example:
// Load asset using typed handle:
AsyncOperationHandle<Texture2D> textureHandle = Addressables.LoadAssetAsync<Texture2D>("mytexture");
// Convert the AsyncOperationHandle<Texture2D> to an AsyncOperationHandle:
AsyncOperationHandle nonGenericHandle = textureHandle;
// Convert the AsyncOperationHandle to an AsyncOperationHandle<Texture2D>:
AsyncOperationHandle<Texture2D> textureHandle2 = nonGenericHandle.Convert<Texture2D>();
// This will throw and exception because Texture2D is required:
AsyncOperationHandle<Texture> textureHandle3 = nonGenericHandle.Convert<Texture>();
Report operation progress
AsyncOperationHandle
has the following methods that you can use to monitor and report the progress of the operation:
GetDownloadStatus
: Returns aDownloadStatus
struct. This struct contains information about how many bytes have been downloaded and how many bytes still need to be downloaded.DownloadStatus.Percent
reports the percentage of bytes downloaded.AsyncOperationHandle.PercentComplete
: Returns an equally weighted total percentage of all the sub-operations that are complete. For example, if an operation has five sub-operations, each of them represents 20% of the total. The value doesn't factor in the amount of data that must be downloaded by the individual sub-operations.
For example, if you call Addressables.DownloadDependenciesAsync
and five AssetBundles need to be downloaded, GetDownloadStatus
tells you what percentage of the total number of bytes for all sub-operations has been downloaded. PercentComplete
tells you what percentage of the number of operations had finished, regardless of their size.
If you call LoadAssetAsync
, and one bundle has to be downloaded before an asset can be loaded from it, the download percentage might be misleading. The values obtained from GetDownloadStatus
reach 100% before the operation finishes, because the operation has additional sub-operations to conduct. The value of PercentComplete
is 50% when the download sub-operation is finished and 100% when the actual load into memory is complete.