Synamon’s Engineer blog

Synamonはリアルとデジタルの融合を加速させるため、メタバース領域で法人向けにサービス提供を行うテックカンパニーです。現在開発を進めている「メタバース総合プラットフォーム」をはじめ、メタバース市場の発展に向けた事業展開を行っています。このブログでは、メタバース技術とその周辺の技術、開発全般に関してエンジニアがお話しします。

UnityのAddressablesのRemoteコンテンツ運用 - ContentCatalog

こんにちは、エンジニアリングマネージャーの渡辺(@mochi_neko_7)です。

今回は前回の記事の続きとして、UnityのAddressablesのCatalog(Content Catalog)の詳しい紹介をします。

前回の記事はこちら。

synamon.hatenablog.com

RemoteからAssetBundleをダウンロードして使用するだけなら前回記事の内容でも十分なのですが、アプリケーションのリリース後にRemoteでコンテンツの追加をするためにはそれだけでは不十分で、Catalogをきちんと管理してあげる必要があります。

Localのみでアセットを管理している場合はCatalogの存在はほとんど意識する必要はないのですが、Remoteでアセットを管理する場合には非常に重要なものになります。

本記事ではその理由も含めて、Catalogがどういったものなのか、どうやって使用するのか、どのように運用できるのかを体系的に整理して紹介します。

少し深めの内容になるため初心者向けではなくなってしまいますが、Catalogに関して公式のドキュメントも情報が少ないですし、体系的に説明してくれている情報も見当たらないため、しっかり目に紹介したいと思います。

今回も前回に引き続き使用しているバージョンは以下になります。

  • Unity 2021.3.0f1
  • Addressables 1.19.19
    • 公式ドキュメントは 1.20 の方が情報が多いのでリンクは1.20を使っています

Catalogの役割

前回記事で紹介したAddressablesにおけるアセットのロードフローをおさらいしましょう。

IResourceLocator にAddressを渡して、IResourceLocation を取得し、それから 指定されている IResourceProvider を使用してAssetBundleやアセットをロードする形でした。

synamon.hatenablog.com

この IResourceLocator の役割をし、Addressablesで管理している全てのAssetBundle、アセットの IResourceLocation を内包しているデータがCatalog(ContentCatalog)です。

つまり、AddressからロードするAssetBundleやアセットを特定し、それらをロードするために必要な情報を提供するのがCatalogの役割になります。

当然ですがCatalogに載っていないAssetBundleやアセットはロードすることができませんので、使用しているCatalogにどのAssetBundleやアセットの情報が載っているのかというのを把握することは重要です。

Catalogの中身

Catalogの大まかな役割を把握したところで、実際にCatalogの中身を覗いてみましょう。

Json

catalog.json の拡張子からも分かるように、Catalogの出力されるデータはJsonで記述されているので、中身をテキストで見ることができます。

Mac版のUnityで新規プロジェクトを作成して、Addressablesのパッケージを追加し、設定の初期化をしてからURPのLit ShaderだけAddressableに設定した状態で生成されるCatalogはこのような中身になっています。

