Addressable Assets Systemをちゃんと導入するための技術検証まとめ

こんにちは、Unityエンジニアの佐藤です。2015年に新卒入社し、2年半ほどサーバーサイドエンジニアとして、2年弱ほどクライアントエンジニアとしてサイバーエージェントのソーシャルゲーム開発に携わってきました。

Unityのアセット管理システムであるAddressable Asset Systemについて調査・検証を行いました。今回はその調査結果とともにAddressable Asset Systemがどんなものなのかをご紹介したいと思います。

環境

  • Unity 2019.3.12f1
  • Addressable Asset System 1.8

Addressable Asset Systemとは

Addressable Asset Systemは一言で言うとアドレスを渡すとリソースを取得してくれる機能です。
リソースの取得先が、

  • ローカルに生データがあるのか
  • ローカルのアセットバンドル内にあるのか
  • リモートからアセットバンドルをダウロードする必要があるのか

に関わらず、呼び出し側は同じインターフェースでリソースをロードできます。Resource.Load([リソースへのパス])と同じ感覚でリソースをロードすることができますが、Addressable Asset Systemでは必ずしもResourcesフォルダ以下にリソースを配置する必要はありません。その代わりにロードしたいリソースにアドレスを付与する(後述します)必要があります。

Addressable Asset Systemを使ってみる

最初にAddressable Asset Systemとはどのようなものか、実際に利用しながら、紹介していきます

セットアップ

Unity2019.3以上を使っているならば、Package Manager > Show preview packages を選択することでAddressable Asset Systemをインストールすることができます。

リソースをAddressable Asset Systemで管理する

適当なPrefabをAddressable Asset Systemで管理します。CubeなどのGameObjectをPrefab化した後、それをInspectorで見てみると名前の下にAddressableチェックボックスが確認できます。
チェックボックスをOnにするとAddressableの管理対象になります。

Addressable Window を開くことで今チェックしたPrefabを確認できます。

下記のようにAddressable Asset WindowでCube.prefabが登録されている事を確認できれば、Addressablesクラスを使ってCube.prefabにアクセスすることができます。

AddressableAddPrefab.png

Addressable Asset System でリソースをロードする

次に上記のような手順でAddressable Asset Systemで管理する設定を行ったリソースをロードします。
ロードの種類ですが、

  • アセットバンドルをビルドして、アセットバンドルからリソースのロードを行う方法
  • アセットバンドルをビルドせずに直接リソースのロードを行う方法

など複数あります。

アセットバンドルをビルドせずに動作させる

下記のようにPlay Mode ScriptFast Modeになっていれば、アセットバンドルをビルドせずに動作させることができます。
Addressableの実装的にはAssetDatabase経由でリソースを取得しています。

PlayMode.png

これで適当なロードスクリプトをシーンのどこかに配置すればCube.prefabが生成されます。

public class LoadScript : MonoBehaviour
{
    void Start()
    {
        StartCoroutine("LoadCube", "Assets/Prefabs/Cube.prefab");
    }

    private IEnumerator LoadCube(string address)
    {
        var handle = Addressables.InstantiateAsync(address);
        yield return handle;
    }
}
InstantiateCube.png

アセットバンドルをローカルからロードして動作させる

下記のようにPlay Mode ScriptPacked Play Modeになっていれば、ビルドしたアセットバンドルを読み込んで動作させることができます。

PackedMode.png

どこからアセットバンドルを読み込むかはグループごとに設定できます。Addressable Asset Window でDefault Local Groupをクリックするとインスペクターに設定が表示されるので、
Build PathがLocalBuildPathに、Load PathがLocalLoadPathになっていることを確認します。

LocalGroupBuildPath.png

この場合はビルドする必要があるのでビルドボタンからビルドします。

これで↑のロードスクリプトを使ってCube.prefabを生成することができます。

InstantiateCube.png

アセットバンドルをリモートからロードして動作させる

アセットバンドルをリモートから読み込む設定にして動作させることができます。Addressable Window のHostingボタンを押すことでローカルサーバー設定ウインドウを立ち上げることができます。

AddHosting1.png
AddHosting2.png

Addressable Asset Settings のRemoteLoadPathをローカルサーバーのアドレスに変更します。

次はGroupの設定ファイルのBuild PathをRemoteBuildPathに、Load PathをRemoteLoadPathに変更します。

defaultLocalGroup.png

この状態でビルドすると、Assets/ServerData/以下にカタログファイル(catalog_XXX.jsonとcatalog_XXX.hash)とアセットバンドルファイルが生成されます。
カタログファイルについては後述しますが、簡単に言うとリソースの依存関係情報が入っているjsonファイルと、そのjsonの変更チェック用のテキストファイルの組です。

ローカルサーバーを起動したままエディタを実行すると上記のLoadScript.csをそのまま使って、サーバーからアセットバンドルをダウンロードしてCube.prefabを生成することができます。

InstantiateCube.png

