UnityのAddressables：UniTaskとUniRxを活用した簡単な使い方サンプル

# 導入
初めてのQiita投稿です。
今回は、私がUnityのAddressablesシステムについて学んだ内容をサンプルコードとともに共有します。
この記事がどなたかの参考になれば幸いです。

# 勉強の背景
最近、Unityの開発に関する勉強を始めました。
Addressablesは現場でもよく利用されるケースが多く、押さえておくべき重要な知識だと思い、学習しました。
Addressablesは、アセットの管理とロードを簡単かつ効率的に行うためのツールです。
活用事例も多く、参考になる記事や書籍が豊富にあり、大変勉強になりました。
私の記事は、知識のインプットや正確な学びという面では、まだ足りない部分があるかもしれませんが、これからも学び続けていきたいと思っています。

# 私の記事の価値
この記事に掲載しているサンプルコードはそのままプロジェクトに組み込んで使えるように意識して作成しています。難しいことは一旦置いておいて、とりあえず実装して動かしてみる際に参考になるのではないでしょうか？

# サンプルコードの作成意図
ソーシャルゲームなどのアプリ開発において、起動時に必要なデータをダウンロードし、起動中はダウンロード済みのデータを扱うことが多いです。
また、Addressableのメモリ管理の効果を発揮するためリリースの管理も重要です。
そういった、機能をすぐに取り込めることを意識して、以下の機能を実装したAddressableManagerを作成しました。
```
	•	ダウンロード容量の確認：指定されたAddressableグループラベルのダウンロードサイズを取得する。
	•	一括ダウンロード：指定されたAddressableグループラベルの依存関係を一括でダウンロードする。
	•	ダウンロード進捗の報告：ダウンロード処理中の進捗をリアルタイムで報告する。
	•	ダウンロード完了の通知：ダウンロード処理が正常に完了したときに通知する。
	•	ダウンロードエラーの通知：ダウンロード処理中にエラーが発生したときに通知する。
	•	キャッシュのクリア：指定されたAddressableグループラベルのキャッシュをクリアする。
	•	アセットの非同期ロード：アセットを非同期にロードし、成功とエラー処理のコールバックを提供する。
	•	シーンの非同期ロード：シーンを非同期にロードし、成功とエラー処理のコールバックを提供する。
	•	リリース処理の管理：ロードしたアセットのリリース（解放）を適切に管理し、メモリリークを防止する。
```

# AddressableManagerの実装
それではコードの紹介です。
以下は、AddressableManagerクラスを使用してアセットを管理する方法を示しています。このサンプルコードは、そのままプロジェクトに追加して使えるように意識しています。

