【Unity】Addressableアセットシステムでアセットバンドルからアセットをロードする処理をカスタムする

UnityのAddressableアセットシステムでアセットバンドルからアセットをロードする処理をカスタムする方法です。

Unity2019.2.10
Addressable1.3.8

はじめに

この記事ではAddressableでアセットバンドルからアセットをロードする処理をカスタムする方法をまとめます。
Addressableの概念や基礎知識についての説明はこの記事では省略しますが、
以下の記事にまとめていますので、必要に応じて参照してください。

light11.hatenadiary.com

アセットをロードする処理をカスタム

以前、Addressablesでアセットバンドルのダウンロードやロードの処理をカスタムする方法を紹介しました。

light11.hatenadiary.com

こうしてロードされたアセットバンドルから、また別のProviderを使ってアセットをロードします。
このProviderもAddressableで標準で用意されてはいますが、独自で作成することもできます。

本記事ではこの方法についてまとめます。

ソースコード(シンプル版)

それでは早速ですが、まずカスタムProviderのソースコード全文を記載します。

using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.ResourceManagement.ResourceProviders;

[System.ComponentModel.DisplayName("Custom Bundled Asset Provider Simple")]
public class CustomBundledAssetProviderSimple : ResourceProviderBase
{
    public class InternalOp
    {
        private AssetBundleRequest _requestOperation;
        private ProvideHandle _provideHandle;

        public void Fetch(ProvideHandle provideHandle)
        {
            provideHandle.SetProgressCallback(ProgressCallback);
            _provideHandle = provideHandle;
            _requestOperation = null;

            // 依存関係にあるAssetBundleを取得してロード
            var dependencies = new List<object>();
            _provideHandle.GetDependencies(dependencies);
            var bundle = LoadBundleFromDependecies(dependencies);
            if (bundle == null) {
                _provideHandle.Complete<AssetBundle>(null, false, new Exception("Unable to load dependent bundle"));
                return;
            }

            var assetPath = _provideHandle.Location.InternalId;
            // アセットをロード
            _requestOperation = bundle.LoadAssetAsync(assetPath, _provideHandle.Type);
            _requestOperation.completed += op => {
                // ロード完了したらPovideHandle.Completeを呼ぶ
                object result = null;
                if (_requestOperation != null) {
                    result = (_requestOperation.asset != null && _provideHandle.Type.IsAssignableFrom(_requestOperation.asset.GetType())) ? _requestOperation.asset : null;
                }
                _provideHandle.Complete(result, result != null, null);
            };
        }

        /// <summary>
        /// 依存関係にあるアセットバンドルをロードして元のアセットバンドルを返す
        /// </summary>
        private AssetBundle LoadBundleFromDependecies(IList<object> dependencies)
        {
            if (dependencies == null || dependencies.Count == 0) {
                return null;
            }

            AssetBundle bundle = null;
            var isFirst = true;
            for (int i = 0; i < dependencies.Count; i++) {
                var assetBundleResource = dependencies[i] as IAssetBundleResource;
                if (assetBundleResource != null) {
                    // ロード
                    var b = assetBundleResource.GetAssetBundle();
                    if (isFirst) {
                        // 最初のアセットバンドルを返却
                        bundle = b;
                    }
                    isFirst = false;
                }
            }
            return bundle;
        }

        public float ProgressCallback() { return _requestOperation != null ? _requestOperation.progress : 0.0f; }
    }

    public override void Provide(ProvideHandle provideHandle)
    {
        new InternalOp().Fetch(provideHandle);
    }
}

細かい説明はコメントに書いた通りなので省略します。

大枠として、独自のProviderを作るにはResourceProviderBaseを継承したクラスを作ります。
ロードがリクエストされるとProvide()が呼ばれるのでここにロード処理を書きます。

ソースコード(ちゃんとした版)

単体のアセットをロードするだけなら上記のソースコードで充分ですが、
Addressablesにはサブアセットをまとめてロードしたり、特定のサブアセットを指定してロードする機能があります。

これらに対応するためには以下のようにソースコードを書き換える必要があります。

using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.ResourceManagement.ResourceProviders;
using System.Collections;

[System.ComponentModel.DisplayName("Custom Bundled Asset Provider")]
public class CustomBundledAssetProvider : ResourceProviderBase
{
    public class InternalOp
    {
        private AssetBundleRequest _requestOperation;
        private ProvideHandle _provideHandle;
        private string _subObjectName = null;

