操作
Addressables の多くのタスクでは、結果を返すことができるようになる前に、情報をロードするかダウンロードする必要があります。Addressables には、プログラムの実行がブロックされないように、非同期操作などのタスクが実装されています。
結果が使用可能になるまで制御が返されない同期操作とは対照的に、非同期操作では、ほぼ瞬時に呼び出し関数に制御が返されます。ただし、将来のある時点までは結果が使用可能にならない場合があります。LoadAssetAsync などの関数を呼び出した場合、ロードされたアセットが直接返されるわけではありません。代わりに AsyncOperationHandle オブジェクトが返され、ロードされたアセットが使用可能になると、このオブジェクトを使用してアクセスすることができます。
他のスクリプトの処理を続行しながら非同期操作の結果を待機するには、以下のテクニックを使用できます。
- [コルーチンと IEnumerator ループ]
- [イベント]
- [タスク]
Note
現在のスレッドをブロックして、非同期操作が完了するまで待機することもできます。ただし、この方法では、パフォーマンスの問題やフレームレートへの悪影響が発生する可能性があります。[操作の同期的使用] を参照してください。
AsyncOperationHandle インスタンスの解放
LoadAssetsAsync などのメソッドは AsyncOperationHandle インスタンスを返します。このインスタンスによって、操作の結果と、結果および操作オブジェクト自体の両方を解放する方法が提供されます。ハンドルオブジェクトは、結果を使用する間ずっと保持される必要があります。これは状況に応じて、1 フレームの場合やレベルの終了時までの場合もあれば、アプリケーションの生存期間全体にわたる場合もあります。操作ハンドルおよび関連付けられているすべての Addressable アセットを解放するには、Addressables.Release 関数を使用します。
操作ハンドルを解放すると、その操作でロードされたあらゆるアセットの参照カウントが減少し、操作ハンドルオブジェクト自体が無効になります。Addressables システムでの参照カウントの詳細については、[メモリ管理] を参照してください。
限定されたスコープを超えて操作の結果を使用する必要がない場合は、すぐにハンドルを解放できます。UnloadSceneAsync などの一部の Addressables メソッドでは、操作ハンドルをその完了時に自動的に解放することができます。
操作が正常に終了しない場合でも、操作ハンドルを解放する必要があります。通常、失敗した操作の間にロードされたアセットはすべて Addressables によって解放されますが、ハンドルを解放することで、ハンドルのインスタンスデータも消去されます。複数のアセットをロードする LoadAssetsAsync などの関数では、ロード操作の一部が失敗した場合に、ロードできたアセットをすべて保持するか、すべて失敗として解放するかを選択できます。
コルーチンおよび IEnumerator ベースの操作ハンドリング
AsyncOperationHandle は IEnumerator インターフェースを実装しており、操作が完了するまでイテレーションを継続します。コルーチンでは、操作ハンドルを中断して、次のイテレーションを待つことができます。完了すると、実行フローは後続のステートメントへと続きます。MonoBehaviour Start 関数はコルーチンとして実装できます。この方法は、ゲームオブジェクトをロードし、必要なアセットをインスタンス化する場合に適しています。
以下のスクリプトでは、Start 関数コルーチンを使用して、ゲームオブジェクトの子としてプレハブをロードします。操作が終了するまで AsyncOperationHandle を中断してから、同じハンドルを使用してプレハブをインスタンス化します。
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);
}
}
Addressables.LoadAssetsAsync は、いったん開始した後はキャンセルできません。ただし、終了前にハンドルを解放すると、ハンドルの参照カウントが減少し、ロードの完了時に自動的に解放されます。
詳細については、[コルーチン] を参照してください。
コルーチンでの操作のグループ化
多くの場合、ゲームロジックの次のステップに進む前に、複数の操作を実行する必要があるという状況が発生します。例えば、レベルを開始する前には、いくつかのプレハブとその他のアセットをロードする必要があります。
すべての操作がアセットのロードである場合は、それらを 1 回の Addressables.LoadAssetsAsync の呼び出しにまとめることができます。このメソッドの AsyncOperationHandle は LoadAssetAsync と同じように機能するため、コルーチンでハンドルを中断することで、操作に指定したすべてのアセットがロードされるまで待機できます。さらに、LoadAssetsAsync にはコールバック関数を渡すことができます。コールバック関数は、操作で特定のアセットのロードが終了したときに呼び出されます。例については、[複数のアセットのロード] を参照してください。
もう 1 つの方法として、ResourceManager.CreateGenericGroupOperation を使用してグループ操作を作成できます。この操作は、そのメンバーのすべてが終了すると完了します。
イベントベースの操作ハンドリング
AsyncOperationHandle の Completed イベントにデリゲート関数を追加できます。操作は、終了時にデリゲート関数を呼び出します。
以下のスクリプトは、[コルーチンおよび IEnumerator ベースの操作ハンドリング] の例と同じ機能を実行しますが、コルーチンの代わりにイベントデリゲートを使用します。
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);
}
}
イベントデリゲートに渡されるハンドルインスタンスは、元の関数呼び出しで返されたものと同じです。どちらを使用しても、操作の結果とステータスにアクセスしたり、最終的に操作ハンドルとロードされたアセットを解放したりできます。
タスクベースの操作ハンドリング
AsyncOperationHandle によって提供される Task オブジェクトを C# の async および await キーワードとともに使用すると、非同期関数の呼び出しと結果の処理を行うコードを順番に実行できます。
以下の例では、キーのリストを使用して Addressable アセットをロードします。このタスクベースのアプローチがコルーチンまたはイベントベースのアプローチと異なる点は、呼び出し側関数のシグネチャに async キーワードを加える必要があることと、操作ハンドルの Task プロパティに対して await キーワードを使用することにあります。呼び出し側関数 (この場合は Start()) は、タスクが終了するまで操作を中断します。その後実行が再開され、この例では、ロードされたすべてのプレハブが (グリッドパターンに) インスタンス化されます。
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
AsyncOperationHandle.Task プロパティは、マルチタスクをサポートしていない Unity WebGL プラットフォームでは使用できません。
タスクベースの操作ハンドリングを使用すると、WhenAll などの C# の 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);
操作の同期的使用
操作の WaitForCompletion メソッドを呼び出すと、中断、イベントの待機、async await
の使用のいずれも行わずに、操作が終了するまで待つことができます。このメソッドは、操作の終了を待機する間現在のプログラム実行スレッドをブロックし、その後現在のスコープで続行します。
データのダウンロードが必要な操作など、所要時間が非常に長くなる可能性のある操作では、WaitForCompletion を呼び出さないようにしてください。WaitForCompletion を呼び出すと、フレーム落ちが発生したり、UI の応答性が低下したりする場合があります。
Unity 2020.1 以前では、Unity は保留中の他のすべての非同期操作の終了も待機するため、このメソッド呼び出しの対象となる単一操作に要する時間よりも、大幅に長い実行遅延が発生する可能性があります。Unity 2020.2 以降では、少なくともすでにダウンロードされているアセットをロードする限り、パフォーマンスへの影響はそれほど顕著ではありません。
以下の例では、アドレスを指定してプレハブアセットをロードし、操作が完了するまで待機してから、プレハブをインスタンス化します。
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);
}
}
カスタム操作
カスタム操作を作成するには、AsyncOperationBase クラスを継承し、その仮想メソッドをオーバーライドします。
派生した操作を ResourceManager.StartOperation メソッドに渡して操作を開始し、AsyncOperationHandle 構造体を受け取ることができます。ResourceManager は、この方法で開始された操作を登録し、それらの操作を Addressables Event Viewer に表示します。
操作の実行
ResourceManager は、任意の依存操作が完了すると、カスタム操作の AsyncOperationBase.Execute メソッドを呼び出します。
完了のハンドリング
カスタム操作が完了したら、カスタム操作オブジェクトで AsyncOperationBase.Complete を呼び出します。これは Execute メソッド内で呼び出すことも、メソッド外に延期することもできます。AsyncOperationBase.Complete は、操作が終了したことを ResourceManager に通知します。ResourceManager は、カスタム操作の関連インスタンスのために、関連付けられた AsyncOperationHandle.Completed イベントを呼び出します。
操作の終了
ResourceManager は、操作の AsyncOperationBase.ReferenceCount がゼロに達すると、カスタム操作の AsyncOperationBase.Destroy メソッドを呼び出します。AsyncOperationBase.ReferenceCount は、参照元の AsyncOperationHandle が Addressables.Release を使用して解放されるか、カスタム操作によって内部的に AsyncOperationBase.DecrementReferenceCount が呼び出されると減少します。AsyncOperationBase.Destroy では、カスタム操作に関連付けられたメモリやリソースをすべて解放する必要があります。
型指定された操作ハンドルと型指定なしの操作ハンドルの使用
操作を開始するほとんどの Addressables メソッドは、ジェネリック AsyncOperationHandle<T> 構造体を返すため、AsyncOperationHandle.Completed イベントと AsyncOperationHandle.Result オブジェクトはタイプセーフになります。また、非ジェネリックの AsyncOperationHandle 構造体を使用し、必要に応じて 2 つのハンドル型の間で変換を行うこともできます。
非ジェネリックハンドルを不適切な型のジェネリックハンドルにキャストしようとすると、ランタイム例外が発生します。以下に例を示します。
// 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>();
操作の進行状況の報告
AsyncOperationHandle には、操作の進行状況の監視と報告に使用できる 2 つのメソッドがあります。
- GetDownloadStatus は DownloadStatus 構造体を返します。この構造体には、これまでにダウンロードされたバイト数と、これからダウンロードする必要のあるバイト数に関する情報が含まれています。DownloadStatus.Percent は、ダウンロードされたバイトの割合 (パーセンテージ) を報告します。
- AsyncOperationHandle.PercentComplete は、完了したすべてのサブ操作の均等加重集計パーセンテージを返します。例えば、操作に 5 つのサブ操作がある場合は、そのそれぞれが全体の 20% を占めます。この値では、個々のサブ操作でダウンロードする必要のあるデータの量は考慮されません。
例えば、Addressables.DownloadDependenciesAsync を呼び出して 5 つの AssetBundle をダウンロードする場合、GetDownloadStatus では、すべてのサブ操作の合計バイト数のうち、これまでにダウンロードされたバイト数の割合が返されます。PercentComplete では、サイズにかかわらず、終了した操作の数の割合が返されます。
一方、LoadAssetAsync を呼び出してアセットをロードする操作において、ロード前に 1 つのバンドルをダウンロードする必要がある場合は、ダウンロードの割合が誤解の原因になることがあります。GetDownloadStatus から取得される値は、操作の終了前に 100% に達します。この操作には、ダウンロード後に実行されるサブ操作が含まれているためです。PercentComplete の値は、ダウンロードサブ操作が終了したときに 50%、メモリへの実際のロードが完了したときに 100% になります。