```c#
using System;
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
using UniRx;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
using UnityEngine.ResourceManagement.ResourceProviders;
using UnityEngine.SceneManagement;

namespace Script.Util
{
    /// <summary>
    /// Addressableアセットを管理し、アセットグループのダウンロード、ロード、およびキャッシュのクリア機能を提供するクラス。
    /// IDisposableを実装して、リソースが適切に解放されるようにする。
    /// このクラスを返してロードしたアセットはこのクラスのDisposeで解放を行います。ただしLoadSceneは活用時にシーンを跨ぐことが想定されるため解放は含めていません。
    /// Singleでの読み込みの場合は考慮不要ですが、マルチシーンの活用の場合は自分で管理してください。
    /// </summary>
    public class AddressableManager : IDisposable
    {
        /// <summary>
        /// コンストラクタ
        /// 扱うAddressableのグループごとにインスタンスを作成して利用する想定。
        /// </summary>
        /// <param name="targetGroup">このクラスで扱うアドレッサブルのグループ</param>
        public AddressableManager(string targetGroup)
        {
            TargetGroup = targetGroup;
            _handles = new List<AsyncOperationHandle>();
        }
        /// <summary>
        /// このクラスで扱うアドレッサブルのグループ
        /// </summary>
        public string TargetGroup { get; }
        private readonly List<AsyncOperationHandle> _handles;
        private readonly Subject<float> _downloadProgressSubject = new Subject<float>();
        private readonly Subject<Unit> _downloadCompleteSubject = new Subject<Unit>();
        private readonly Subject<Exception> _downloadErrorSubject = new Subject<Exception>();

        /// <summary>
        /// ダウンロード処理中の進捗を報告するイベント。
        /// </summary>
        public IObservable<float> OnDownloadProgress => _downloadProgressSubject;

        /// <summary>
        /// ダウンロード処理が正常に完了したときにトリガーされるイベント。
        /// </summary>
        public IObservable<Unit> OnDownloadComplete => _downloadCompleteSubject;

        /// <summary>
        /// ダウンロード処理中にエラーが発生したときにトリガーされるイベント。
        /// </summary>
        public IObservable<Exception> OnDownloadError => _downloadErrorSubject;

        /// <summary>
        /// 指定されたAddressableグループラベルのダウンロードサイズを取得する。
        /// </summary>
        /// <param name="groupLabel">確認するAddressableグループのラベル。</param>
        /// <returns>ダウンロードサイズ（バイト単位）を表すUniTask。</returns>
        public async UniTask<long> GetDownloadSizeAsync(string groupLabel)
        {
            var sizeHandle = Addressables.GetDownloadSizeAsync(groupLabel);
            long downloadSize = await sizeHandle.ToUniTask();
            return downloadSize;
        }

        /// <summary>
        /// 指定されたAddressableグループラベルの依存関係をダウンロードする。
        /// 進捗を報告し、完了およびエラーのイベントをトリガーする。
        /// </summary>
        /// <returns>非同期操作を管理するためのUniTaskVoid。</returns>
        public async UniTaskVoid DownloadGroupAsync()
        {
            try
            {
                var downloadHandle = Addressables.DownloadDependenciesAsync(TargetGroup);
                _handles.Add(downloadHandle);

                while (!downloadHandle.IsDone)
                {
                    float progress = downloadHandle.PercentComplete;
                    _downloadProgressSubject.OnNext(progress);
                    await UniTask.Yield();
                }

                if (downloadHandle.Status == AsyncOperationStatus.Succeeded)
                {
                    _downloadCompleteSubject.OnNext(Unit.Default);
                }
                else
                {
                    throw new Exception("Failed to download updated assets.");
                }
            }
            catch (Exception ex)
            {
                _downloadErrorSubject.OnNext(ex);
            }
        }

        /// <summary>
        /// 指定されたAddressableグループラベルのキャッシュをクリアする。
        /// Addressables.ClearDependencyCacheAsyncをラップしているだけ。
        /// </summary>
        public void ClearCache()
        {
            // Asyncと名前がついているのに戻り値がvoidになっている。
            Addressables.ClearDependencyCacheAsync(TargetGroup);
        }

        /// <summary>
        /// アセットを非同期にロードし、成功とエラー処理のコールバックを提供する。
        /// </summary>
        /// <typeparam name="T">ロードするアセットの型。</typeparam>
        /// <param name="assetAddress">ロードするアセットのアドレス。</param>
        /// <param name="onSuccess">アセットが正常にロードされたときに呼び出されるコールバック。</param>
        /// <param name="onError">ロード中にエラーが発生したときに呼び出されるコールバック。</param>
        /// <returns>非同期操作を表すUniTask。</returns>
        public async UniTask LoadAssetAsync<T>(string assetAddress, Action<T> onSuccess, Action<Exception> onError)
        {
            try
            {
                var handle = Addressables.LoadAssetAsync<T>(assetAddress);
                _handles.Add(handle);
                T asset = await handle.ToUniTask();

                if (handle.Status != AsyncOperationStatus.Succeeded)
                {
                    throw new Exception($"Failed to load asset at address: {assetAddress}");
                }

                onSuccess?.Invoke(asset);
            }
            catch (Exception ex)
            {
                onError?.Invoke(ex);
            }
        }

        /// <summary>
        /// シーンを非同期にロードし、成功とエラー処理のコールバックを提供する。
        /// シーン遷移後に管理しているオブジェクトが破棄される場合に備え、ハンドルは管理リストに追加しない。
        /// </summary>
        /// <param name="sceneAddress">ロードするシーンのアドレス。</param>
        /// <param name="loadMode">シーンのロードモード。</param>
        /// <param name="onSuccess">シーンが正常にロードされたときに呼び出されるコールバック。</param>
        /// <param name="onError">ロード中にエラーが発生したときに呼び出されるコールバック。</param>
        /// <returns>非同期操作を表すUniTask。</returns>
        public async UniTask LoadSceneAsync(string sceneAddress, LoadSceneMode loadMode, Action<SceneInstance> onSuccess, Action<Exception> onError)
        {
            try
            {
                var handle = Addressables.LoadSceneAsync(sceneAddress, loadMode);
                SceneInstance sceneInstance = await handle.ToUniTask();

                if (handle.Status != AsyncOperationStatus.Succeeded)
                {
                    throw new Exception($"Failed to load scene at address: {sceneAddress}");
                }

                onSuccess?.Invoke(sceneInstance);
            }
            catch (Exception ex)
            {
                onError?.Invoke(ex);
            }
        }

        /// <summary>
        /// このAddressableManagerインスタンスが保持するすべてのリソースを解放する。
        /// </summary>
        public void Dispose()
        {
            foreach (var handle in _handles)
            {
                Addressables.Release(handle);
            }
            _handles.Clear();
            _downloadProgressSubject.Dispose();
            _downloadCompleteSubject.Dispose();
            _downloadErrorSubject.Dispose();
        }
    }
}
```

