【Unity】【Addressable】AssetDatabaseにアセットがあったらそれを読み込み、なければリモートから取得する仕組みを作る

UnityのAddressableアセットシステムでAssetDatabaseにアセットがあったらそれを読み込み、なければリモートから取得する仕組みを作る方法についてまとめました。

Unity2020.3.40
Addressables 1.18.19

やりたいこと

まず、以下のようなプロジェクトを考えます。

  • Unity プロジェクト内(AssetDatabase)には全てのアセットが必ずしも存在せず、部分的に存在する可能性がある
    • PlasticSCMPerforce などのバージョン管理ツールで部分チェックアウトを行うようなケース
    • 全てのアセットをプロジェクトに取り込むと動作が重くなるため
  • Addressable でビルド済みのリソースがリモート環境に配置されている

ここで、ワークフローを踏まえて次のようなリソースのロード戦略を実現したいとします。

  • プロジェクト(AssetDatabase)にリソースが存在すればそれをロードする
    • 修正確認をリソースのビルドをせずに即座にできるようにするため
  • プロジェクトにないリソースはリモートからロードする
    • 全てのアセットをプロジェクトに取り込まなくても動作させたいため

本記事ではこれを実現する方法をまとめます。

方針と実装

実装方法はいくつか考えられますが、今回は ResourceLocator を使って以下の方針で実装します。

  • Addressableに登録されている ResourceLocator から ResourceLocationMapを取得
    • カタログを元に作られた、キーと ResourceLocation の情報を保持しているオブジェクト
  • プロジェクトに存在するリソースについては、ResourceLocationAssetDatabase のものに上書きする

以下はこの方針を元に実装したスクリプトです。
細かい説明はコメントに記述しています。

using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.AddressableAssets.ResourceLocators;
using UnityEngine.ResourceManagement.ResourceLocations;
using UnityEngine.ResourceManagement.ResourceProviders;

public sealed class Example : MonoBehaviour
{
    private IEnumerator Start()
    {
        yield return Addressables.InitializeAsync();

        // AssetDatabaseProviderを使用するので、ResourceManagerに登録
        var resourceProviders = Addressables.ResourceManager.ResourceProviders;
        if (resourceProviders.All(x => x.GetType() != typeof(AssetDatabaseProvider)))
            resourceProviders.Add(new AssetDatabaseProvider());

        // AssetDatabaseから読むリソースのキーとResourceLocationの対応表を作成
        var localLocations = new Dictionary<string, List<IResourceLocation>>();

        // 対応表にAssetDatabaseから読み込むものを追加
        {
            var address = "fooAddress";
            var assetPath = "Assets/Foo.prefab";
            var providerName = typeof(AssetDatabaseProvider).FullName;
            var assetType = typeof(GameObject);
            var location = new ResourceLocationBase(address, assetPath, providerName, assetType);
            localLocations.Add(address, new List<IResourceLocation> { location });
        }

        // すでに登録されているResourceLocationMap(カタログ用のIResourceLocator)を取得
        var resourceLocationMap = Addressables
            .ResourceLocators
            .OfType<ResourceLocationMap>()
            .FirstOrDefault();
        
        if (resourceLocationMap == null)
            yield break;

        // Keyがアドレスやラベルと一致して、それらがAssetDatabaseにあったらValueを書き換える
        foreach (var location in resourceLocationMap.Locations.ToArray())
        {
            if (!(location.Key is string key))
                continue;

            if (localLocations.ContainsKey(key))
                resourceLocationMap.Locations[location.Key] = localLocations[key];
        }

        // プロジェクト内にアセットがあったらそれを読み込み、なければリモートから取得する
        var handle = Addressables.LoadAssetAsync<GameObject>("fooAddress");
        yield return handle;
    }
}

ローカル環境のAddressablesが適切に設定されている場合には、localLocationsAddressableAssetSettings から作ったりしても良いと思います。

ResourceManagerResourceProviderResourceLocator については以下の記事にもまとめていますので、必要に応じて参照してください。

light11.hatenadiary.com

light11.hatenadiary.com

注意点

上記の実装の注意点として、カタログが更新されると ResourceLocationMap が上書き更新されるという点が挙げられます。
したがって、カタログを更新した後には再度 AssetDatabase 用の ResourceLocation に書き換える必要があるのでご注意ください。

その他検討した方法メモ

その他検討した方法をメモしておきます。

AssetDatabase用のResourceLocatorを追加する
  • ResourceLocationMap を書き換えるのではなく、AssetDatabase 用の IResourceLocator を追加する方法
  • AddressablesResourceLocators の最初に AssetDatabase 用のものを追加する
    • Addressables の仕様として、最初に見つかった有効な ResourceLocator を採用するため
  • 単体アセットをロードする際にはこの実装で問題なかった
  • Addressables.LoadAssetsAsync で複数アセットをロードする際に問題が起こった
  • AssetDatabase用の ResourceLocatorResourceLocationMap の両方から IResourceLocation が取られてしまい、AssetDatabase とリモートの両方から同じリソースのロードが走ってしまう結果になった
新しいResourceLocatorを実装する
  • 以下の要件を備えたResourceLocatorを新しく実装する方法
    • プロジェクト内にアセットがある場合には AssetDatabase 用の ResourceLocation を返す
    • プロジェクト内にアセットがない場合にはリモート用の ResourceLocation を返す
  • ResourceLocationMap を削除してこの ResourceLocator を追加する
    • 追加する際に第二引数と第三引数にカタログの情報を渡す
    • カタログを更新したらこの処理を再度行う
  • この方法はおそらく実現可能ですが、検討しただけで今回は試してはいません

関連

light11.hatenadiary.com

light11.hatenadiary.com