[unity]Addressable Asset System(AAS) アドレス(ラベル)名で解放したい

Addressable Asset System(AAS) 1.16Asset Bundle に比べて遥かに便利ですが、いざ使ってみると解放が面倒に感じました。

Addressables.Release(ASyncOperationHandle handle)

handle か…。
これを読み込んだ側でいちいち取っておくのは面倒ですよね。

Addressables.InstantiateAsync()ReleaseInstance() を使うという手もありますが GameObject 用なので、AudioClip やら Sprite やらといったリソースに使うのは、(使えなくもないけど)面倒そうです。
こんな感じでアドレス名(やラベル名)解放したい。

Addressables.Release(string key)

ということで、それが出来るようなヘルパークラスを作ってみます。
ついでに、現在の読み込みアセット数や、指定したアセットが現在読み込み中かを確認するメソッドなんかもつけておきます。

ソースコード

AddressablesKey.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;

public class AddressablesKey
{
    enum eLoadStatus
    {
        None = 0,
        Load,
        Ready,
    }

    class AssetEntity
    {
        /// <summary>アドレス、ラベル</summary>
        public string               Key;
        /// <summary>参照カウンタ</summary>
        public int                  RefCount;
        /// <summary>ロード状況</summary>
        public eLoadStatus          LoadStatus = eLoadStatus.None;
        /// <summary>ロード中にキャンセル指示が来たら true</summary>
        public bool                 LoadCancel;
        /// <summary>Addressables のロードハンドラ</summary>
        public AsyncOperationHandle Handle;
    }

    static Dictionary<string, AssetEntity> entities = new Dictionary<string, AssetEntity>();
    
    /// <summary>
    /// Addressables.LoadAssetAsync() の代わり
    /// </summary>
    /// <typeparam name="T">読み込むデータのデータ型</typeparam>
    /// <param name="key">アドレス、ラベル</param>
    /// <param name="action">引数は ASyncOperationHandle ではなく T型 のインスタンス</param>
    public static void LoadAssetAsync<T>(string key, System.Action<T> action = null)
    {
        loadAssetAsync(key, action, null);
    }

    /// <summary>
    /// Addressables.LoadAssetsAsync() の代わり
    /// </summary>
    /// <typeparam name="T">読み込むデータのデータ型</typeparam>
    /// <param name="key">アドレス、ラベル</param>
    /// <param name="actions">引数は ASyncOperationHandle ではなく T型 のインスタンス配列</param>
    public static void LoadAssetsAsync<T>(string key, System.Action<IList<T>> actions = null)
    {
        loadAssetAsync(key, null, actions);
    }

    /// <summary>
    /// 現在のロードファイル数を取得します
    /// </summary>
    public static int GetLoadCount()
    {
        int loadCount = 0;
        
        foreach (var pair in entities)
        {
            if (pair.Value.LoadStatus == eLoadStatus.Load)
            {
                loadCount++;
            }
        }

        return loadCount;
    }
    
    /// <summary>
    /// 指定したアドレス、ラベルの読み込み完了を確認します
    /// </summary>
    public static bool CheckLoadDone<T>(string key)
    {
        string typekey = $"{key} {typeof(T)}";

        if (entities.ContainsKey(typekey) == false)
        {
            return false;
        }
        if (entities[typekey].LoadStatus != eLoadStatus.Ready)
        {
            return false;
        }
        return true;
    }
    
    /// <summary>
    /// アセットアンロード
    /// </summary>
    /// <typeparam name="T">読み込んだデータのデータ型</typeparam>
    /// <param name="key">アドレス、ラベル</param>
    public static void Release<T>(string key)
    {
        string typekey = $"{key} {typeof(T)}";

        if (entities.ContainsKey(typekey) == false)
        {
            return;
        }

        AssetEntity entity = entities[typekey];
        if (--entity.RefCount > 0)
        {
            return;
        }

        if (entity.LoadStatus == eLoadStatus.Ready)
        {
            unload(typekey);
        }
        else
        if (entity.LoadStatus == eLoadStatus.Load)
        {
            // ロード中は、ロード完了を待ってからアンロードする
            entity.LoadCancel = true;
            CoroutineAccessor.Start(loadCancel(typekey));
        }
    }
    