### 実践的なサンプル
#### シーンの非同期ロード
以下は、AddressableManagerを使用してシーンを非同期にロードする例です。進捗状況の表示やエラーハンドリングも含まれています。
```c#
using System;
using Script.Util;
using UniRx;
using UnityEngine;
using UnityEngine.SceneManagement;

namespace Script
{
    public class InitScene : MonoBehaviour
    {
        private AddressableManager _addressableManager;
        private const String AddressableGroup = "default";

        // Start is called before the first frame update
        async void Start()
        {
            //マネージャのインスタンス化　今回はデフォルトGroupのアセットを利用
            _addressableManager = new AddressableManager(AddressableGroup);

            //一度読み込むとダウンロード済みになるので、以下のコメントアウトをかいじょすると初期化され最初からダウンロードされる様になる。
            //_addressableManager.ClearCache(AddressableGroup);
        
            // ダウンロード進捗を購読
            _addressableManager.OnDownloadProgress
                .Subscribe(progress => Debug.Log($"Download progress: {progress * 100:F2}%"))
                .AddTo(this);

            // ダウンロード完了を購読
            _addressableManager.OnDownloadComplete
                .Subscribe(_ =>
                {
                    Debug.Log("Download complete");
                    //次のシーンへ
                    LoadScene();
                })
                .AddTo(this);

            // ダウンロードエラーを購読
            _addressableManager.OnDownloadError
                .Subscribe(ex => Debug.LogError($"Download error: {ex.Message}"))
                .AddTo(this);

            // 特定のグループの差分ダウンロードサイズを確認
            long downloadSize = await _addressableManager.GetDownloadSizeAsync(AddressableGroup);
            Debug.Log($"Download size: {downloadSize} bytes");
            // ダウンロード開始　（downloadSizeが０だと即座に終わるが、その後の遷移を統一させるためによぶ
            _addressableManager.DownloadGroupAsync().Forget();
        }

        async void LoadScene()
        {
            await _addressableManager.LoadSceneAsync("Assets/Scenes/TitleScene.unity", LoadSceneMode.Single,
                sceneInstance =>
                {
                    // ロード成功時の処理
                    Debug.Log($"Scene {sceneInstance.Scene.name} loaded successfully.");
                },
                error =>
                {
                    // ロード失敗時の処理
                    Debug.LogError($"Error loading scene: {error.Message}");
                });
        }
        private void OnDestroy()
        {
            _addressableManager.Dispose();
        }
    }
}
```

#### アセットの非同期ロード
次に、AddressableManagerを使用してアセットを非同期にロードし、ボタン押下でシーンを切り替える例です。
```C#
using System;
using Script.UI;
using Script.Util;
using UniRx;
using UnityEngine;
using UnityEngine.SceneManagement;

namespace Script
{
    public class TitleScene : MonoBehaviour
    {
        [SerializeField] private Canvas canvas;
        private AddressableManager _addressableManager;
        private const String AddressableGroup = "default";

        private CustomButton _nextButton;
        private async void Start()
        {
            _addressableManager = new AddressableManager(AddressableGroup);
            await _addressableManager.LoadAssetAsync<GameObject>("Assets/Prefabs/NextButton.prefab", obj =>
                {
                    Debug.Log($"Scene {obj} loaded successfully.");
                    // ロード成功時の処理
                    _nextButton = Instantiate(obj, canvas.transform).GetComponent<CustomButton>();
                    
                    //取得したボタン押下時に次のシーン遷移を設定
                    _nextButton.OnClickAsObservable
                        .Subscribe(_ => { LoadScene(); })
                        .AddTo(this.gameObject);
                },
                error =>
                {
                    // ロード失敗時の処理
                    Debug.LogError($"Error loading scene: {error.Message}");
                });
        }

        async void LoadScene()
        {
            await _addressableManager.LoadSceneAsync("Assets/Scenes/MainScene.unity", LoadSceneMode.Single,
                sceneInstance =>
                {
                    // ロード成功時の処理
                    Debug.Log($"Scene {sceneInstance.Scene.name} loaded successfully.");
                },
                error =>
                {
                    // ロード失敗時の処理
                    Debug.LogError($"Error loading scene: {error.Message}");
                });
        }

        private void OnDestroy()
        {
            _addressableManager.Dispose();
        }
    }
}

```


# 実行環境について
以下の環境で行いました。
```
Unity:2022.3.27f1
Addressables:1.21.21
UniTask:2.5.5
UniRx:7.1.0
```
活用事例の実践的なサンプルコードはaddressablesで以下のアセットがdefaultというラベルで
登録されている前提の処理です。
```
Assets/Prefabs/NextButton.prefab
Assets/Scenes/MainScene.unity
Assets/Scenes/TitleScene.unity
```


# まとめ
この記事では、私がUnityのAddressablesシステムを学ぶ過程でまとめた情報を共有しました。UniTaskとUniRxを活用することで、非同期処理やイベント駆動型のアセット管理を簡単に行うことができます。
今回の内容が、誰かの参考になれば幸いです。理解不足や改善点がありましたら、ぜひ教えてください。
※記事の執筆中にキャンセル処理の実装ができていないことに気がつきました.

# 免責事項
この記事で紹介したサンプルコードは、私自身の学習と経験をもとに作成したものです。
このコードを使用することで生じたトラブルや損害については、一切の責任を負いかねます。
自己責任のもとでご利用ください。