まとめ

  • インスペクターのAddressableチェックボックスONにするとAddressableがそのリソースを認識する
  • インスペクターのAddressableチェックボックスONにするとリソースにアドレスが付与される
  • デフォルトのアドレスはそのリソースのフォルダパス
  • アセットバンドルがリモートにあるかローカルにあるかに関わらずロードするプログラムは同じものが使える

Addressable Asset Systemとは

Addressable Asset Systemは一言で言うとアドレスを渡すとリソースを取得してくれる機能です。
前述したようにリソースの取得先が、

  • ローカルに生データがあるのか
  • ローカルのアセットバンドル内にあるのか
  • リモートからアセットバンドルをダウロードする必要があるのか

に関わらず、呼び出し側は同じインターフェースで呼び出せるところがポイントです。実際上記ではロード先が変わってもロードするスクリプトは同じものを使うことができました。
Resource.Load([リソースへのパス])と同じ感覚でリソースをロードすることができますが、Addressableでは必ずしもResourcesフォルダ以下にリソースを配置する必要はありません。

void Start()
{
    StartCoroutine("LoadCube");
}

private IEnumerator LoadCube()
{
    // ここでのアドレスは「"Assets/Prefabs/Cube.prefab"」
    var handle = Addressables.LoadAssetAsync<GameObject>("Assets/Prefabs/Cube.prefab");
    yield return handle;
    if (handle.Status == AsyncOperationStatus.Succeeded)
    {
        Instantiate(handle.Result);
    }
}

アドレスを付与する

Addressable Asset SystemをUnityにインポートすると、Prefabファイルだけでなく、Addressableが管理可能なすべてのもの(jpg,png,prefab,scene,…)のインスペクタ上にAddressableチェックボックスが出現します。
このチェックボックスをONにすることでアドレスを付与することができます。デフォルトのアドレスはそのリソースのフォルダパスです。

このアドレスは自由に変更可能です。リソースをAddressable管理にした時のデフォルトがそのリソースのパスになるというだけで、好きなように編集できます。
例えば”Assets/Prefabs/Cube.prefab”というアドレスを”Cube.prefab”に変えてしまうこともできます。
そうすると呼び出し側もAddressables.LoadAssetAsync<GameObject>("Cube.prefab");というように、新しいアドレスを使うことでリソースをロードすることができます。

add_addressable_cube_edit.gif

アドレスと実際のパスの関係

Window > Asset Management > Addressable Asset で登録されたリソース一覧を確認できるウインドウを起動できます。自分が登録したリソースがここに表示されることを確認することができます。左からAsset Address, アセットタプごとのアイコン, Path, Labelsという順で並んでいます。「Asset Address」がそのリソースを参照したいときに使うアドレス、「Path」が実際のリソースのパスです。参照用文字列と実リソースパスとが別管理になっているところがポイントです。この仕組みによって参照用文字列を書き換えても正しいリソースを特定できます。

アドレスとアセットバンドルの依存関係解決

リソースのロード時に取得リソースを含むアセットバンドルファイル間の依存関係も解決し、追加で必要なリソースのダウンロードも行ってくれます。

例えば、Cube.prefabがAsset1.bundleに入っており、このCube.prefabがTest.matを使っていて、Test.matがAsset2.bundleに入っているとします。このときAddressables.LoadAssetAsync<GameObject>("Cube.prefab");という呼び出しの際に自動的にAsset2.bundleまでダウンロードされます。これはAddressable Asset Systemが「リソース間の依存関係とアセットバンドル間の依存関係を対応付ける情報」を持つことで可能としています。

asset.jpg

まとめ

  • ロードしたいリソースにアドレスを付与するとAddressableクラスからロードできる
  • アドレスの付与は各リソースのインスペクターでAddressableチェックボックスをONにするとできる
  • アドレスは自由に編集可能
  • リソースを別々のアセットバンドルにしてもリソース間の依存関係をアセットバンドル間の依存関係として認識してくれる

コンテンツカタログという仕組み

Addressable Asset Systemが保持しているリソース間の依存関係とアセットバンドル間の依存関係を対応付ける情報をコンテンツカタログと呼びます。コンテンツカタログと呼ばれるファイルの中に、アドレスとリソースのフォルダパスの対応や、リソース間やアセットバンドル間の依存関係解決を行うための情報を保持しています。

依存関係情報はコンテンツカタログに入っている

コンテンツカタログはcatalog.jsoncatalog.hashという2つのファイルで構成されます。このjsonファイルを初期化時に読み込みむことで、「アドレス」と「リソースパス」の対応表やリソース間の依存関係情報を内部に構築します。この2つのファイルはアセットバンドルのビルド時に同時に作成され、すべての依存関係解決はこのcatalog.jsonに書き込まれた情報によって行われます。

前節でビルドしたときのcatalog.jsonの一部抜粋したものを下記に示します。書き込まれているはずの”Cube.prefab”とそのリソースパスはBase64エンコードされている部分にアドレスとリソースのフォルダパスの対応が記述されています。内部の実装を見るとこのcatalog.jsonを読み込んだ時にBase64デコードを行って情報を取り出していることが確認できます。
(※ ContentCatalogData.cs の CreateLocatorメソッドあたりを参照)