    /// <summary>
    /// ロード本体
    /// </summary>
    static void loadAssetAsync<T>(string key, System.Action<T> action = null, System.Action<IList<T>> actions = null)
    {
        string typekey = $"{key} {typeof(T)}";

        if (entities.ContainsKey(typekey) == true)
        {
            AssetEntity entity = entities[typekey];
            entity.RefCount++;

            if (entity.LoadStatus == eLoadStatus.Ready)
            {
                // 既に読み込まれているならキャッシュで complete
                loadCompleted(entity, action, actions);
            }
            else
            if (entity.LoadStatus == eLoadStatus.Load)
            {
                // 既に読み込み中なら読み込み完了イベントで complete
                entity.Handle.Completed += 
                    (op) =>
                    {
                        loadCompleted(entity, action, actions);
                    };
            }
        }
        else
        {
            AssetEntity entity = new AssetEntity();
            entity.RefCount++;

            entity.LoadStatus = eLoadStatus.Load;
            if (action != null)
            {
                entity.Handle = Addressables.LoadAssetAsync<T>(key);
            }
            else
            {
                entity.Handle = Addressables.LoadAssetsAsync<T>(key, null);
            }
            
            entity.Key = typekey;
            entity.Handle.Completed +=
                (op) =>
                {
                    loadCompleted(entity, action, actions);
                    entity.LoadStatus = eLoadStatus.Ready;
                };
            entities[typekey] = entity;
        }
    }

    /// <summary>
    /// ロード完了
    /// </summary>
    static void loadCompleted<T>(AssetEntity entity, System.Action<T> action = null, System.Action<IList<T>> actions = null)
    {
        if (entity.LoadCancel == true)
        {
            // キャンセル指示が入ってるので、complete を成立させない
            return;
        }

        if (action != null)
        {
            action?.Invoke((T)entity.Handle.Result);
        }
        else
        {
            actions?.Invoke((IList<T>)entity.Handle.Result);
        }
    }

    /// <summary>
    /// ロード中にキャンセル入った場合のアンロード処理
    /// ロード完了を待ってからアンロードする
    /// </summary>
    static IEnumerator loadCancel(string typekey)
    {
        AssetEntity entity = entities[typekey];

        while (entity.LoadStatus == eLoadStatus.Load)
        {
            yield return null;
        }
        
        unload(typekey);
    }
    
    /// <summary>
    /// アンロード
    /// </summary>
    static void unload(string typekey)
    {
        AssetEntity entity = entities[typekey];

        Addressables.Release(entity.Handle);
        entities.Remove(typekey);
    }
}

CoroutineAccessor.cs

このクラスは static メソッド内でコルーチンを使うための仕掛けです。
UniRx を使っている場合、その StartCoroutine に置き換えてもいいかもしれません。

//#define USING_UniRx

using System.Collections;
using UnityEngine;

public class CoroutineAccessor : MonoBehaviour
{
#if !USING_UniRx
    /// <summary>
    /// GameObject アタッチなしで StartCoroutine を使うための instance
    /// </summary>
    static CoroutineAccessor Instance
    {
        get
        {
            if (instance == null)
            {
                GameObject obj = new GameObject(nameof(CoroutineAccessor));
                DontDestroyOnLoad(obj);
                instance = obj.AddComponent<CoroutineAccessor>();
            }
            return instance;
        }
    }
    static CoroutineAccessor instance;

    /// <summary>
    /// OnDisable
    /// </summary>
    void OnDisable()
    {
        if (instance != null)
        {
            Destroy(instance.gameObject);
            instance = null;
        }
    }
#endif
    
