UnityのAddressablesのRemoteコンテンツ運用 - ResourceProviderのカスタマイズ

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

皆さんはUnityのAddressables(Addressable Asset System)を使っていますか?

自分はベータ版の頃に不安定だったという印象でこれまであまり触ってこなかったのですが、いざ触っているとかなり便利で現在は正式版になっているため不安定なところもそれほどありませんでした。

今後は積極的に使っていきたいところですが、Addressablesを使ったRemote(アプリケーション外)でのコンテンツ管理に関する情報が公式のドキュメント以外(英語しかない)にまとまったものがあまり見つからなかったため、取っ掛かりで少し苦労をしました。

docs.unity3d.com

ですので本記事とそれ以降の記事で、AddressablesでのRemoteコンテンツ管理の運用に関するノウハウや注意点などを紹介していきたいと思います。

Addressables自体の紹介やAPIの触り方に関しては弊社テックブログのこちらの記事

synamon.hatenablog.com

をはじめ他にも検索するといくつも入門の記事が出てくると思います。

まだAddressablesを触ったことないよ、という方はまずはこちらを参照してAddressablesの導入をしてみて、実際にアセットのロード・アンロードのAPIを触ってみてください。

ここでは入門の内容から一歩踏み込んで、Remoteでのコンテンツ管理、つまりアプリケーションのリリース後にコンテンツの更新や追加ができることを目指して、実装すべき内容、Addressablesの内部使用や運用上注意すべき点などを紹介します。

公式のドキュメントを読んでもRemoteでのコンテンツ管理の全体像がいまいち掴めない方の参考になれば幸いです。

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

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

前提

まずAddressablesを採用することで得られるメリットを簡単におさらいします。

アセットのメモリ使用量を最適化することができる

Built-inのアセットはアプリケーション起動時に全てメモリ上に展開されますが、Addressablesでは使用しているアセットのみメモリに載せるような管理をすることができる。

blog.unity.com

docs.unity3d.com

アセットのロードを非同期的に行うことができる

Addressablesはストレージやネットワークからアセット(正確にはAssetBundle)をロードしますが、そのAPIはawaitで非同期*1に呼び出すことができます*2

docs.unity3d.com

アセットをロードするときにそのアセットがLocalにあるかRemoteにあるかを意識しなくてよい

PlayModeScriptという機能を使用することで、開発中はEditor上で直接ロードし*3素早くデバッグしながら、本番ではLocalやRemoteにあるビルド済みのAssetBundleからロードすることができ、その使い分けをコードを変更することなく実現できます。

tsubakit1.hateblo.jp

アプリケーション本体のデータ量を削減することができる

一部のアセットをRemoteから取得するようにすると、その分アプリケーション本体に同梱するアセットの量を減らすことができ、アプリのデータサイズを小さくすることができます。

アプリケーションのバージョンを上げずに外部からのコンテンツの追加や更新をすることができる

Remoteから取得するようにしたアセットはアプリケーションのリリース後に変更できる設定にすることができます。

それによりアプリケーションの機能開発とコンテンツの開発運用を切り分けた柔軟な運用をすることができます。

docs.unity3d.com

こちらのドキュメントには重要なことがたくさん書かれているため、機械翻訳でも良いので一読することをお勧めします。

ちなみにAddressablesではLocalのアセット管理に限定したとしても十分有用なので、ほとんどのプロジェクトで導入することを個人的にはお勧めできます。

アセットのロードフロー

Remoteのコンテンツのダウンロード処理のカスタマイズ方法を紹介する前に、Addressableにおけるアセットのロードフローの全体像を理解しておいた方がイメージが掴みやすいと思いますので簡単に紹介します。

おそらく最も標準的なAPIの Addressables.LoadAssetAsync(...) を起点として、ソースコードを追ってみましょう。

public static AsyncOperationHandle<TObject> LoadAssetAsync<TObject>(object key)
{
    return m_Addressables.LoadAssetAsync<TObject>(key);
}