{
    "m_InstanceProviderData": {
        "m_Id": "UnityEngine.ResourceManagement.ResourceProviders.InstanceProvider",
        "m_ObjectType": {
            "m_AssemblyName": "Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null",
            "m_ClassName": "UnityEngine.ResourceManagement.ResourceProviders.InstanceProvider"
        },
        "m_Data": ""
    },
    "m_SceneProviderData": {
        "m_Id": "UnityEngine.ResourceManagement.ResourceProviders.SceneProvider",
        "m_ObjectType": {
            "m_AssemblyName": "Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null",
            "m_ClassName": "UnityEngine.ResourceManagement.ResourceProviders.SceneProvider"
        },
        "m_Data": ""
    },
    "m_ResourceProviderData": [
        {
            "m_Id": "UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider",
            "m_ObjectType": {
                "m_AssemblyName": "Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null",
                "m_ClassName": "UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider"
            },
            "m_Data": ""
        },
        {
            "m_Id": "UnityEngine.ResourceManagement.ResourceProviders.BundledAssetProvider",
            "m_ObjectType": {
                "m_AssemblyName": "Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null",
                "m_ClassName": "UnityEngine.ResourceManagement.ResourceProviders.BundledAssetProvider"
            },
            "m_Data": ""
        }
    ],
    "m_ProviderIds": [
        "UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider",
        "UnityEngine.ResourceManagement.ResourceProviders.BundledAssetProvider"
    ],
    "m_InternalIds": [
        "http://192.168.12.8:59068/defaultlocalgroup_assets_all_a24ac760a59d320b3bbb60f775296776.bundle",
        "Assets/Prefabs/Cube.prefab",
        "http://192.168.12.8:59068/defaultlocalgroup_unitybuiltinshaders_9a67fe534833c5f7d495e9fbb4da1db6.bundle"
    ],
    "m_KeyDataString": "BQAAAAAjAAAAZGVmYXVsdGxvY2FsZ3JvdXBfYXNzZXRzX2FsbC5idW5kbGUAGgAAAEFzc2V0cy9QcmVmYWJzL0N1YmUucHJlZmFiACAAAABlNWQ0YWE4YWQ1ZjJhNDRmYWIwNzllYWFlNjQyM2NiYwAsAAAAZGVmYXVsdGxvY2FsZ3JvdXBfdW5pdHlidWlsdGluc2hhZGVycy5idW5kbGUENtPzYA==",
    "m_BucketDataString": "BQAAAAQAAAABAAAAAAAAACwAAAABAAAAAQAAAEsAAAABAAAAAQAAAHAAAAABAAAAAgAAAKEAAAACAAAAAAAAAAIAAAA=",
    "m_EntryDataString": "AwAAAAAAAAAAAAAA/////wAAAAAAAAAAAAAAAAAAAAABAAAAAQAAAAQAAAA20/Ng/////wEAAAABAAAAAgAAAAAAAAD/////AAAAAEsCAAADAAAAAAAAAA==",
    "m_ExtraDataString": "B0xVbml0eS5SZXNvdXJjZU1hbmFnZXIsIFZlcnNpb249MC4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj1udWxsSlVuaXR5RW5naW5lLlJlc291cmNlTWFuYWdlbWVudC5SZXNvdXJjZVByb3ZpZGVycy5Bc3NldEJ1bmRsZVJlcXVlc3RPcHRpb25zrgEAAHsAIgBtAF8ASABhAHMAaAAiADoAIgBhADIANABhAGMANwA2ADAAYQA1ADkAZAAzADIAMABiADMAYgBiAGIANgAwAGYANwA3ADUAMgA5ADYANwA3ADYAIgAsACIAbQBfAEMAcgBjACIAOgAyADIAMgA4ADgAMAA3ADQAMAA1ACwAIgBtAF8AVABpAG0AZQBvAHUAdAAiADoAMAAsACIAbQBfAEMAaAB1AG4AawBlAGQAVAByAGEAbgBzAGYAZQByACIAOgBmAGEAbABzAGUALAAiAG0AXwBSAGUAZABpAHIAZQBjAHQATABpAG0AaQB0ACIAOgAtADEALAAiAG0AXwBSAGUAdAByAHkAQwBvAHUAbgB0ACIAOgAwACwAIgBtAF8AQgB1AG4AZABsAGUATgBhAG0AZQAiADoAIgBkAGUAZgBhAHUAbAB0AGwAbwBjAGEAbABnAHIAbwB1AHAAXwBhAHMAcwBlAHQAcwBfAGEAbABsAC4AYgB1AG4AZABsAGUAIgAsACIAbQBfAEIAdQBuAGQAbABlAFMAaQB6AGUAIgA6ADQAMQA0ADIAfQAHTFVuaXR5LlJlc291cmNlTWFuYWdlciwgVmVyc2lvbj0wLjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPW51bGxKVW5pdHlFbmdpbmUuUmVzb3VyY2VNYW5hZ2VtZW50LlJlc291cmNlUHJvdmlkZXJzLkFzc2V0QnVuZGxlUmVxdWVzdE9wdGlvbnPCAQAAewAiAG0AXwBIAGEAcwBoACIAOgAiADkAYQA2ADcAZgBlADUAMwA0ADgAMwAzAGMANQBmADcAZAA0ADkANQBlADkAZgBiAGIANABkAGEAMQBkAGIANgAiACwAIgBtAF8AQwByAGMAIgA6ADQAMgAyADcAMQAyADkAOQAzADAALAAiAG0AXwBUAGkAbQBlAG8AdQB0ACIAOgAwACwAIgBtAF8AQwBoAHUAbgBrAGUAZABUAHIAYQBuAHMAZgBlAHIAIgA6AGYAYQBsAHMAZQAsACIAbQBfAFIAZQBkAGkAcgBlAGMAdABMAGkAbQBpAHQAIgA6AC0AMQAsACIAbQBfAFIAZQB0AHIAeQBDAG8AdQBuAHQAIgA6ADAALAAiAG0AXwBCAHUAbgBkAGwAZQBOAGEAbQBlACIAOgAiAGQAZQBmAGEAdQBsAHQAbABvAGMAYQBsAGcAcgBvAHUAcABfAHUAbgBpAHQAeQBiAHUAaQBsAHQAaQBuAHMAaABhAGQAZQByAHMALgBiAHUAbgBkAGwAZQAiACwAIgBtAF8AQgB1AG4AZABsAGUAUwBpAHoAZQAiADoAOQAwADIANQA2AH0A",
    "m_Keys": [
        "defaultlocalgroup_assets_all.bundle",
        "Assets/Prefabs/Cube.prefab",
        "e5d4aa8ad5f2a44fab079eaae6423cbc",
        "defaultlocalgroup_unitybuiltinshaders.bundle",
        "1626592054"
    ],
    "m_resourceTypes": [
        {
            "m_AssemblyName": "Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null",
            "m_ClassName": "UnityEngine.ResourceManagement.ResourceProviders.IAssetBundleResource"
        },
        {
            "m_AssemblyName": "UnityEngine.CoreModule, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null",
            "m_ClassName": "UnityEngine.GameObject"
        }
    ]
}

