【Unity】Addressableアセットシステムで同期ロードが公式サポート、Resources.Loadを置き換え可能に

UnityのAddressableアセットシステムで同期ロードが公式にサポートされたので、その使い方とResources.Loadを置き換える方法についてまとめました。

Unity2019.4.0
Addressbles 1.17.4

Addressablesと同期ロード

今までは非同期なロード方法しかなかった

Addressableアセットシステムにはこれまで同期ロードのサポートがありませんでした。
そのため以下のように非同期メソッドやコルーチンなどでロードの完了を待機する必要がありました。

// 非同期メソッド
var handle = Addressables.LoadAssetAsync<GameObject>("FooPrefab");
await handle.Task; // 待機
var prefab = handle.Result;

// コルーチン
var handle = Addressables.LoadAssetAsync<GameObject>("FooPrefab");
yield return handle; // 待機
var prefab = handle.Result;

// コールバック
var handle = Addressables.LoadAssetAsync<GameObject>("FooPrefab").Completed += x =>
{
    var prefab = handle.Result;
};

もし同期的にリソースを取得したい場合には、以下の記事のようにプリロードをしたり、専用のProviderを書いたりする必要がありました。

light11.hatenadiary.com

同期ロードが公式サポートされた

しかしやはり公式としてのサポートには強い要望があり、以下のスレッドで長らく議論されていました。

forum.unity.com

そして今回、Addressbles1.17.4でついに公式なサポートが追加されました。

Addressablesの1.17系が現時点ではプレビュー版なので同期ロードもプレビュー版とはなりますが、
本記事ではこの機能の使い方についてまとめます。

同期ロードの仕方

従来の非同期ロードでは、AsyncOperationHandleをawaitやコルーチンで待機する必要がありました。
今回実装された同期ロードではその代わりに、以下のようにWaitForCompletion()を呼ぶことで同期的な待機ができます。

using UnityEngine;
using UnityEngine.AddressableAssets;

public class Example : MonoBehaviour
{
    private void Start()
    {
        // 今まで通りロードメソッドを呼ぶ
        var op = Addressables.LoadAssetAsync<GameObject>("FooPrefab");
        
        // WaitForCompletionで同期的にロード完了を待機
        var prefab = op.WaitForCompletion();
        
        // 使い終わったらリリース(今まで通り)
        Addressables.Release(op);
    }
}

Resources.Loadと置き換えるには

さて前節の実装ではリソースの解放をする必要がある関係でソースコードが複数行になってしまっています。
が、すでに書かれているResources.Loadを置き換えることを考えるともう少し簡潔に書けると嬉しいところです。

// Resources.Loadで読み込む例
var prefab = Resources.Load<GameObject>("FooPrefab");

// Addressablesでもこんな感じで書きたい(理想)
var prefab = Addressables.Load<GameObject>("FooPrefab");
Addlerを使って一行で書く

Addressablesは明示的に解放処理をする必要があるため、上記のように複数行にまたがる記述になってしまいます。
そこでこの解放処理を簡潔に書くため、Addlerというライブラリ(僕が公開してるライブラリです)を使います。

light11.hatenadiary.com

これを使うとリソースの寿命を指定したGameObjectと紐づけ、そのGameObjectが破棄されるときに一緒に解放することができます。
記述は以下のように一行で行えるようになり、また解放をし忘れてメモリリークする心配もなくなります。

using Addler.Runtime.Core;
using UnityEngine;
using UnityEngine.AddressableAssets;

public class Example : MonoBehaviour
{
    private void Start()
    {
        // gameObjectが破棄されたら自動的にこのアセットも解放される
        var prefab = Addressables.LoadAssetAsync<GameObject>("FooPrefab").BindTo(gameObject).WaitForCompletion();
    }
}

なおAddlerの機能はこれだけではなく、オブジェクトプールやプリロードなどの機能を包括したライブラリになっています。

さらにシンプルに書く

実際に使うときにはこれをラップしてもっと簡潔に書けるクラスを作っておくのが良いかと思います。