すると処理自体は内部の AddressablesImpl に移譲されているので、そちらを見てみます。

public AsyncOperationHandle<TObject> LoadAssetAsync<TObject>(object key)
{
    QueueEditorUpdateIfNeeded();
    if (ShouldChainRequest)
        return TrackHandle(LoadAssetWithChain<TObject>(ChainOperation, key));

    key = EvaluateKey(key);

    IList<IResourceLocation> locs;
    var t = typeof(TObject);
    if (t.IsArray)
        t = t.GetElementType();
    else if (t.IsGenericType && typeof(IList<>) == t.GetGenericTypeDefinition())
        t = t.GetGenericArguments()[0];
    foreach (var locatorInfo in m_ResourceLocators)
    {
        var locator = locatorInfo.Locator;
        if (locator.Locate(key, t, out locs))
        {
            foreach (var loc in locs)
            {
                var provider = ResourceManager.GetResourceProvider(typeof(TObject), loc);
                if (provider != null)
                    return TrackHandle(ResourceManager.ProvideResource<TObject>(loc));
            }
        }
    }
    return ResourceManager.CreateCompletedOperationWithException<TObject>(default(TObject), new InvalidKeyException(key, t, this));
}

やや追いづらいところもあるかもしれませんが、基本的な処理は以下になります。

1: 引数の key が何か特別なものなら変換をし、

key = EvaluateKey(key);

2: IResourceLocator の列挙をforeachで回して、

foreach (var locatorInfo in m_ResourceLocators)

3: keyを使ってアセットが特定できたら、

if (locator.Locate(key, t, out locs))

4: IResourceLocationで指定されている IResourceProviderを取得して AyncOperationHandle を生成する。

var provider = ResourceManager.GetResourceProvider(typeof(TObject), loc);
if (provider != null)
  return TrackHandle(ResourceManager.ProvideResource<TObject>(loc));

2の IResourceLocator というのはkey(通常はAddress)とそれに紐づくアセットの対応関係の情報を持っているものです。

docs.unity3d.com

ちなみにContentCatalogはメモリ上ではこの IResourceLocator として扱われますので、Catalogを知っている方はどんな情報を持っているかイメージできるかと思います。*4

3の Locate(...) の戻り値は IResourceLocation というもので、アセットをロードするために必要な情報を持っているものです。

docs.unity3d.com

4の IResourceProvider は実際にアセットをロードする機能を持っているものです。

docs.unity3d.com

アセットのロード処理に登場する主な登場人物は、

  • key(Address)
  • IResourceLocator
  • IResourceLocation
  • IResourceProvider

となりますので、これらの役割を理解しておくと良いと思います。

ResourceProviderのカスタマイズ

Remoteで管理しているコンテンツを実行時に取得して利用するということは、アプリケーションの実行時にネットワーク経由で特定のサーバーで管理されているAssetBundle(とCatalog)をダウンロードして利用するということになります。

Unityが提供しているクラウドサービスを利用することもできるのですが、自社のサーバーで管理したいというケースもあり、後者は自分でダウンロード処理をカスタマイズする必要があります。

ここでは後者の場合で、特にAssetBundleのダウンロード処理をカスタマイズする方法を紹介します。

前節で紹介したので予想ができるかもしれませんが、AssetBundleのダウンロード処理のカスタマイズは先ほど紹介した IResourceProvider を自作することで可能になります。

ベースクラスの ResourceProviderBase が用意されているので、これを継承して実装することができます。

docs.unity3d.com

基本的な操作はロード時に IResourceProvider.Provide(...) 、アンロード時に IResourceProvider.Provide(...) が呼ばれるイメージです。

Provide(...) メソッドの引数の ProvideHandle には ProvideHandle.Location というPropertyで IResourceLocation を持っているため、ここに書かれている情報を元にAssetBundleのロード処理をカスタマイズすることができます。