{
    "m_LocatorId": "AddressablesMainContentCatalog",
    "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.LegacyResourcesProvider",
            "m_ObjectType": {
                "m_AssemblyName": "Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null",
                "m_ClassName": "UnityEngine.ResourceManagement.ResourceProviders.LegacyResourcesProvider"
            },
            "m_Data": ""
        },
        {
            "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_Id": "UnityEngine.ResourceManagement.ResourceProviders.LegacyResourcesProvider",
            "m_ObjectType": {
                "m_AssemblyName": "Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null",
                "m_ClassName": "UnityEngine.ResourceManagement.ResourceProviders.LegacyResourcesProvider"
            },
            "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.LegacyResourcesProvider",
        "UnityEngine.ResourceManagement.ResourceProviders.BundledAssetProvider",
        ""
    ],
    "m_InternalIds": [
        "{UnityEngine.AddressableAssets.Addressables.RuntimePath}/StandaloneOSX/defaultlocalgroup_assets_all_cf7c3574ff06b3d38474c7759827fb82.bundle",
        "DebugUICanvas",
        "DebugUIPersistentCanvas",
        "Packages/com.unity.render-pipelines.universal/Shaders/Lit.shader",
        "Scenes/SampleScene"
    ],
    "m_KeyDataString": "CgAAAABEAAAAZGVmYXVsdGxvY2FsZ3JvdXBfYXNzZXRzX2FsbF9jZjdjMzU3NGZmMDZiM2QzODQ3NGM3NzU5ODI3ZmI4Mi5idW5kbGUADQAAAERlYnVnVUlDYW52YXMAIAAAAGNmNmNiZGQ2NzIwODlhODQ3OTZlNTVhMjFmZWQxY2JlABcAAABEZWJ1Z1VJUGVyc2lzdGVudENhbnZhcwAgAAAAZjZiMWEwZmU3NWQ1MDA5NDQ5Y2Y1NWFlNzYyMjBlMmIAQAAAAFBhY2thZ2VzL2NvbS51bml0eS5yZW5kZXItcGlwZWxpbmVzLnVuaXZlcnNhbC9TaGFkZXJzL0xpdC5zaGFkZXIAIAAAADkzMzUzMmE0ZmNjOWJhZjRmYTA0OTFkZTE0ZDA4ZWQ3AAsAAABTYW1wbGVTY2VuZQAgAAAAOTljOTcyMGFiMzU2YTA2NDJhNzcxYmVhMTM5NjlhMDUEAAAAAA==",
    "m_BucketDataString": "CgAAAAQAAAABAAAAAAAAAE0AAAACAAAAAQAAAAIAAABfAAAAAgAAAAEAAAACAAAAhAAAAAQAAAADAAAABAAAAAUAAAAGAAAAoAAAAAQAAAADAAAABAAAAAUAAAAGAAAAxQAAAAEAAAAHAAAACgEAAAEAAAAHAAAALwEAAAEAAAAIAAAAPwEAAAEAAAAIAAAAZAEAAAEAAAAIAAAA",
    "m_EntryDataString": "CQAAAAAAAAAAAAAA/////wAAAAAAAAAAAAAAAAAAAAABAAAAAQAAAP////8AAAAA/////wEAAAABAAAAAQAAAAEAAAD/////AAAAAP////8BAAAAAgAAAAIAAAABAAAA/////wAAAAD/////AwAAAAEAAAACAAAAAQAAAP////8AAAAA/////wMAAAADAAAAAgAAAAEAAAD/////AAAAAP////8DAAAABAAAAAIAAAABAAAA/////wAAAAD/////AwAAAAUAAAADAAAAAgAAAAAAAACF+Vmw/////wUAAAAGAAAABAAAAAMAAAD/////AAAAAP////8HAAAABwAAAA==",
    "m_ExtraDataString": "B0xVbml0eS5SZXNvdXJjZU1hbmFnZXIsIFZlcnNpb249MC4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj1udWxsSlVuaXR5RW5naW5lLlJlc291cmNlTWFuYWdlbWVudC5SZXNvdXJjZVByb3ZpZGVycy5Bc3NldEJ1bmRsZVJlcXVlc3RPcHRpb25zrAIAAHsAIgBtAF8ASABhAHMAaAAiADoAIgBjAGYANwBjADMANQA3ADQAZgBmADAANgBiADMAZAAzADgANAA3ADQAYwA3ADcANQA5ADgAMgA3AGYAYgA4ADIAIgAsACIAbQBfAEMAcgBjACIAOgAyADIAMQAxADUAOQAzADAAMAAxACwAIgBtAF8AVABpAG0AZQBvAHUAdAAiADoAMAAsACIAbQBfAEMAaAB1AG4AawBlAGQAVAByAGEAbgBzAGYAZQByACIAOgBmAGEAbABzAGUALAAiAG0AXwBSAGUAZABpAHIAZQBjAHQATABpAG0AaQB0ACIAOgAtADEALAAiAG0AXwBSAGUAdAByAHkAQwBvAHUAbgB0ACIAOgAwACwAIgBtAF8AQgB1AG4AZABsAGUATgBhAG0AZQAiADoAIgA1AGQANgA0ADEANgBlADgAYwAwADkANgA0ADEAZQBkADMANgA1ADkANABmADAAZAA0AGUAZgBlADIAMQBkADEAIgAsACIAbQBfAEEAcwBzAGUAdABMAG8AYQBkAE0AbwBkAGUAIgA6ADAALAAiAG0AXwBCAHUAbgBkAGwAZQBTAGkAegBlACIAOgA2ADMAMgAwADEALAAiAG0AXwBVAHMAZQBDAHIAYwBGAG8AcgBDAGEAYwBoAGUAZABCAHUAbgBkAGwAZQBzACIAOgB0AHIAdQBlACwAIgBtAF8AVQBzAGUAVQBXAFIARgBvAHIATABvAGMAYQBsAEIAdQBuAGQAbABlAHMAIgA6AGYAYQBsAHMAZQAsACIAbQBfAEMAbABlAGEAcgBPAHQAaABlAHIAQwBhAGMAaABlAGQAVgBlAHIAcwBpAG8AbgBzAFcAaABlAG4ATABvAGEAZABlAGQAIgA6AGYAYQBsAHMAZQB9AA==",
    "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"
        },
        {
            "m_AssemblyName": "Unity.RenderPipelines.Core.Runtime, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null",
            "m_ClassName": "UnityEngine.Rendering.UI.DebugUIPrefabBundle"
        },
        {
            "m_AssemblyName": "UnityEngine.CoreModule, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null",
            "m_ClassName": "UnityEngine.RectOffset"
        },
        {
            "m_AssemblyName": "UnityEngine.UI, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
            "m_ClassName": "UnityEngine.UI.MaskableGraphic+CullStateChangedEvent"
        },
        {
            "m_AssemblyName": "UnityEngine.CoreModule, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null",
            "m_ClassName": "UnityEngine.Events.PersistentCallGroup"
        },
        {
            "m_AssemblyName": "UnityEngine.CoreModule, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null",
            "m_ClassName": "UnityEngine.Shader"
        },
        {
            "m_AssemblyName": "Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null",
            "m_ClassName": "UnityEngine.ResourceManagement.ResourceProviders.SceneInstance"
        }
    ],
    "m_InternalIdPrefixes": []
}