コンテンツカタログの生成と取得

コンテンツカタログの生成

すべての依存関係解決情報が記されたcatalog.jsonはリソースのビルド時に生成されます。リソースのビルドを行うと、アセットバンドルファイルとともにcatalog.jsoncatalog.hashが生成されます。catalog.hashはcatalog.jsonのハッシュ値が記述されたテキストファイルです。

※Addressable1.1 だとデフォルトでcataglog_[日付]という命名になる

コンテンツカタログの取得

Addressable Asset Systemを利用した場合、アプリビルド時(リソースのビルド時ではない!)にアプリのStreamingAssets以下にsettings.jsonと言うもの(catalog.jsonではない!)が書き込まれます。このjsonファイルはcatalog.jsonとは別物です。このsettings.jsonコンテンツカタログの取得先が書き込まれています。

前節でリモートからダウンロードする設定でビルドした場合のsetting.jsonを示します。この場合、catalog.jsonをリモートから取得する設定なので、「AddressablesMainContentCatalogRemoteHash」の設定がhttp://192.168.12.8:59068....となっています。

{
    "m_buildTarget": "StandaloneOSX",
    "m_SettingsHash": "",
    "m_CatalogLocations": [
        {
            "m_Keys": [
                "AddressablesMainContentCatalogRemoteHash"
            ],
            "m_InternalId": "http://192.168.12.8:59068/catalog_2020.05.03.19.43.34.hash",
            "m_Provider": "UnityEngine.ResourceManagement.ResourceProviders.TextDataProvider",
            "m_Dependencies": [],
            "m_ResourceType": {
                "m_AssemblyName": "mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089",
                "m_ClassName": "System.String"
            }
        },
        {
            "m_Keys": [
                "AddressablesMainContentCatalogCacheHash"
            ],
            "m_InternalId": "{UnityEngine.Application.persistentDataPath}/com.unity.addressables/catalog_2020.05.03.19.43.34.hash",
            "m_Provider": "UnityEngine.ResourceManagement.ResourceProviders.TextDataProvider",
            "m_Dependencies": [],
            "m_ResourceType": {
                "m_AssemblyName": "mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089",
                "m_ClassName": "System.String"
            }
        },
        {
            "m_Keys": [
                "AddressablesMainContentCatalog"
            ],
            "m_InternalId": "{UnityEngine.AddressableAssets.Addressables.RuntimePath}/catalog.json",
            "m_Provider": "UnityEngine.AddressableAssets.ResourceProviders.ContentCatalogProvider",
            "m_Dependencies": [
                "AddressablesMainContentCatalogRemoteHash",
                "AddressablesMainContentCatalogCacheHash"
            ],
            "m_ResourceType": {
                "m_AssemblyName": "Unity.Addressables, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null",
                "m_ClassName": "UnityEngine.AddressableAssets.ResourceLocators.ContentCatalogData"
            }
        }
    ],
    "m_ProfileEvents": false,
    "m_LogResourceManagerExceptions": true,
    "m_ExtraInitializationData": [],
    "m_CertificateHandlerType": {
        "m_AssemblyName": "",
        "m_ClassName": ""
    }
}

