【Unity】【Addressable】ResourceManagerの使い方まとめ

Addressableアセットシステムの中で使われているResourceManagerの使い方をまとめました。

Unity2019.3.5
Addressables 1.7.5

Resource Manager?

Addressableアセットシステムは、内部的にResourceManagerというリソース管理システムを使用しています。
これを使うとResourcesやAssetBundleなど様々なリソース読み込みを共通のインタフェースで実装できます。

ProviderをカスタムしたりAddressableを少し高度に使う場合には理解が必要になりますが、公式ドキュメントは全くと言っていいほどありません。
おとなしくAddressableの標準機能を使えというお告げな気もしますが、本記事ではこのResource Managerの使い方をまとめてみます。

Resourcesから読み込む

さてまずはResource ManagerでResourcesフォルダからリソースを読み込むシンプルな例で基本的な流れを確認します。
まずはコード全文です。

using UnityEngine;
using UnityEngine.ResourceManagement;
using UnityEngine.ResourceManagement.ResourceLocations;
using UnityEngine.ResourceManagement.ResourceProviders;

public class Example : MonoBehaviour
{
    private ResourceManager _resourceManager;

    private void Awake()
    {
        // ResourceMangerを生成
        _resourceManager = new ResourceManager();
        // 使用するProviderを登録
        _resourceManager.ResourceProviders.Add(new LegacyResourcesProvider());
    }
    
    private void Start()
    {
        var resourceId = "Example"; // リソースID(LegacyResourcesProviderを使う場合はResourcesからの拡張子なしの相対パス)
        var providerId = typeof(LegacyResourcesProvider).FullName; // 使用するProviderの型のFullName
        var resouceType = typeof(GameObject); // リソースの型

        // ResourceLocationを生成する
        var location = new ResourceLocationBase(resourceId, resourceId, providerId, resouceType);
        
        // locationを使って読み込む
        var handle = _resourceManager.ProvideResource<GameObject>(location);
        handle.Completed += x =>
        {
            Instantiate(handle.Result);
            
            // 不要になったら解放
            _resourceManager.Release(handle);
        };
    }

    private void OnDestroy()
    {
        // ResourceManagerを破棄
        _resourceManager.Dispose();
    }
}

ResourceManagerはいろんなリソースの読み込み方を同じインタフェースで実現している関係で
使い方が若干複雑なので、上のコードで登場した概念について以下で一つ一つ説明します。

Providerについて

まずAwakeメソッドで生成しているLegacyResourcesProviderについて説明します。

ResourceManagerではProviderと呼ばれるクラスがリソースの取得を行います。
このProviderには複数の種類が存在し、例えばResourcesから読み込むためのもの、AssetDatabaseから読み込むためのもの、
AssetBundleを読み込むためのもの、AssetBundleからAssetを読み込むためのものなどが用意されています。

今回はそのうちResourcesから読み込むためのProviderであるLegacyResourcesProviderを生成しています。
生成したProviderはResourceManager.ResourceProviders.Add()でResourceManagerに登録しておきます。

_resourceManager.ResourceProviders.Add(new LegacyResourcesProvider());

ちなみにProviderは複数登録しておくことができます。

Resource Locationについて

次にStartメソッド内で生成しているResourceLocationBaseについて説明します。
これはResourceManagerに読み込みで使用するProviderの種類やProviderがリソースを特定するのに使うID、読み込むリソースの型などを教えます。

今回のようにLegacyResourceProviderを使う場合、IDにはResourcesフォルダからの拡張子なしの相対パスを使います。

var resourceId = "Example"; // リソースID(LegacyResourcesProviderを使う場合はResourcesからの拡張子なしの相対パス)
var providerId = typeof(LegacyResourcesProvider).FullName; // 使用するProviderの型のFullName
var resouceType = typeof(GameObject); // リソースの型

// ResourceLocationを生成する
var location = new ResourceLocationBase(resourceId, resourceId, providerId, resouceType);

ちなみにResourceLocationBaseIResourceLocationを実装したクラスです。
こうして作ったIResourceLocationをResourceManagerに渡せばリソースを取得できます。

 _resourceManager.ProvideResource<GameObject>(location)
解放処理について

ResourceManagerは内部でリソースの参照カウントを管理しています。
リソースの取得を行ったときに参照カウントがインクリメントされます。
また不要になったリソースの参照カウントは減らす必要があります。

参照カウントを減らすには、ResourceManager.Release()を呼びます。

_resourceManager.Release(handle);

またResourceManager自体も不要になったら破棄する必要があります。

ResourceManager.Dispose();

AssetDatabaseから読み込む

次にAssetDatabaseProviderを使ってAssetDatabaseからリソースを読み込んでみます。
使い方はLegacyResourcesProviderとほぼ変わりません。

using UnityEngine;
using UnityEngine.ResourceManagement;
using UnityEngine.ResourceManagement.ResourceLocations;
using UnityEngine.ResourceManagement.ResourceProviders;