実際のプロジェクトではAddressablesで管理されているアセットの情報がさらに追加されます。

ここでJsonを見て分かる注意点が3つあります。

  1. InternalIdは直接見ることができる
    • m_InternalIds
    • 設定でInternalIdに何を使うのか制御できるので、どんなアセットを使用しているのか知られたくない場合にはFullPathではなくGUIDを使うのがおすすめです
    • ただしSceneはFullPathにしないとSceneのロードができなくなることに注意が必要です
  2. Base64エンコードされていて直接見ることができないデータもある
    • m_KeyDataStringm_BucketDataStringm_EntryDataStringm_ExtraDataString
    • ちゃんとBase64デコードすれば見ることもできます
  3. InternalIdに StandaloneOSX などのプラットフォーム固有の情報を含んでいる
    • LocalのAssetBundleのInternalId(Path)
    • このためCatalogはAssetBundleと同様にビルドするプラットフォーム別にファイルが必要になることが分かります

インターフェース

Base64エンコードされていることにより具体的な内部の情報が分かりづらいので、IResourceLocator の定義を確認してみましょう。

docs.unity3d.com

namespace UnityEngine.AddressableAssets.ResourceLocators
{
    /// <summary>
    /// Interface used by the Addressables system to find the 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="type">The resource type.</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);
    }
}

Keys はそのCatalogに記載されているAddressなどの一覧です。

そのKeyを Locate(...) で指定することで、指定したKeyが含まれている場合は IResourceLocatioin (のリスト)を返してくれます。