特に IResourceLocation.InternalId というPropertyはURLを書き込んだり、WebAPIのアクセスに必要なパラメータを持たせたりある程度柔軟に使って良いとされています。

docs.unity3d.com

例えばGroupの設定に独自のSchemaを用意してURLやパラメータを持っておき、カスタマイズしたBuild Scriptでビルド時にInternalIdを書き換えて使用することも可能です。

もう一つ IResourceLocation.Data というobject型の一見どう使っていいか分からないPropertyもありますが、AssetBundleRequestOptions というTypeにCastすることができ、HashやCRCなどの情報を取得できるので意外と重要です。

docs.unity3d.com

Remoteに置きたいGroupの設定を変更する

カスタマイズしたResourceProviderはAddressables Groups WindowのGroupの設定で使用するProviderに指定してあげる必要があります。

その設定も含めて、Groupの設定でRemoteに置く場合に気を付けるべき項目を紹介します。

  • Build & Load Paths
    • ここは Remote に設定する必要があります
  • Asset Bundle Compression
    • Remoteから取得するGroupは圧縮率の大きい LZMA が推奨されています
    • docs.unity3d.com
  • Internal Asset Naming Mode
    • 含まれているアセットの InternalId の持ち方を設定できます
    • 基本的には任意の設定で良いのですが、Sceneを含む場合は Full Path に設定しないとSceneのロードができなくなるので注意が必要です*5
    • docs.unity3d.com
  • Bundle Mode
    • Pack Together の場合でもSceneを含むときはSceneのAssetBundleとそれ以外のアセットを含むAssetBundleの2つに分割されることに注意してください
    • docs.unity3d.com
  • Bundle Naming Mode
    • ビルドしたAssetBundleのファイル名の指定方法を設定できます
    • 名前を隠したい場合はHashに設定することも有効ですが、ビルドした後にサーバーにアップロードしたり管理する際にはファイル名が残っているほうが管理しやすいかもしれません
    • docs.unity3d.com
  • Asset Provider
    • 含まれる個別のアセットのロード方法をカスタマイズするときにはここでProviderを指定します
  • Asset Bundle Provider
    • 前節で作成したAssetBundleのロード方法をカスタマイズしたProviderはここで指定します

おわりに

ResourceProviderをカスタマイズをすることでAssetBundleを任意の場所からロードして使用することができるようになります。

その内部にはCRCのチェック、ストレージへのキャッシュの生成、データの暗号化などプロジェクトの性質に合わせてさらにカスタマイズを加えることもできます。

ただ、AssetBundleをアプリのリリース後に更新したり追加したりするためにはこれだけでは不十分です。

アセットのロードフローを見返してみると、IResourceProviderIResourceLocation で指定されているもので、それは IResourceLocator (ContentCatalog)から取得できるものでした。

つまりContentCatalogに更新した、あるいは追加したAssetBundleの情報が含まれていないと原理的にロードできないことが分かります。

ですのでContentCatalogの運用もとても重要なのですが、これもあまりドキュメントの情報量がなく仕様が掴みづらいものですので、また次回の記事で詳しく紹介したいと思います。

プロジェクトの性質によって変わる部分は省略してしまったので、適宜設定や実装を補完してください。

本記事でAddressablesのアセットのロードフローの全体像、RemoteからAssetBundleを取得する方法のとっかかりを掴んでいただけると幸いです。

*1:AyncOperationHandle.WaitForCompletion()で同期的に呼ぶことも可能です

*2:CancellationTokenを渡す口を生やしてくれるとなお良いのですが...

*3:正確にはAssetDatabase経由で直接ロードしたり、Groupの構造をエミュレートしたりしてくれます

*4:Catalogの詳しい説明をするときに紹介しますが、foreachで回しているだけ、という仕様は意外と重要なポイントになります。

*5:SceneProviderの実装を追うと、InternalIdをSceneのPathに使用していることが分かると思います