📗

【Unity】Addressablesを管理するマネージャー

2024/08/29に公開

はじめに

初めての方も、そうでない方もこんにちは!
現役ゲームプログラマーのたむぼーです。
自己紹介を載せているので、気になる方は見ていただければ嬉しいです!

今回は
 Addressablesを管理するマネージャー
について、僕が実践している方法紹介します

https://zenn.dev/tmb/articles/1072f8ea010299

Addressablesについて

Addressablesとは、動的にアセットをロードおよび管理するためのシステム

AddressableManagerについて

AddressableManager
AddressableManager.cs
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
using UnityEngine.ResourceManagement.ResourceProviders;
using UnityEngine.SceneManagement;
using Cysharp.Threading.Tasks;
using UnityEditor.AddressableAssets.Settings;
using UnityEditor.AddressableAssets;
using UnityEditor;

namespace Utils
{
    public class AddressableManager
    {
        static private AddressableManager _instance;
        static public AddressableManager Instance
        {
            get
            {
                if (_instance == null)
                {
                    _instance = new AddressableManager();
                    _instance.Initialize();
                }
                return _instance;
            }
        }

        private bool _isInitialize;
        Dictionary<string, AsyncOperationHandle> _cacheHandle;
        Dictionary<string, AsyncOperationHandle> _staticCacheHandle;

        /// <summary>
        /// 初期化
        /// </summary>
        private void Initialize()
        {
            if (_isInitialize)
            {
                return;
            }

            _cacheHandle = new Dictionary<string, AsyncOperationHandle>();
            _staticCacheHandle = new Dictionary<string, AsyncOperationHandle>();

            _isInitialize = true;
        }

        /// <summary>
        /// アドレスの作成
        /// </summary>
        public string CreateAddress(params string[] paths)
        {
            return string.Concat(paths);
        }

        /// <summary>
        /// アセットの登録
        /// </summary>
        public void RegisterAsset(string assetPath, string groupName)
        {
            AddressableAssetSettings settings = AddressableAssetSettingsDefaultObject.GetSettings(false);

            AddressableAssetGroup group = settings.FindGroup(groupName);
            if (group == null)
            {
                group = settings.CreateGroup(groupName, false, false, false, settings.DefaultGroup.Schemas);
            }

            AddressableAssetEntry entry = settings.CreateOrMoveEntry(AssetDatabase.AssetPathToGUID(assetPath), group, false, false);
            entry.address = assetPath;

            settings.SetDirty(AddressableAssetSettings.ModificationEvent.EntryMoved, entry, true);
            AssetDatabase.SaveAssets();
        }

        /// <summary>
        /// 破棄
        /// </summary>
        public void DisposeAll(bool isStatic = false)
        {
            if (isStatic)
            {
                foreach (AsyncOperationHandle handle in _staticCacheHandle.Values)
                {
                    Addressables.Release(handle);
                }
                _staticCacheHandle.Clear();
            }
            foreach (AsyncOperationHandle handle in _cacheHandle.Values)
            {
                Addressables.Release(handle);
            }
            _cacheHandle.Clear();
        }

        /// <summary>
        /// 破棄
        /// </summary>
        public void Dispose(string address, bool isStatic = false)
        {
            if (isStatic && _staticCacheHandle.TryGetValue(address, out AsyncOperationHandle staticHandle))
            {
                Addressables.Release(staticHandle);
                _staticCacheHandle.Remove(address);
            }
            else if (_cacheHandle.TryGetValue(address, out AsyncOperationHandle dynamicHandle))
            {
                Addressables.Release(dynamicHandle);
                _cacheHandle.Remove(address);
            }
            else
            {
                Debug.LogWarning($"Address {address} not found in cache.");
            }
        }

        /// <summary>
        /// Addressableでアセットを読み込む
        /// </summary>
        public async UniTask<T> LoadAssetAsync<T>(string address, bool isStatic = false) where T : UnityEngine.Object
        {
            string normalizeAddress = CommonUtility.NormalizePath(address);
            try
            {
                if (TryGetCacheHandle(normalizeAddress, out AsyncOperationHandle handle))
                {
                    return await GetAsset<T>(handle);
                }
                handle = Addressables.LoadAssetAsync<T>(normalizeAddress);
                AddCacheHandle(normalizeAddress, handle, isStatic);
                return await GetAsset<T>(handle);
            }
            catch (Exception ex)
            {
                Debug.LogError($"Failed to load addressable asset at {normalizeAddress}: {ex.Message}\n{ex.StackTrace}");
                return null;
            }
        }

        /// <summary>
        /// Addressableで複数アセットを読み込む
        /// </summary>
        public async UniTask<List<T>> LoadAssetsAsync<T>(List<string> addresses, bool isStatic = false) where T : UnityEngine.Object
        {
            List<string> normalizeAddresses = CommonUtility.NormalizePath(addresses);
            try
            {
                List<T> assets = new List<T>();
                foreach (string normalizeAddress in normalizeAddresses)
                {
                    T asset = await LoadAssetAsync<T>(normalizeAddress, isStatic);
                    assets.Add(asset);
                }
                return assets;
            }
            catch (Exception ex)
            {
                Debug.LogError($"Failed to load addressable asset : {ex.Message}\n{ex.StackTrace}");
                return null;
            }
        }

        /// <summary>
        /// Addressableでパッキングアセットを読み込む
        /// </summary>
        public async UniTask<UnityEngine.Sprite[]> LoadPackingAssetAsync(string address, bool isStatic = false)
        {
            string normalizeAddress = CommonUtility.NormalizePath(address);
            try
            {
                if (TryGetCacheHandle(normalizeAddress, out AsyncOperationHandle handle))
                {
                    return await GetAsset<UnityEngine.Sprite[]>(handle);
                }

                handle = Addressables.LoadAssetAsync<UnityEngine.Sprite[]>(normalizeAddress);
                AddCacheHandle(normalizeAddress, handle, isStatic);
                return await GetAsset<UnityEngine.Sprite[]>(handle);
            }
            catch (Exception ex)
            {
                Debug.LogError($"Failed to load addressable asset at {normalizeAddress}: {ex.Message}\n{ex.StackTrace}");
                return null;
            }
        }

        /// <summary>
        /// Addressableでシーンを読み込む
        /// </summary>
        public async UniTask<SceneInstance> LoadSceneAsync(string address, LoadSceneMode loadMode, bool isStatic = false)
        {
            string normalizeAddress = CommonUtility.NormalizePath(address);
            try
            {
                if (TryGetCacheHandle(normalizeAddress, out AsyncOperationHandle handle))
                {
                    return await GetAsset<SceneInstance>(handle);
                }
                handle = Addressables.LoadSceneAsync(normalizeAddress, loadMode, true);
                AddCacheHandle(normalizeAddress, handle, isStatic);
                return await GetAsset<SceneInstance>(handle);
            }
            catch (Exception ex)
            {
                Debug.LogError($"Failed to load scene at {normalizeAddress}: {ex.Message}\n{ex.StackTrace}");
                return default;
            }
        }

        /// <summary>
        /// Addressableで事前にシーンを読み込む
        /// </summary>
        public async UniTask PreloadSceneAsync(string address, LoadSceneMode loadMode, bool isStatic = false)
        {
            string normalizeAddress = CommonUtility.NormalizePath(address);
            try
            {
                if (TryGetCacheHandle(normalizeAddress, out AsyncOperationHandle handle))
                {
                    await UniTask.WaitUntil(() => handle.PercentComplete >= 0.9f);
                    return;
                }
                handle = Addressables.LoadSceneAsync(normalizeAddress, loadMode, false);
                AddCacheHandle(normalizeAddress, handle, isStatic);
                await UniTask.WaitUntil(() => handle.PercentComplete >= 0.9f);
                return;
            }
            catch (Exception ex)
            {
                Debug.LogError($"Failed to load scene at {normalizeAddress}: {ex.Message}\n{ex.StackTrace}");
            }
        }

        /// <summary>
        /// Addressableで事前に読み込んだシーンをアクティベート
        /// </summary>
        public async UniTask ActivatePreloadedSceneAsync(string address)
        {
            string normalizeAddress = CommonUtility.NormalizePath(address);
            try
            {
                if (! TryGetCacheHandle(normalizeAddress, out AsyncOperationHandle handle))
                {
                    throw new Exception($"addressable load error.");
                }
                await UniTask.WaitUntil(() => handle.Status == AsyncOperationStatus.Succeeded && handle.PercentComplete >= 0.9f);
                if (handle.Status == AsyncOperationStatus.Succeeded)
                {
                    SceneInstance sceneInstance = await GetAsset<SceneInstance>(handle);
                    await sceneInstance.ActivateAsync();
                }
            }
            catch (Exception ex)
            {
                Debug.LogError($"Failed to load scene at {normalizeAddress}: {ex.Message}\n{ex.StackTrace}");
            }
        }