次に IResourceLocation の定義を見てみましょう。

docs.unity3d.com

namespace UnityEngine.ResourceManagement.ResourceLocations
{
    /// <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; }

        /// <summary>
        /// The hash of this location combined with the specified type.
        /// </summary>
        /// <param name="resultType">The type of the result.</param>
        /// <returns>The combined hash of the location and the type.</returns>
        int Hash(Type resultType);

        /// <summary>
        /// The precomputed hash code of the dependencies.
        /// </summary>
        int DependencyHashCode { get; }

        /// <summary>
        /// Gets the dependencies to other IResourceLocations
        /// </summary>
        /// <value>The dependencies.</value>
        bool HasDependencies { get; }

        /// <summary>
        /// Gets any data object associated with this locations
        /// </summary>
        /// <value>The object.</value>
        object Data { get; }

        /// <summary>
        /// Primary address for this location.
        /// </summary>
        string PrimaryKey { get; }

        /// <summary>
        /// The type of the resource for th location.
        /// </summary>
        Type ResourceType { get; }
    }
}

InternalId はJsonでも見える形で含まれていましたね。

Dependencies があることから、アセットやAssetBundleの依存関係の情報も持っていることが分かります。

Data は前回記事でも説明したようにCRCなどの情報を含む AssetBundleRequestOptions という型にキャストして利用できます。

docs.unity3d.com

PrimaryKey はAddressに相当するものになります。*1

重要な情報のみ解説しましたが、これらの情報がBase64エンコードされてCatalogに含まれていることになっています。

以上がCatalogの中身になります。

CRCに関する補足

CRC(Cyclic Redundancy Check)に関して少し補足説明します。

CRC(Cyclic Redundancy Check)はAssetBundleの内容の差異を検知するためのHash値のような役割をする情報です。

AssetBundleをロードする際にオプションで指定することができ、期待されるCRCの値と実際にロードしたAssetBundleから計算したCRCの値を比較して、そのAssetBundleが期待している内容であるかを判別できます。

docs.unity3d.com

これはダウンロードしたデータの破損や古くなったキャッシュの検出などに利用できます。*2

特にRemoteのコンテンツをストレージにキャッシュしたり、その中身を更新したりする際にはCRCを利用することになるため、期待されるCRCの情報がCatalogに載っていることも知っておくべき事項になります。

Catalogのロード方法

Catalogには大きく分けて3つのロード方法があります。

  1. Local
  2. Remote
  3. API(URL)

1. Local