    /// <summary>
    /// StartCoroutine
    /// </summary>
    public static Coroutine Start(IEnumerator routine)
    {
#if USING_UniRx
        return UniRx.MainThreadDispatcher.StartCoroutine(routine);
#else
        return Instance.StartCoroutine(routine);
#endif
    }

    /// <summary>
    /// StopCoroutine
    /// </summary>
    public static void Stop(Coroutine coroutine)
    {
        Instance.StopCoroutine(coroutine);
    }

    /// <summary>
    /// StopCoroutine Reference is cleard by null.
    /// </summary>
    public static void StopNull(ref Coroutine coroutine)
    {
        Stop(coroutine);
        coroutine = null;
    }
}

使い方

AddressablesKey.LoadAssetAsync(string key, System.Action<T> action = null)
AddressablesKey.LoadAssetsAsync(string key, System.Action<IList<T>> actions = null)

Addressables のものとそれほど違いはありませんが、読み込み完了後は AsyncOperationHandle ではなく、読み込んだデータのみを返します。

AddressablesKey.LoadAssetAsync<Object>("sample", (data) => Debug.Log($"{data.name}"));
AddressablesKey.LoadAssetsAsync<Object>("samples",
    (datas) =>
    {
        foreach (var data in datas) Debug.Log($"{data.name}"));
    }
);

この隠蔽は、エラー処理の分岐なども考えると邪魔なのかもしれません。
Addressables は今のところ失敗した原因を教えてくれませんが…。

AddressablesKey.Release(string key);

今回やりたかったこと。指定した名前のリソースを解放します。

同フレームで複数回ロード&解放をしても、正しく動作するようにしています。
例えば以下のようなコードだと、ロードの action は呼ばれません。

AddressablesKey.LoadAssetAsync<Object>("sample", (data) => Debug.Log($"sample load done."));
AddressablesKey.Release<Object>("sample");

// "sample load done." は表示されない(action メソッドコール前にリリースしたため)

あまりないケースですが、例えばボイス読み込みがユーザー入力でキャンセルされる…といったケースを想定しています。

AddressablesKey.CheckLoadDone<T>(string key)

指定された名前のロードが完了されれば true、されていない・もしくはロードが呼ばれていなければ false を返します。
コルーチンでロードを待つ場合などに使えます。

IEnumerator loadWait()
{
    AddressablesKey.LoadAssetAsync<Object>("sample", (data) => Debug.Log($"sample"));
    while (AddressablesKey.CheckLoadDone("sample") == false)
    {
        yield return null;
    }
    Debug.Log("Load done.");
}

以下の順でログが表示される
sample
Load done.

AddressablesKey.GetLoadCount()

現在ロード中のアセット数を返します。
数ファイルロードして、それを全て待つ場合などに使えます。

IEnumerator loadWait()
{
    AddressablesKey.LoadAssetAsync<Object>("sample", (data) => Debug.Log($"sample"));
    AddressablesKey.LoadAssetAsync<Object>("sample2", (data) => Debug.Log($"sample2"));
    AddressablesKey.LoadAssetAsync<Object>("sample3", (data) => Debug.Log($"sample3"));
    while (AddressablesKey.GetLoadCount() > 0)
    {
        yield return null;
    }
    Debug.Log("Load done.");
}

以下の順でログが表示される
sample
sample2
sample3
Load done.

終わりに

本当は「その名前のアドレス(ラベル)は存在するか」というメソッドも欲しかったんですが、長くなってしまうので別記事にて紹介します。

本当は Addressables.Exists<T>(string key) みたいなのがあるといいんですけどね…。
Addressables.ResourceLocators から名前は取れますが、type Locate () メソッドにて自分が指定した結果でしかわからないらしい。

私が無知なだけのような気がしなくもない。

よろしければ Twitter をフォローしてもらえると嬉しいです!

返信を残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA