うにてぃブログ

主にUnityとC#に関する記事を書いていきます

【Unity】Addressable Asset Systemを活用したアセットの動的なカタログ登録方法

UnityのAddressable Asset Systemは、アプリケーションのアセットを効率的に管理し、ダイナミックなロードやアンロードを可能にする強力なツールです。このシステムを使用することで、ゲームやアプリの開発者は、リソースの管理を容易にし、ユーザー体験を向上させることができます。

しかし、Addressable Asset Systemのカタログに登録されていないアセットをロードしようとすると、通常はエラーが発生します。この問題を解決するために、UnityのEditor上で動作する便利な処理を開発しました。

こちらの処理は Unity Editor上でのみ動作します。そのため、開発中にアセットをダイナミックにロードしたい場合に有用です。

この機能は、指定されたアドレスのアセットを非同期でロードすることです。しかし、そのアドレスがAddressableのカタログに登録されていない場合、自動的にカタログに登録する処理が行われます。これにより、アプリケーションの実行時にエラーが発生することなく、必要なアセットを動的にロードすることが可能になります。

ただし、TryGetPathToAddressの処理は Addressable パスの付け方によって異なると思うので、環境に合った変換処理が必要になります

using System.IO;
using System.Linq;
using System.Threading.Tasks;
using UnityEditor;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.AddressableAssets.ResourceLocators;
using UnityEngine.ResourceManagement.AsyncOperations;
using UnityEngine.ResourceManagement.ResourceLocations;
using UnityEngine.ResourceManagement.ResourceProviders;

public static class AddressableUtility
{
    private static readonly string LOCATION_ID = "Temp";
    
    /// <summary>
    /// カタログにアドレスが登録されていないのであれば登録する
    /// </summary>
    public static async Task<T> LoadAssetAsync<T>(string address)
    {
        var handle = Addressables.LoadResourceLocationsAsync(address);

        await handle.Task;

        // ロード成功した
        if (handle.Status == AsyncOperationStatus.Succeeded
            && handle.Result != null
            && handle.Result.Count > 0)
        {
            return (T) handle.Result.ElementAt(0);
        }
        
        // アドレスに対応したファイルを探す
        if (!TryGetPathToAddress(address, out var path))
        {
            Debug.LogError("Not Found");
            return default;
        }
        
        var resourceProviders = Addressables.ResourceManager.ResourceProviders;
        if (resourceProviders.All(v => v.GetType() != typeof(AssetDatabaseProvider)))
        {
            resourceProviders.Add(new AssetDatabaseProvider());
        }

        var resourceLocationMap = Addressables.ResourceLocators
            .Where(v => v.GetType() == typeof(ResourceLocationMap))
            .FirstOrDefault(v => v.LocatorId == LOCATION_ID);
        if (resourceLocationMap == null)
        {
            resourceLocationMap = new ResourceLocationMap(LOCATION_ID);
            Addressables.AddResourceLocator(resourceLocationMap);
        }
        
        var location = new ResourceLocationBase(
            address, 
            path, 
            typeof(AssetDatabaseProvider).FullName,
            AssetDatabase.GetMainAssetTypeAtPath(path)
        );
        ((ResourceLocationMap)resourceLocationMap).Add(address, location);
        
        var reloadHandle = Addressables.LoadAssetAsync<T>(address);
        await reloadHandle.Task;
        return reloadHandle.Result;
    }
    
    /// <summary>
    /// 対象のアドレスからパスを検索する
    /// </summary>
    private static bool TryGetPathToAddress(string address, out string path)
    {
        var addressDirName = Path.GetDirectoryName(address);
        var addressFileName = Path.GetFileNameWithoutExtension(address);
        var guids = AssetDatabase.FindAssets(addressFileName, new[] {addressDirName});
        foreach (var guid in guids)
        {
            var assetPath = AssetDatabase.GUIDToAssetPath(guid);
            var fileName = Path.GetFileNameWithoutExtension(assetPath);
            if (fileName != addressFileName)
                continue;

            path = assetPath;
            return true;
        }

        path = null;
        return false;
    }
}

呼び出し処理は以下のようにします

ただし今回の処理では Handle を返してないので内部でリークしてしまうのでよしなに調整する必要があります

public class Sample : MonoBehaviour
{
    public string Address;
    public string Address2;
    
    private async void Start()
    {
        var obj = await AddressableUtility.LoadAssetAsync<GameObject>(Address);
        Instantiate(obj);
        obj = await AddressableUtility.LoadAssetAsync<GameObject>(Address2);
        Instantiate(obj);
    }
}