アプリケーション本体をビルドすると、StreamingAssets/aa のフォルダ内に

  • settings.json
  • catalog.json(圧縮設定している場合は catalog.bundle

の2つのファイルが作成されます。

アプリケーション起動中のAddressablesの初期化時(明示的に初期化した時 or 初めてAddressablesにアクセスする時)に、まず settings.json をロードします。

中にはLocalのCatalogのPath、次で説明するRemoteのCatalogのPath or URLが書かれていて、それを元に catalog.json をロードします。

このLocalのCatalogにはGroupの設定で Include in Build = true になっているものがアプリケーションのビルド時の状態で全て含まれます。

Addressablesの内部的には AddressablesMainContentCatalog というLocatorIdの IResourceLocator としてロードされます。

当然ですがこのLocalのCatalogの中身を更新するためにはアプリケーション本体をビルドし直す必要があります。

2. Remote

AddressablesAssetSettingsContent Update の項目で Build Remote Catalog をtrueに設定すると、Addressablesのビルド時にCatalogを出力するようになります。

そのすぐ下の Build & Load Paths の設定で、出力先のPathとロード先のPathを指定できます。

Load Path はLocalの settings.json にも記載されて、Addressablesの初期化時に特別な操作をしなくても自動でロードをしてくれます。*3

ただしPathを固定するのが難しい場合には、次で説明するようにAPIを叩いて手動でロードしてあげる必要があります。

3. API(URL)

AddressablesにはRuntimeで手動でCatalogをロード・アンロードするAPIが用意されています。

docs.unity3d.com

PathにはURLを指定しても問題ありません。

docs.unity3d.com

現在ロードしているCatalogの一覧も取得できます。

docs.unity3d.com

自分でロードしたCatalogをきちんとアンロードするためには IResourceLocator のインスタンスを保持しておく必要があります。

それは LoadContentCatalogAsync の戻り値で取得できるのですが、メソッドのパラメータの autoReleaseHandle = true に指定していると null になってしまうため、アンロードも細かく管理する場合には autoReleaseHandle = false を指定するようにしましょう。

docs.unity3d.com

Catalogの運用

RemoteからCatalogを指定して使用したい場合には、先ほど紹介した Build Remote Catalog の設定が必要になります。

それを含めた全般的な話は公式のドキュメントにも大まかな説明はありますので、一読しておくことをお勧めします。

docs.unity3d.com

それ以外にCatalogを運用する上で知っておくと良い話も紹介します。

Catalogの更新

アプリケーション外からコンテンツを追加して使用したい場合には、AssetBundleをサーバーなどに配置するだけではなく、Addressablesのビルド時に同時に生成されるCatalogもサーバーなどに配置してあげる必要があります。

そのためAssetBundleを追加したり更新する際にはCatalogも更新しなければなりません。

AssetBundleの中身を更新したい場合にも、CRCのチェックをする場合にはCatalogにはCRCの情報が記載されているため、Catalogも更新してあげる必要があることには注意が必要です。

Catalogの分割

CatalogのAPIの説明からも分かるかもしれませんが、CatalogはLocalのものだけではなく任意に追加して複数のCatalogをロードして使用することができます。

前回の記事でも少し触れましたが、Addressablesでアセットをロードするときには IResourceLocator の列挙を foreach で回して、初めに取得できた IResourceLocation を使用してロードするということがソースコードから分かりました。

このため、複数のCatalogを使用してもどれかのCatalogから正常にアセットをロードできる状態であれさえすれば問題ないことが分かります。

この性質を考慮すると、少し凝った形のアセット管理の方法にもAddressablesを適用できます。

そもそものテーマであるRemoteでのアセット管理をする上では、アプリケーションのリリース後にAssetBundleを更新したり追加して使用したいという話でした。

そのようなケースではアプリケーション本体に含まれているLocalのCatalogには更新後のAssetBundleや追加したいAssetBundleの情報は当然持っていません。

そのため、それら更新したいAssetBundle、追加したいAssetBundleの情報を持っているCatalogをRuntimeで外からロードする必要があります。

従って最低でも2種類のCatalogを使用することになります。

  • LocalのCatalog
    • アプリケーション本体に含まれている、アプリケーションビルド時に生成されるCatalog
  • RemoteのCatalog
    • アプリケーションの外からロードして使用する、アプリケーションのビルド後に追加して使用したいAssetBundleの情報を含んでいるCatalog

もちろん数の制限もないので、もっとたくさんのRemoteのCatalogを使い分けることもできます。

例として、Remoteから3つのSceneのAssetBundleを追加したい場合には、以下のようなCatalogを作成して運用することも可能です。

  • LocalのCatalog
    • 追加したいScene以外の情報を持っている
  • SceneAのCatalog
    • LocalのCatalogの情報に加えて、SceneAのAssetBundleの情報を持っている
  • SceneBのCatalog
    • LocalのCatalogの情報に加えて、SceneBのAssetBundleの情報を持っている
  • SceneCのCatalog
    • LocalのCatalogの情報に加えて、SceneCのAssetBundleの情報を持っている

ただしこのようなことをするためにはアセットの運用面で下記などの注意が必要です。

  • LocalのCatalogの情報の部分はアプリケーションのリリース時の内容とは変わっている可能性があるため、LocalのAssetBundleのCRCのチェックをすると失敗する場合がある
  • SceneA~Cの間に依存関係を持たせることができないので、多重参照しているアセットは複製して各AssetBundleに含める必要がある
  • Addressablesのビルド時にGroupの設定で Include in Build = true にしているものがCatalogに含まれるため、1つのUnityProjectでSceneを作成している場合にはビルド時に設定を切り替えてあげる必要がある
  • Catalogのアンロードのタイミングでは使用しているアセットもちゃんとアンロードしてあげる

もちろんScene単位でなくても任意の単位でcatalogを分割して運用することも可能です。

公式のドキュメントを読む感じでは、大規模なプロジェクトで大量のアセットを管理する場合を想定してこのような運用ができるようにしているようです。(どのページに書いてあったか忘れてしまったのですが...)

UnityProjectの分割

やや強引な言い方をするなら、AddressablesはCatalogから正しくアセットが読み込める状態を作ってあげればどうにかなるものです。

Catalogが分割できるので、UnityProjectを分割する運用、例えばアプリケーション本体を開発するUnityProjectと、Remoteから追加したいアセットを作成するUnityProjectを分けて運用することも可能です。

もちろん注意事項はあります。

  • C#のScriptをComponentとして使用したい場合には、アセット作成側のUnityProjectにも同じScriptを含んでおく必要がある
    • そもそもScriptが絡むと運用が大変なのでおすすめはしないのですが...
    • 厳密に言うとAssembly名、NameSpaceを含むClass名、SerilaizedObjectの構造が一致していればソースコードに細かい差分があっても問題ないです
    • UPMのPackageとして共有できるのが理想です
  • UnityProjectを跨いで共有したいアセットがある場合には、それぞれのProjectで全く同じ設定のGroupを用意する必要がある
    • 設定内容にもよるかもしれませんが、GroupのGUIDも強制的に揃えておく必要がある場合もあります
    • 特に Universal Render Pipeline/Lit など標準的なShaderなどは使いまわすケースが多いので気を付ける必要があります
  • 共有するGroupの設定の Internal Bundle Id Mode やAddressablesAssetSettingsの Shader Bundle Naming Prefix などでProjectIdを使用することはできません
    • ProjectIdはおそらくUnityProject固有のものなので、これを使用してしまうと差分が発生してしまいます

一応公式のドキュメントにも少し説明があります。

docs.unity3d.com

終わりに

以上、Catalogに関する詳しい紹介をしました。

  • Catalogはどういった役割をするものなのか
  • Catalogが実際に持っている情報は何なのか
  • Catalogはどこからロードして使用するのか
  • Catalogがどのように運用できるか

こちらの情報を参考にしつつ、実際にご自身で色々触ってみるとより理解が深まると思います。

公式のドキュメントにもあまり体系的な説明がないため、Catalogの実態が掴みづらいかと思いますが、分かってしまえばそれほど複雑なものでもありませんし、意外と緩い部分もあって融通が効く部分もあります。

実際のプロジェクトでは実現したい運用に合わせて、開発体制を整えたり、細かい機能を調整したり、CIを組んだり、デリバリーの体制を整えたりと細かい調整をしていくことになるかと思います。

Remoteでのアセット管理はニーズがある一方で導入ハードルがやや高いと思いますが、前回と今回の記事の内容で基本的な内容は抑えられているかと思いますので、チャレンジしたい方に参考になれば幸いです。

記述内容には気を配ってはいますが、もし本記事の内容に間違っている記述や不適切な記述がある場合にはコメントやTwitterDMなどでお知らせいただけると幸いです。

今後は社内の開発のために作成したデバッグに便利なCatalogBrowser的なEditor拡張や、細かい運用面で苦労した話のTips集なども公開できればと思いますが、余力があればということで。

*1:AssetBundleの場合はAddressではなくAssetBundle名だったりしますが

*2:データの改竄も同じ理屈で検出できそうに思えますが、CRCの計算ロジックはその桁数からしても暗号学的にセキュアなものではなさそうなため、そこまで信頼しない方が良いかもしれません

*3:CatalogのUpdateの操作が必要だったかもしれません