Addressableクラスの初期化のフローの中に上記のsettings.jsonを読み込んで、記載されているコンテンツカタログを取得するが処理があります。
(※InitializationOperation.csCreateInitializationOperationを参照)

internal static AsyncOperationHandle<IResourceLocator> CreateInitializationOperation(AddressablesImpl aa, string playerSettingsLocation, string providerSuffix)
{
    new ResourceManagerDiagnostics(aa.ResourceManager);
    var jp = new JsonAssetProvider();
    jp.IgnoreFailures = true;
    aa.ResourceManager.ResourceProviders.Add(jp);
    var tdp = new TextDataProvider();
    tdp.IgnoreFailures = true;
    aa.ResourceManager.ResourceProviders.Add(tdp);
    aa.ResourceManager.ResourceProviders.Add(new ContentCatalogProvider(aa.ResourceManager));

    var runtimeDataLocation = new ResourceLocationBase("RuntimeData", playerSettingsLocation, typeof(JsonAssetProvider).FullName);

    var initOp = new InitializationOperation(aa);
    initOp.m_rtdOp = aa.ResourceManager.ProvideResource<ResourceManagerRuntimeData>(runtimeDataLocation);
    initOp.m_ProviderSuffix = providerSuffix;
    return aa.ResourceManager.StartOperation<IResourceLocator>(initOp, initOp.m_rtdOp);
}

まとめ

  • アドレスとリソース間(アセットバンドル間)の依存関係を解決するためにコンテンツカタログという仕組みがある
  • コンテンツカタログはcatalog.jsonとcatalog.hashで構成される
  • catalog.jsonはアドレスとリソース間の依存関係情報が記述されているファイルで、catalog.hashはcatalog.jsonのハッシュ値が記載されているテキストファイル
  • コンテンツカタログの取得先はアプリビルド時にアプリ内に書き込まれるsettings.jsonというファイルに記載されている
  • Addressableクラス初期化時にsettings.jsonを読み込み、コンテンツカタログを取得するという実装になっている

Addressables と ResourceManagerの中身の話

前節の通りAddressable Asset Systemはコンテンツカタログに記載された情報を元に、各種依存関係を解決しつつ、必要なリソースをロードしてくれるシステムです。次はこのシステムがどういうクラス構成でそれぞれがどういった役割を持っているのかを実装を参照しつつ説明します。

Addressableクラスを利用したリソースのロードは下記の画像に示すような引数と返り値が主だったのものとなります。アドレスを文字列で渡し、そのロード状態を表すAsyncOperationHandleが返されます。使用者は「リモートからロードするのか」「ローカルからロードするのか」などの詳細を意識することなく、AsyncOperationHandleの完了を待つだけで使いたいリソースを手に入れることができます。

リソースの配置情報 -IResourceLocation-

IResourceLocationインターフェースはリソースがどこに置かれているかを定義しています。
IResourceLocationに依存関係のあるリソースの情報が入っているおかげで、リソース取得時に自動的に依存関係解決ができます。
IResourceLocationには主に下記の情報が含まれています。

  1. リソースの配置情報
  2. 実際にリソースの取得を行うプロバイダ(後述します)
  3. このリソースに依存しているリソースの配置情報

IResourceLocationの一部抜粋します。