using Addler.Runtime.Core;
using UnityEngine;
using UnityEngine.AddressableAssets;

public class ResourceLoader
{
    private readonly GameObject _defaultBindTarget;

    public ResourceLoader(GameObject defaultBindTarget)
    {
        _defaultBindTarget = defaultBindTarget;
    }
    
    public T Load<T>(string address, GameObject bindTarget = null)
    {
        if (bindTarget == null)
        {
            bindTarget = _defaultBindTarget;
        }
        return Addressables.LoadAssetAsync<T>(address).BindTo(bindTarget).WaitForCompletion();
    }
}

こんな感じで作っておけば以下のようにとてもシンプルにロードできます。

using Addler.Runtime.Core;
using UnityEngine;
using UnityEngine.AddressableAssets;

public class Example : MonoBehaviour
{
    private ResourceLoader _resourceLoader;
    
    private void Start()
    {
        _resourceLoader = new ResourceLoader(gameObject);

        // 同期ロード
        _resourceLoader.Load<GameObject>("FooPrefab");
    }
}
ついでにEZAddresserを宣伝(Resourcesライクなアドレスを自動的に付与する)

さてResourcesと置き換える上でもう一つ障害となるのが、アドレスの設定です。
上記のようにFooPrefabを読み込むには、あらかじめそのPrefabにFooPrefabというアドレスを設定しておく必要があります。

f:id:halya_11:20210413172311p:plain
アドレス設定

これは上図のウィンドウにアセットをドラッグ&ドロップしてから名前を設定したりするのですが、
これを手作業でやるのはさすがに人間の仕事ではありません。

EZAddresserを使うと、「Addressables」という名前のフォルダを作ってそこにアセットを放り込むだけで自動的にアドレスが付与されます。

light11.hatenadiary.com

デフォルトではファイル名がそのままアドレスとなりますが、 設定を変えればResourcesフォルダのように、
「Addressablesフォルダからの相対パス(拡張子なし)」をアドレスにすることもできます。

つまりこの設定をして既存のResourcesフォルダの名前を「Addressables」に変えれば、
ResourcesをAddressablesにそのまま置き換えることが可能になるということです。

ただしこちらのライブラリはUnity2020以降が必要なのでご注意ください。

同期ロード使用時の注意点

さてマニュアルには同期ロード時の注意点としていくつか重要なことが記載されています。

docs.unity3d.com

すべてのAsyncOperationが終わるまで待機される

いま、いくつかのアセットを並列で読み込んでいる状況を考えます。
このうちいずれか一つのみWaitForCompletion()を使って同期的にロードを待つと、
直感的にはそれだけが同期的に待機され、他のアセットは非同期で読み込まれる挙動になると想像できます。

しかしながら実際には、いずれか一つのみWaitForCompletion()したとしても、
その時点で存在するすべてのAsyncOperationが終わるまで同期的に待機が行われるそうです。

これはかなり微妙な仕様ですがエンジン上の制約であり、
何が読み込み中であるかちゃんと管理できている状況下でのみWaitForCompletion()を使うように書かれています。

Unity2021.2.0以降じゃないと遅い(どの程度遅いかは謎)

またUnity2021.2.0よりも前のバージョンで同期ロードを使うとパフォーマンスに影響があるとも書かれています。
どの程度影響あるのかはわかりませんが、WaitForCompletion()が呼び出されたときに行われているロード処理が多いほど影響があるそうです。

ダウンロードには原則使わない

Addressableのロードメソッドは、リソースがローカルになければダウンロードを行います。
この挙動はWaitForCompletion()を使った場合にも同様ですが、
ダウンロードのような時間のかかる処理は基本的に同期的に処理するべきではありません(固まるので)。

従って、ダウンロード時にはWaitForCompletion()を使わないことが推奨されています(意図的ならOKです)。

関連

light11.hatenadiary.com

light11.hatenadiary.com

light11.hatenadiary.com

参考

github.com

github.com

forum.unity.com

docs.unity3d.com