        public void Fetch(ProvideHandle provideHandle)
        {
            provideHandle.SetProgressCallback(ProgressCallback);
            _subObjectName = null;
            _provideHandle = provideHandle;
            _requestOperation = null;

            // 依存関係にあるAssetBundleを取得してロード
            var dependencies = new List<object>();
            _provideHandle.GetDependencies(dependencies);
            var bundle = LoadBundleFromDependecies(dependencies);
            if (bundle == null) {
                _provideHandle.Complete<AssetBundle>(null, false, new Exception("Unable to load dependent bundle"));
                return;
            }

            var assetPath = _provideHandle.Location.InternalId;
            if (_provideHandle.Type.IsArray) {
                // 全てのサブアセットをリストで表示する場合
                _requestOperation = bundle.LoadAssetWithSubAssetsAsync(assetPath, _provideHandle.Type.GetElementType());
            }
            else if (_provideHandle.Type.IsGenericType && typeof(IList<>) == _provideHandle.Type.GetGenericTypeDefinition()) {
                // 全てのサブアセットをリストで表示する場合(Generic)
                _requestOperation = bundle.LoadAssetWithSubAssetsAsync(assetPath, _provideHandle.Type.GetGenericArguments()[0]);
            }
            else {
                var i = assetPath.LastIndexOf('[');
                if (i > 0) {
                    // 中括弧がある場合はサブアセットから取得
                    var i2 = assetPath.LastIndexOf(']');
                    if (i2 < i) {
                        _provideHandle.Complete<AssetBundle>(null, false, new Exception(string.Format("Invalid index format in internal id {0}", assetPath)));
                    }
                    else {
                        _subObjectName = assetPath.Substring(i + 1, i2 - (i + 1));
                        assetPath = assetPath.Substring(0, i);
                        _requestOperation = bundle.LoadAssetWithSubAssetsAsync(assetPath, _provideHandle.Type);
                    }
                }
                else {
                    // アセットを取得
                    _requestOperation = bundle.LoadAssetAsync(assetPath, _provideHandle.Type);
                }
            }
            _requestOperation.completed += OnComplete;
        }

        /// <summary>
        /// 依存関係にあるアセットバンドルをロードして元のアセットバンドルを返す
        /// </summary>
        private AssetBundle LoadBundleFromDependecies(IList<object> dependencies)
        {
            if (dependencies == null || dependencies.Count == 0) {
                return null;
            }

            AssetBundle bundle = null;
            var isFirst = true;
            for (int i = 0; i < dependencies.Count; i++) {
                var assetBundleResource = dependencies[i] as IAssetBundleResource;
                if (assetBundleResource != null) {
                    // ロード
                    var b = assetBundleResource.GetAssetBundle();
                    if (isFirst) {
                        // 最初のアセットバンドルを返却
                        bundle = b;
                    }
                    isFirst = false;
                }
            }
            return bundle;
        }

        private void OnComplete(AsyncOperation obj)
        {
            object result = null;
            if (_requestOperation != null) {
                if (_provideHandle.Type.IsArray) {
                    result = CreateArrayResult(_provideHandle.Type, _requestOperation.allAssets);
                }
                else if (_provideHandle.Type.IsGenericType && typeof(IList<>) == _provideHandle.Type.GetGenericTypeDefinition()) {
                    result = CreateListResult(_provideHandle.Type, _requestOperation.allAssets);
                }
                else {
                    if (string.IsNullOrEmpty(_subObjectName)) {
                        result = (_requestOperation.asset != null && _provideHandle.Type.IsAssignableFrom(_requestOperation.asset.GetType())) ? _requestOperation.asset : null;
                    }
                    else {
                        if (_requestOperation.allAssets != null) {
                            foreach (var o in _requestOperation.allAssets) {
                                if (o.name == _subObjectName) {
                                    if (_provideHandle.Type.IsAssignableFrom(o.GetType())) {
                                        result = o;
                                        break;
                                    }
                                }
                            }
                        }
                    }
                }
            }
            _provideHandle.Complete(result, result != null, null);
        }

        private Array CreateArrayResult(Type type, System.Object[] allAssets)
        {
            var elementType = type.GetElementType();
            if (elementType == null)
                return null;
            int length = 0;
            foreach (var asset in allAssets) {
                if (asset.GetType() == elementType)
                    length++;
            }
            var array = Array.CreateInstance(elementType, length);
            int index = 0;

            foreach (var asset in allAssets) {
                if (elementType.IsAssignableFrom(asset.GetType()))
                    array.SetValue(asset, index++);
            }

            return array;
        }

        private TObject CreateArrayResult<TObject>(System.Object[] allAssets) where TObject : class
        {
            return CreateArrayResult(typeof(TObject), allAssets) as TObject;
        }

        private IList CreateListResult(Type type, System.Object[] allAssets)
        {
            var genArgs = type.GetGenericArguments();
            var listType = typeof(List<>).MakeGenericType(genArgs);
            var list = Activator.CreateInstance(listType) as IList;
            var elementType = genArgs[0];
            if (list == null)
                return null;
            foreach (var a in allAssets) {
                if (elementType.IsAssignableFrom(a.GetType()))
                    list.Add(a);
            }
            return list;
        }

        private TObject CreateListResult<TObject>(UnityEngine.Object[] allAssets)
        {
            return (TObject)CreateListResult(typeof(TObject), allAssets);
        }

        public float ProgressCallback() { return _requestOperation != null ? _requestOperation.progress : 0.0f; }
    }

    public override void Provide(ProvideHandle provideHandle)
    {
        new InternalOp().Fetch(provideHandle);
    }
}

このあたりはAddressableで用意されているBundled Asset Providerをベースに作ったほうがよさそうです。

Providerを設定する

あとはこのProviderを使うように設定するだけです。
ProviderはAssetGroupのInspectorから設定できます。

Asset Providerから先ほど作ったProviderが選択できるので、これを選択すればOKです。

f:id:halya_11:20191120141839p:plain

これでアセットバンドルからアセットをロードする処理がカスタムできました。

関連

light11.hatenadiary.com

light11.hatenadiary.com