しっぽを追いかけて

ぐるぐるしながら考えています

Unity と猫の話題が中心   掲載内容は個人の私見であり、所属組織の見解ではありません

Unity の Addressables のリモートアセットを残したまま更新する

※ これは 2022/09/02 時点の Unity 2022.1.15f1 Addressables v1.20.5 Windows 11 の情報です

最新版では動作が異なる可能性がありますのでご注意ください

前回引き続き Addressables を使いつつ、今回はアプリ実行中に古いアセットを解放して消滅させることなく、リモートアセットを更新してみたい

例によって下記の うちのこメーカー で作成した2枚の画像を同じアセットとして並列表示させる

切り替える画像

何も考えずにアプリ実行中にリモートカタログを更新してアセットをダウンロードし直すと AssetBundle の重複で例外発生するのでこれに対処する

画像の切り替えは以前試した下記の方法を流用する

https://www.matatabi-ux.com/entry/2022/08/25/100000www.matatabi-ux.com

まず新旧2つの画像を並べるために画面を下記のように変更

Image を2つ並べる

1つだった Image を2つ左右に並べてそれぞれ Image(cafe)Image(mocha) という名前を指定、Test.cs に Cafe ImageMocha Image のフィールドを生やして各自アタッチした

Test.cs は下記のように変更

using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
using UnityEngine.ResourceManagement.ResourceLocations;
using UnityEngine.ResourceManagement.ResourceProviders;
using UnityEngine.UI;

public class Test : MonoBehaviour
{
    [SerializeField]
    private Image cafeImage = null;

    [SerializeField]
    private Image mochaImage = null;

    private Image thumbnailImage = null;

    private AsyncOperationHandle<Sprite> handle = default;

    private const string RemoteLoadPath = @"http://localhost/StandaloneWindows64/{0}/";
    private static string OriginalRootPath = null;
    private static string CurrentRootPath = null;

    private AssetBundle currentBundle = null;
    private string currentName = null;
    private Hash128 currentHash = default;

    public async void Start()
    {
        // Addresables 初期化
        var locator = await Addressables.InitializeAsync().Task;

        // 初期化時のルートパス取得
        var info = Addressables.GetLocatorInfo(locator.LocatorId);
        OriginalRootPath = info.HashLocation.InternalId.Replace(Path.GetFileName(info.HashLocation.InternalId), "");

        // アドレス書き換えを有効化
        Addressables.InternalIdTransformFunc = MylIdTransform;

        await SetCatAsset("Cafe");
    }

    private static string MylIdTransform(IResourceLocation location)
    {
        // ルートパスを置き換える
        if (location.InternalId.StartsWith(OriginalRootPath))
        {
            return location.InternalId.Replace(OriginalRootPath, CurrentRootPath);
        }
        return location.InternalId;
    }

    private async Task SetCatAsset(string catName)
    {
        Cat.Name = catName;

        this.thumbnailImage = catName switch
        {
            "Cafe" => this.cafeImage,
            "Mocha" => this.mochaImage,
            _ => null
        };

        // ルートパスの再評価
        CurrentRootPath = string.Format(RemoteLoadPath, Cat.Name);

        // リモートカタログに変更があれば更新する
        var catalogs = await Addressables.CheckForCatalogUpdates(true).Task;
        if (catalogs != null && catalogs.Count > 0)
        {
            var locators = await Addressables.UpdateCatalogs(catalogs, true).Task;

            // リモートアセットの取得先を確認
            if (locators[0].Locate("Assets/Sprites/cat.png", typeof(Sprite), out var locations))
            {
                // リモートアセットの URL は Dependencies に入っている
                foreach(var location in locations[0].Dependencies)
                {
                    UnityEngine.Debug.Log($"--------------> uri={Addressables.InternalIdTransformFunc(location)}");
                }

                var option =  locations
                        .SelectMany(x => x.Dependencies)
                        .Select(x => x.Data)
                        .OfType<AssetBundleRequestOptions>()
                        .First();
                var newName = option.BundleName;
                var newHash = Hash128.Parse(option.Hash);

                if (this.currentBundle != null)
                {
                    UnityEngine.Debug.Log($"--------------> {this.currentName}{this.currentHash}】 -> {newName}{newHash}】");

                    // 同じ名前で内容(Hash)が異なる AssetBundle がないか調べる
                    if (this.currentName.Equals(newName) && this.currentHash != newHash)
                    {
                        // アセットのインスタンスは残したまま、古い AssetBundle を解放してキャッシュからも破棄する
                        this.currentBundle.Unload(false);
                        if (Caching.IsVersionCached(newName, this.currentHash))
                        {
                            Caching.ClearCachedVersion(newName, this.currentHash);
                        }
                    }
                }

                // BundleName と Hash を保持しておく
                this.currentName = newName;
                this.currentHash = newHash;
            }
        }

        AsyncOperationHandle download = default;
        try
        {
            // AssetBundle を抽出するために Load 前に Download
            download = Addressables.DownloadDependenciesAsync("Assets/Sprites/cat.png");
            var resources = await download.Task as List<IAssetBundleResource>;
            this.currentBundle = resources[0].GetAssetBundle();

            // Addressables 経由で Sprite を読み込んで表示
            this.handle = Addressables.LoadAssetAsync<Sprite>("Assets/Sprites/cat.png");
            this.thumbnailImage.sprite = await this.handle.Task;
        }
        finally
        {
            // Load が終わったら Download ハンドルは用済みなので解放
            if (download.IsValid())
            {
                Addressables.Release(download);
            }
        }
    }

    /// <summary>
    /// Mocha ボタン押下イベントハンドラ
    /// </summary>
    public async void OnMochaButtonClicked()
    {
        await SetCatAsset("Mocha");
    }

    public void OnDestroy()
    {
        // いらなくなったら handle を Release する
        if (this.handle.IsValid())
        {
            Addressables.Release(this.handle);
        }
    }
}

アセットのダウンロード前にリモートカタログから BundleNameHash を取得し、BundleName が同じなのに Hash が異なる場合、既存の AssetBundleUnload しているところがポイント

Addressables のアセットの AssetBundleAddressables.DownloadDependenciesAsync() の結果を List<IAssetBundleResource> にキャストすることで抽出することができる

これでお試し・・・実行後 Cafe の画像が表示されたのち Mocha のボタンを押下

同じ名前の別の画像アセットを同時表示

エラーなくちゃんと2つの画像が並んで表示された!