public class Example : MonoBehaviour
{
    private ResourceManager _resourceManager;

    private void Awake()
    {
        _resourceManager = new ResourceManager();
        // AssetDatabaseProviderを登録
        _resourceManager.ResourceProviders.Add(new AssetDatabaseProvider());
    }
    
    private void Start()
    {
        var resourceId = "Assets/Example/Example.prefab"; // アセットパス
        var providerId = typeof(AssetDatabaseProvider).FullName; // AssetDatabaseProviderのFullName
        var resouceType = typeof(GameObject);

        var location = new ResourceLocationBase(resourceId, resourceId, providerId, resouceType);
        
        var handle = _resourceManager.ProvideResource<GameObject>(location);
        handle.Completed += x =>
        {
            Instantiate(handle.Result);
            _resourceManager.Release(handle);
        };
    }

    private void OnDestroy()
    {
        _resourceManager.Dispose();
    }
}

LegacyResourcesProviderソースコードから変更した部分のみコメントを記載しています。
リソースIDにアセットパス(Assetsフォルダからのパス・拡張子あり)を設定する点に注意が必要です。

AssetBundleからリソースを読み込む

次に若干複雑なAssetBundleからのリソース取得について説明します。
まずはソースコードの全文を記載します。

using UnityEngine;
using UnityEngine.ResourceManagement;
using UnityEngine.ResourceManagement.ResourceLocations;
using UnityEngine.ResourceManagement.ResourceProviders;

public class Example : MonoBehaviour
{
    private ResourceManager _resourceManager;

    private void Awake()
    {
        _resourceManager = new ResourceManager();

        // AssetBundleProviderとBundledAssetProviderを登録する
        _resourceManager.ResourceProviders.Add(new AssetBundleProvider());
        _resourceManager.ResourceProviders.Add(new BundledAssetProvider());
    }

    private void Start()
    {
        // アセットパスをIDにしてResourceLocationを作成
        // ProviderはBundledAssetProvider
        var assetPath = "Assets/Bundles/PrefabA.prefab";
        var location = new ResourceLocationBase(assetPath, assetPath, typeof(BundledAssetProvider).FullName,
            typeof(GameObject));

        // Prefabが所属するAssetBundleをDependenciesに追加
        // ProviderはAssetBundleProvider
        // 順番大事: 当該アセットが含まれているAssetBundleを一番最初に持ってくること
        var prefabBundlePath = "http://(リソース用URL)/prefab_a.bundle";
        location.Dependencies.Add
        (
            new ResourceLocationBase(prefabBundlePath, prefabBundlePath, typeof(AssetBundleProvider).FullName,
                typeof(IAssetBundleResource))
        );

        // 依存するAssetBundleをDependenciesに追加
        // ProviderはAssetBundleProvider
        var shaderBundlePath = "(StreamingAssetsのパス)/defaultlocalgroup_unitybuiltinshaders.bundle";
        location.Dependencies.Add
        (
            new ResourceLocationBase(shaderBundlePath, shaderBundlePath, typeof(AssetBundleProvider).FullName,
                typeof(IAssetBundleResource))
        );

        location.ComputeDependencyHash();

        // ロード
        _resourceManager.ProvideResource<GameObject>(location).Completed += x =>
        {
            Instantiate(x.Result);
            _resourceManager.Release(x);
        };
    }

    private void OnDestroy()
    {
        _resourceManager.Dispose();
    }
}

AssetBundleを読み込むためにはBundledAssetProviderとAssetBundleProviderの二つのProviderを使う必要があります。
以下、それぞれについて細かく説明します。

BundledAssetProvider

BundledAssetProviderはAssetBundleからAssetを読み込むためのProviderです。
アセットパスをIDとして与えると、指定されたAssetBundleからそのアセットを読み込みます。

var assetPath = "Assets/Bundles/PrefabA.prefab";
var location = new ResourceLocationBase(assetPath, assetPath, typeof(BundledAssetProvider).FullName, typeof(GameObject));

さてこのProviderはあくまで与えられたAssetBundleからアセットを読み込むためのものであるため、
AssetBundleのロードやダウンロードは別のProviderで行って、その結果をBundledAssetProviderに渡す必要があります。

AssetBundleProvider

BundledAssetProviderに渡すためのAssetBundleのロードやダウンロードを行うのがAssetBundleProviderです。

AssetBundleProviderは、AssetBundleのURLやStreamingAssetsなどのローカルパスを与えることでそこからAssetBundleを読み込みます。
型にはIAssetBundleResource型を指定します。

// URLを与える場合
var prefabBundlePath = "http://(リソース用URL)/prefab_a.bundle";
var prefabBundleLocation = new ResourceLocationBase(prefabBundlePath, prefabBundlePath, typeof(AssetBundleProvider).FullName, typeof(IAssetBundleResource))