/// <summary>
/// Contains enough information to load an asset (what/where/how/dependencies)
/// </summary>
public interface IResourceLocation
{
    /// <summary>
    /// Internal name used by the provider to load this location
    /// </summary>
    /// <value>The identifier.</value>
    string InternalId { get; }
    /// <summary>
    /// Matches the provider used to provide/load this location
    /// </summary>
    /// <value>The provider id.</value>
    string ProviderId { get; }
    /// <summary>
    /// Gets the dependencies to other IResourceLocations
    /// </summary>
    /// <value>The dependencies.</value>
    IList<IResourceLocation> Dependencies { get; }
.....

ResourceManagerはリソース取得の入力として、このIResourceLocationインターフェースしか受け付けません。しかし前述してきたように、リソースのパスは文字列であり、それを取得するために指定するアドレスも文字列です。利用者としては、文字列でアドレスを渡してリソースを取得したいはずです。

そこで、AddressableクラスがアドレスからIResourceLocationに変換する役目を担っています。Addressables Asset Systemを使う場合、利用者はAddressablesクラスを通じてリソースのロードを行いますが、リソースのロード処理などはResourceManagerクラスへ移譲しています。Addressablesクラスは初期化やアドレス、IResourceLocation間の変換処理がメインのようです。

image.png

リソースの配置探索情報 -IResourceLocator-

AddressableクラスがアドレスからIResouceRocationを取得するための情報がIResourceLocatorに定義されています。このインターフェースにはkeyに対応するIResourceLocationを取り出すメソッドがあることがわかります。Addressableクラスはこのインターフェースを利用してアドレスからIResourceLocationを取り出ししています。

image.png

/// <summary>
/// Interface used by the Addressables system to find th locations of a given key.
/// </summary>
public interface IResourceLocator
{
    /// <summary>
    /// The id for this locator.
    /// </summary>
    string LocatorId { get; }
    /// <summary>
    /// The keys defined by this locator.
    /// </summary>
    IEnumerable<object> Keys { get; }
    /// <summary>
    /// Retrieve the locations from a specified key.
    /// </summary>
    /// <param name="key">The key to use.</param>
    /// <param name="locations">The resulting set of locations for the key.</param>
    /// <returns>True if any locations were found with the specified key.</returns>
    bool Locate(object key, Type type, out IList<IResourceLocation> locations);
}

初期化処理 – InitializationOperation –

前節で示したように、アドレスとリソース間の依存関係情報はcatalog.jsonに記述されており、Addressable Asset Systemは初期化時にこのファイルを読み込みます。前項のリソースの配置探索情報はこのタイミングでロードされます。この初期化時の処理を司るクラスとして、InitializationOperationクラスがあります。Addressableクラスのどんなメソッドを呼び出しても必ず最初にこのクラスが呼ばれるようになっています。

// 明示的に初期化
yield return Addressable.InitializeAsync();

// いきなりロードするメソッドを呼んでも、中で勝手に初期化用のロジックが呼ばれます
yield return Addressable.InstantiateAsync(resourceAddress);

プロバイダ – IResourceProvider –

ResourceManagerはIResourceLocationを元に実際にリソースを取得します。このリソースの取得には取得するリソースの種類ごとに用意されたプロバイダが利用されます。

プロバイダはリソースの配置情報を受け取って実際にリソースを取得してくれるクラス達です。ResourceManagerは何を取得するかに応じて適切なプロバイダが選択します。下記のようにどのプロバイダが適任かを判断して依頼を出すという仕事をしています。どのような方法でリソースを取得するかやリソースの取得が完了したかどうかはResourceManagerがハンドリングしない形になっています。利用者へはProviderOperationの参照を返します。

  • アセットバンドルの取得ならAssetBundleProvider
  • jsonの取得ならJsonAssetProvider
  • 〇〇の取得なら□□Provider
image.png
// AsyncOperationHandleが返ってくる
var handle = Addressables.LoadAssetAsync<GameObject>("Assets/AssetBundleResources/unitychan.prefab");
// 実行の終了を待つ
yield return handle;
// handle.Resultに取得結果が入る
Instantiate(handle.Result);

拡張可能性について

ユーザーがアドレスを渡してからリソースのロードが行われるまでの流れを大まかに追いかけてみました。それぞれのクラスがインターフェースを介してやり取りしているので、ユーザーごとの拡張可能性が高い設計になっています。アセットバンドルではないリソースをこのワークフローで管理したいと思った場合、IResourceProviderを実装した独自クラスを書いてあげれば他のリソースと同様にResourceManagerから取得することができます。Addressableクラスがいけてないなーと思ったら、アドレスからIResourceLocationに変換するクラス(IResouceLocatorを所持するクラス)を自分で作れば、Addressableクラスを使う必要はなくなります。

まとめ

