Operations
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 function almost immediately. However, the results may not be available until some time in the future. When you call a function, 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. See Using operations synchronously.
Releasing AsyncOperationHandle instances
Methods, like LoadAssetsAsync, return AsyncOperationHandle instances that both provide the results of the operation and a way to release both the results and the operation object itself. You must retain 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 function 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. See Memory management for more information about reference counting in the Addressables system.
In cases where you don’t need to use the results of an operation beyond a limited scope, you can release the handles right away. A few 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. Normally, Addressables releases any assets that it loaded during a failed operation, but releasing the handle still clears the handle’s instance data. Note that some functions, like LoadAssetsAsync, which load multiple assets, give you the option to either retain any assets that it could load or to fail and release everything if any part of the load operation failed.
Coroutine- and IEnumerator-based operation handling
The AsyncOperationHandle implements the IEnumerator interface and will continue 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. Recall that you can implement the MonoBehaviour Start function 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 function 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 {
Addressables.Release(opHandle);
}
}
void OnDestroy() {
Addressables.Release(opHandle);
}
}
Note that Addressables.LoadAssetsAsync is not able to be canceled once started. However, releasing the handle before it has finished will decrement the handle reference count and it will automatically release when the load is complete.
See Coroutines for more information.
Grouping operations in a coroutine
You will probably encounter situations in which you want to perform several operations before moving on to the next step in your game logic. For example, you want to load a number of Prefabs and other assets before you start a level.
If the operations all load assets, you can combine them with a single call to the Addressables.LoadAssetsAsync function. 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. In addition, you can pass a callback function to LoadAssetsAsync and the operation calls that function when it finishes loading a specific asset. See Loading multiple assets for an example.
Another option is to use the ResourceManager.CreateGenericGroupOperation to create a group operation that completes when all of 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-based 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 {
Addressables.Release(obj);
}
}
void OnDestroy() {
Addressables.Release(opHandle);
}
}
Note that the handle instance passed to the event delegate is the same as that returned by the original function call. You can use either to access the results and status of the operation and, ultimately, to release the operation handle and loaded assets.
Task-based operation handling
The AsyncOperationHandle provides a Task object that you can use with the C# async and await keywords to sequence code that calls asynchronous functions 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 function, which must include the async keyword and the use of the await keyword with the operation handle’s Task property. The calling function, 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() {
Addressables.Release(loadHandle);
// 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 is not available on the Unity 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);
Using 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.
In Unity 2020.1 or earlier, Unity also waits for all other pending asynchronous operations to finish, so the delay in execution can be much longer than that required for just the single operation for which you call this method. In Unity 2020.2 or later, the performance impact can be less pronounced, at least when loading assets that have already been downloaded.
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 {
Addressables.Release(opHandle);
}
}
void OnDestroy() {
Addressables.Release(opHandle);
}
}
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 and shows them in the Addressables Event Viewer.
Executing the operation
The ResourceManager invokes the AsyncOperationBase.Execute method for your custom operation once the optional dependent operation completes.
Completion handling
When your custom operation completes, call AsyncOperationBase.Complete on your custom operation object. You can call this within 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.
Terminating the operation
The ResourceManager invokes the AsyncOperationBase.Destroy method for your custom operation when you release the AsyncOperationHandle that references it. This is where you should release any memory or resources associated with your custom operation.
Using typed versus 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.
Note that a runtime exception occurs if you attempt to cast a non-generic handle to a generic handle of an incorrect 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>();
Reporting operation progress
AsyncOperationHandle has two methods that you can use to monitor and report the progress of the operation:
- GetDownloadStatus returns a DownloadStatus struct. This struct contains information about how many bytes have been downloaded and how many bytes still need to be downloaded. The DownloadStatus.Percent reports the percentage of bytes downloaded.
- AsyncOperationHandle.PercentComplete returns an equally-weighted aggregate 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 called Addressables.DownloadDependenciesAsync and five AssetBundles needed to be downloaded, GetDownloadStatus would tell you what percentage of the total number of bytes for all sub-operations had been downloaded so far. PercentComplete would tell you what percentage of the number of operations had finished, regardless of their size.
On the other hand, if you called LoadAssetAsync, and one bundle had to be downloaded before an asset could be loaded from it, the download percentage might be misleading. The values obtained from GetDownloadStatus would reach 100% before the operation finished, because the operation had additional sub-operations to conduct. The value of PercentComplete would be 50% when the download sub-operation finished and 100% when the actual load into memory was complete.