// ローカルパスを与える場合
var shaderBundlePath = "(StreamingAssetsのパス)/defaultlocalgroup_unitybuiltinshaders.bundle";
var shaderBundleLocation = new ResourceLocationBase(shaderBundlePath, shaderBundlePath, typeof(AssetBundleProvider).FullName, typeof(IAssetBundleResource))

ローカルパスを与えた場合にはAssetBundle.LoadFromFileAsync()を使ってAssetBundleが読み込まれます。

これに対してURLを与えた場合にはUnityWebRequestAssetBundle.GetAssetBundle()が使われます。
従って、UnityWebRequestAssetBundleのキャッシュ機構に基づいて、ローカルキャッシュがあればそちらからロードされ、なければダウンロードが行われます。

light11.hatenadiary.com

さてこのようにして作成したAssetBundleProviderは前節で作ったBundledAssetProviderのResourceLocationのDependenciesに追加します。
こうすることでAssetBundleProviderで読み込まれたAssetBundleがBundledAssetProviderに渡されます。
またDependenciesに追加する際には読み込み対象のアセットが含まれるAssetBundleを最初に追加する必要があります。

Sceneを読み込む

ResourceManagerを使ってシーンを読み込む際には使うべきメソッドがResourceManager.ProvideScene()に変わります。

組み込みシーンを読み込む

まずビルド設定にシーンを追加してビルドにシーンを含んでいる場合には、以下のように読み込みます。

using System;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement;
using UnityEngine.ResourceManagement.ResourceLocations;
using UnityEngine.ResourceManagement.ResourceProviders;
using UnityEngine.SceneManagement;

public class Example : MonoBehaviour
{
    private ResourceManager _resourceManager;
    private SceneProvider _sceneProvider;

    private void Awake()
    {
        _resourceManager = new ResourceManager();

        // SceneProviderを生成
        _sceneProvider = new SceneProvider();
    }

    private void Start()
    {
        // Assetsからの相対パス(Assetsは含まない) = Build Settingsに表示されるパス で指定する必要がある
        // シーン名だけだと実機だと読み込めるがエディタでうまくいかない(ケースがある)ので注意
        // AssetsPathだとエディタだとうまくいくが実機だと読めないので注意
        var path = "Scenes/Example2";
        
        // ResourceLocationを作成
        // 型はSceneInstance
        var location = new ResourceLocationBase(path, path, typeof(SceneProvider).FullName, typeof(SceneInstance));

        // ProvideSceneで読み込み
        _resourceManager.ProvideScene(_sceneProvider, location, LoadSceneMode.Additive, true, 0);
    }

    private void OnDestroy()
    {
        _resourceManager.Dispose();
    }
}

パスにはAssetsからの相対パス(Build Settingsに表示されているパス)を指定する点に注意してください。

AssetBundleから読み込む

次にAssetBundleに格納されているシーンを読み込みます。
要領は非シーンのリソースをAssetBundle経由で読み込む際とほぼ同じです。

using System;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement;
using UnityEngine.ResourceManagement.ResourceLocations;
using UnityEngine.ResourceManagement.ResourceProviders;
using UnityEngine.SceneManagement;
using UnityEngine.UIElements;

public class Example : MonoBehaviour
{
    private ResourceManager _resourceManager;
    private SceneProvider _sceneProvider;

    private void Awake()
    {
        _resourceManager = new ResourceManager();
        _sceneProvider = new SceneProvider();
        _resourceManager.ResourceProviders.Add(new BundledAssetProvider());
        _resourceManager.ResourceProviders.Add(new AssetBundleProvider());
    }

    private void Start()
    {
        // シーン名をIDにしてResourceLocationを作成
        var name = "Example2";
        // Assetsからの相対パスでもOK
        //var name = "Scenes/Example2";
        // AssetPathでもOK
        //var name = "Assets/Scenes/Example2.unity";
        var location = new ResourceLocationBase(name, name, typeof(BundledAssetProvider).FullName, typeof(SceneInstance));

        // Sceneを含むAssetBundleをDependenciesに追加
        var sceneBundlePath = "http://(リソース用URL)/scene_a.bundle";
        location.Dependencies.Add
        (
            new ResourceLocationBase(sceneBundlePath, sceneBundlePath, typeof(AssetBundleProvider).FullName,
                typeof(IAssetBundleResource))
        );

        // 依存するAssetBundleをDependenciesに追加
        var shaderBundlePath = "(StreamingAssetsのパス)/defaultlocalgroup_unitybuiltinshaders.bundle";
        location.Dependencies.Add
        (
            new ResourceLocationBase(shaderBundlePath, shaderBundlePath, typeof(AssetBundleProvider).FullName,
                typeof(IAssetBundleResource))
        );

        location.ComputeDependencyHash();

        // ProvideSceneでロード
        _resourceManager.ProvideScene(_sceneProvider, location, LoadSceneMode.Additive, true, 0);
    }

    private void OnDestroy()
    {
        _resourceManager.Dispose();
    }
}

関連

light11.hatenadiary.com