        /// <summary>
        /// ハンドルキャッシュに追加
        /// </summary>
        private void AddCacheHandle(string address, AsyncOperationHandle handle, bool isStatic = false)
        {
            if (isStatic)
            {
                _staticCacheHandle.Add(address, handle);
            }
            else
            {
                _cacheHandle.Add(address, handle);
            }
        }

        /// <summary>
        /// キャッシュしたハンドルを取得
        /// </summary>
        private bool TryGetCacheHandle(string address, out AsyncOperationHandle handle)
        {
            if (_staticCacheHandle.TryGetValue(address, out handle))
            {
                return true;
            }

            if (_cacheHandle.TryGetValue(address, out handle))
            {
                return true;
            }

            handle = default;
            return false;
        }

        /// <summary>
        /// アセットを取得
        /// </summary>
        private async UniTask<T> GetAsset<T>(AsyncOperationHandle handle)
        {
            await handle.Task;

            // ハンドルが無効な場合
            if (!handle.IsValid())
            {
                throw new Exception($"Addressable load error: Invalid handle.");
            }

            // ロードが失敗した場合
            if (handle.Status == AsyncOperationStatus.Failed)
            {
                string errorMessage = handle.OperationException != null ? handle.OperationException.Message : "Unknown error";
                throw new Exception($"Failed to load addressable asset. Error: {errorMessage}");
            }

            return (T)handle.Result;
        }
    }
}
CommonUtility
CommonUtility.cs
using System.Collections.Generic;

namespace Utils
{
    static public class CommonUtility
    {
        /// <summary>
        /// パスのスラッシュを正しい形式に変換
        /// </summary>
        static public List<string> NormalizePath(List<string> paths)
        {
            List<string> normalizedPaths = new List<string>();
            foreach (string path in paths)
            {
                normalizedPaths.Add(NormalizePath(path));
            }
            return normalizedPaths;
        }

        /// <summary>
        /// パスのスラッシュを正しい形式に変換
        /// </summary>
        static public string NormalizePath(string path)
        {
            if (string.IsNullOrEmpty(path))
            {
                return path;
            }

            string normalizedPath = path.Replace("\\", "/");

            while (normalizedPath.Contains("//"))
            {
                normalizedPath = normalizedPath.Replace("//", "/");
            }

            return normalizedPath;
        }
    }
}

使い方

・アセットアドレスの作成

// アドレスを生成
string address = AddressableManager.Instance.CreateAddress("path/to/", "assetName", ".identifier");

// ※"path/to/assetName.identifier"になっていれば何でも良い
// CreateAddress("path/". "to/assetName, ".identifier");
// CreateAddress("path/". "to/", "assetName, ".identifier");
// CreateAddress("path/to/assetName, ".identifier");
// CreateAddress("path/to/, "assetName.identifier");

・アセットの読み込み

// addressからアセットを読み込む
GameObject displayObj = await AddressableManager.Instance.LoadAssetAsync<GameObject>(address);

// staticなハンドルとしてaddressからアセットを読み込む(isStaticをtrueにする)
GameObject displayObj = await AddressableManager.Instance.LoadAssetAsync<GameObject>(address, isStatic);

・指定のハンドルを破棄(ハンドルの解放)

// addressに紐づくハンドルを破棄
AddressableManager.Instance.Dispose(address);

// addressに紐づくstaticなハンドルを破棄(isStaticをtrueにする)
AddressableManager.Instance.Dispose(address, isStatic);

・すべてのハンドルを破棄(ハンドルの解放)

// すべてのハンドルを破棄
AddressableManager.Instance.DisposeAll();

// staticなハンドルを含めたすべてのハンドルを破棄(isStaticをtrueにする)
AddressableManager.Instance.DisposeAll(isStatic);

おまけ

・Editor等でスクリプトからアセットを登録

// Addressableに登録
AddressableManager.Instance.RegisterAsset(assetPath, AssetGroupName);

Discussion