  • Addressablesクラスはアドレスを認識できるが、ResourceManagerクラスはIResourceLocationしか認識できない
  • AddressablesクラスはIResourceLocatorを使ってアドレスをIResourceLocationに変換している
  • Addressablesクラスが持つIResourceLocatorは初期化時にInitializeOperationクラスによってセットアップされる
  • ResourceManagerクラスはIResourceLocationに従って適切なIResourceProviderに処理を依頼する
  • ResourceManagerクラスは処理の状態をProvideOperationとしてユーザーに返す

その他調査した項目

実際にプロジェクトに導入する際に、必要になりそうな項目について調べました。

複数のコンテンツカタログを利用可能か

Addressables.LoadContentCatalogAsync('catalog.jsonのパス');で違うカタログをロードすることができます。このメソッドを呼び出すと、Addressableクラスの初期化時と同じInitializeOperationが呼び出され、自動的にリソース探索情報がセットアップされます。ただし、同じカタログを2度以上読み込むとIResourceLocatorの件数がガンガン重複して増えていくので要注意です。

var h  = Addressables.InitializeAsync();
yield return h;
Debug.Log($"Addressables.ResourceLocators.Count: {Addressables.ResourceLocators.Count}"); // 1

var h2 = Addressables.LoadContentCatalogAsync("http://192.168.12.8:59068/catalog_2020.05.03.19.43.34.json");
yield return h2;
Debug.Log($"Addressables.ResourceLocators.Count: {Addressables.ResourceLocators.Count}"); // 2

ロードしたアセットバンドルの参照カウントの管理について

AssetBundleResourceクラスがアセットバンドルの実体を持っています。Addressableクラスを利用してアセットバンドルに含まれるリソースをロードしようとすると「ResourceManaer > AssetBundleProvider > AssetBundleResource」という順でクラスの呼び出しが行われますが、返り値としてはAsyncOperationHandleクラスのインスタンスを返されます。参照カウント自体はこのAsyncOperationHandleクラスが内部で持っているAsyncOperationBaseクラスの中に実装されており、適切にリリースすることでアセットバンドルの参照カウントもきちんと減るように設計されています。

var handle = Addressables.DownloadDependenciesAsync(assetsPath);
yield return handle;

// 参照カウントがデクリメントされる
// 参照カウントが0になった場合、AssetBundleProvider経由でAssetBundle.Unload(true)が呼ばれる
Addressables.Release(handle);

ResourceManagerはこのAsyncOperationHandleクラスのインスタンスをキャッシュしているので、同じリソースのリクエストがきた場合は参照カウントをインクリメントして同じインスタンスを返します。

// キャッシュしているAsyncOperationの参照カウントを上げて返している
IAsyncOperation op;
int hash = location.Hash(desiredType);
if (m_AssetOperationCache.TryGetValue(hash, out op))
{
    op.IncrementReferenceCount();
    return new AsyncOperationHandle(op);
}

アセットバンドルの参照カウントの値によらず強制アンロードすることはできるのか

リソースのロードの結果をIAssetBundleResourceインターフェースへキャストすることで、AssetBundleクラスヘのアクセスができます。

var handle = Addressables.DownloadDependenciesAsync(assetsPath);
yield return handle;
if (handle.Status == AsyncOperationStatus.Succeeded)
{
    foreach (var resource in handle.Result as System.Collections.Generic.List<UnityEngine.ResourceManagement.ResourceProviders.IAssetBundleResource>)
    {
        // AssetBundleファイル
        AssetBundle ab = resource.GetAssetBundle();
        // 強制Unload
        ab.Unload(true);
    }
}
Addressables.Release(handle);

ダウンロードしたアセットバンドルファイルのキャッシュ機構はあるのか

Addressable標準の機能でアセットバンドルをダウンロードする場合、AssetBundleProvidersが使われます。AssetBundleProvidersはアセットバンドルの取得にUnityWebRequestAssetBundle.GetAssetBundle()を使っているため、HashとCrcが有効であればUnity標準のCache apiが使われることになります。Unity標準のCacheを使いたくないのであれば、アセットバンドルをダウンロードするProvider を自作することになるのかなと思います。

UnityWebRequest CreateWebRequest(IResourceLocation loc)
{
    if (m_Options == null)
        return UnityWebRequestAssetBundle.GetAssetBundle(loc.InternalId);
    var webRequest = !string.IsNullOrEmpty(m_Options.Hash) ?
        UnityWebRequestAssetBundle.GetAssetBundle(loc.InternalId, Hash128.Parse(m_Options.Hash), m_Options.Crc) :
        UnityWebRequestAssetBundle.GetAssetBundle(loc.InternalId, m_Options.Crc);
....
....

ダウンロードサイズを取得できるか

Addressables.GetDownloadSizeAsync(key)で取得することができます。Addressablesクラスのメソッドの引数はアドレスということになるので、ダウンロードが必要なアセットバンドルファイルの依存関係まで含めたすべてのダウンロードサイズを取得できます。

AssetBundleProviders.csに記述されているAssetBundleRequestOptions.ComputeSizeが呼ばれることになります。このメソッドは内部でUnityEngine.Cachingクラスを使ってすでにアプリ内に対象のファイルがあるかどうかをチェックしています。Cache機構を自前で用意してUnityのCacheクラスを使わない選択肢を取っている場合このメソッドは使えないです。ですが、おそらくその場合はAssetBundleProvider自体を使わずに独自のProviderを実装しているはずなので、あまり問題にはならないかもしれません。独自のProviderを作ってアセットバンドルのダウンロードを行う場合、ILocationSizeDataインターフェースを使って、自分でlong ComputeSize(IResourceLocation loc);を実装すれば同様に計算できるはずです。

/// <summary>
/// Computes the amount of data needed to be downloaded for this bundle.
/// </summary>
/// <param name="loc">The location of the bundle.</param>
/// <returns>The size in bytes of the bundle that is needed to be downloaded.  If the local cache contains the bundle or it is a local bundle, 0 will be returned.</returns>
public virtual long ComputeSize(IResourceLocation loc)
{
    if (!ResourceManagerConfig.IsPathRemote(loc.InternalId))
        return 0;
    var locHash = Hash128.Parse(Hash);
#if !UNITY_SWITCH &amp;&amp; !UNITY_PS4
    var bundleName = Path.GetFileNameWithoutExtension(loc.InternalId);
    if (locHash.isValid) //If we have a hash, ensure that our desired version is cached.
    {
        if (Caching.IsVersionCached(new CachedAssetBundle(bundleName, locHash)))
            return 0;
        return BundleSize;
    }
    else //If we don't have a hash, any cached version will do.
    {
        List<Hash128> versions = new List<Hash128>();
        Caching.GetCachedVersions(bundleName, versions);
        if (versions.Count > 0)
            return 0;
    }
#endif //!UNITY_SWITCH &amp;&amp; !UNITY_PS4
    return BundleSize;
}
/// <summary>
/// Interface for computing size of loading a location.
/// </summary>
public interface ILocationSizeData
{
    /// <summary>
    /// Compute the numder of bytes need to download for the specified location.
    /// </summary>
    /// <param name="loc">The location to compute the size for.</param>
    /// <returns>The size in bytes of the data needed to be downloaded.</returns>
    long ComputeSize(IResourceLocation loc);
}

Addressable Asset Systemを通してアセットバンドルファイル以外のリソースを管理できるか

ResourceProviderBaseを継承した独自のプロバイダを定義することで、AddressableクラスとResourceManagerクラスのインターフェースを使いつつアセットバンドル以外のリソースを管理できます。TextDataProviderJsonAssetProviderなどが参考になります。

ビルドしたアセットバンドルを使ってUnityEditor上の実行は可能か

Addressable > Play Mode Script > Packed Play Modeに設定し、BundleedAssetGroupSchemaを利用しているGroupのBuild PathLoad Pathをどちらもローカルにすることで、ローカルのアセットバンドルを利用して実行することができます。Load Pathをリモートアドレスにすればリモートにあるアセットバンドルを利用して実行できるかと思います。

FastMode.png

アセットの更新ができるか

Addressableによるアセットバンドルのビルドを行うと、AddressableAssetsData/[TargetPlatform]フォルダにaddressables_content_state.binというファイルが作成されます。ここにはコンテンツカタログのバージョン情報が記述される(catalog_[日付].jsonの[日付]部分)ので次回からのビルド時はこのaddressables_content_state.binを指定して上書きビルドを行うことで、同一カタログの更新を行うことができます。catalog_[日付].jsonに対応したcatalog_[日付].hashもこのタイミング書き変わるので、このファイルを見てクライアントはリソースが更新されたことを認識してコンテンツカタログとアセットの更新を行います。

この[日付]の部分ですが、addressableの設定ファイルであるAddressalbeAssetSettingsPlayer Version Overrideを利用することでこの部分を好きな文字列に変更可能です。

override_catalog_setting.png
override_catalog.png

まとめ

今回はUnityのリソース管理システムであるAddressable Asset Systemについて調査・検証を行いました。Addressable Asset Systemはそれ自体がシステムとして閉じているわけではなく、システムを構成する各コンポーネントがユーザーによって拡張・変更できる設計であることがわかりました。便利なところはそのまま使いつつ、足りないところや欲しい機能は独自で実装して利用することができます。新しい機能なのでまだまだ手の届かないところがあるかもしれませんが、今後のアップデートやユーザー自身による拡張性を鑑みると、導入するリソース管理システムの選択肢の1つには十分入ると思います。

参考

以下の記事を参考にさせていただきました。

今更誰も教えてくれない、Unityにおけるアセット読み込みについての基礎知識
https://qiita.com/k7a/items/df6dd8ea66cbc5a1e21d

AssetBundleを完全に理解する
https://qiita.com/k7a/items/d27640ac0276214fc850

Addressable Assets Systemを完全に理解する
https://qiita.com/k7a/items/b4fd298bcb64dc968ad1

複雑化するAssetBundleの配信からロードまでを基盤化した話【CEDEC 2017】
https://creator.game.cyberagent.co.jp/?p=4791

『CARAVAN STORIES』のアセットバンドル事例
https://www.slideshare.net/UnityTechnologiesJapan002/unite-2018-tokyocaravan-stories

【Unite 2017 Tokyo】最適化をする前に覚えておきたい技術
https://www.slideshare.net/UnityTechnologiesJapan/unite-2017-tokyo-75775983

Understanding Automatic Memory Management
https://docs.unity3d.com/ja/current/Manual/UnderstandingAutomaticMemoryManagement.html

[Unity 2018.2] AssetBundleのキャッシュを完全に理解する
https://qiita.com/k7a/items/23d909ffeea3bab7dfcb

AssetBundle.LoadFromFileとLoadFromMemoryの挙動の違いについて
https://qiita.com/k7a/items/974c2cc86ef012f6d68a

そろそろ楽がしたい!新アセットバンドルワークフロー&リソースマネージャー詳細解説
https://www.slideshare.net/UnityTechnologiesJapan/unite-2018-tokyo-96499382/UnityTechnologiesJapan/unite-2018-tokyo-96499382

【Unity】【AssetBundle】Unity2017~Unity2018.1 のためのAssetBundle システム構築方法
https://qiita.com/Cova8bitdot/items/20e8962ed0ca0544289d

Unity5 AssetBundleまとめメモ
https://qiita.com/hinagawa/items/da7a960bcdd610a90124

【Unity】AddressableAssetSystemで、他のプロジェクトのAASが作ったAssetBundleを利用する
http://tsubakit1.hateblo.jp/entry/2019/03/27/011230


関連記事一覧

  1. この記事へのコメントはありません。