こんにちは、エンジニアリングマネージャーの渡辺(@mochi_neko_7)です。
皆さんはUnityのAddressables(Addressable Asset System)を使っていますか?
自分はベータ版の頃に不安定だったという印象でこれまであまり触ってこなかったのですが、いざ触っているとかなり便利で現在は正式版になっているため不安定なところもそれほどありませんでした。
今後は積極的に使っていきたいところですが、Addressablesを使ったRemote(アプリケーション外)でのコンテンツ管理に関する情報が公式のドキュメント以外(英語しかない)にまとまったものがあまり見つからなかったため、取っ掛かりで少し苦労をしました。
ですので本記事とそれ以降の記事で、AddressablesでのRemoteコンテンツ管理の運用に関するノウハウや注意点などを紹介していきたいと思います。
Addressables自体の紹介やAPIの触り方に関しては弊社テックブログのこちらの記事
をはじめ他にも検索するといくつも入門の記事が出てくると思います。
まだAddressablesを触ったことないよ、という方はまずはこちらを参照してAddressablesの導入をしてみて、実際にアセットのロード・アンロードのAPIを触ってみてください。
ここでは入門の内容から一歩踏み込んで、Remoteでのコンテンツ管理、つまりアプリケーションのリリース後にコンテンツの更新や追加ができることを目指して、実装すべき内容、Addressablesの内部使用や運用上注意すべき点などを紹介します。
公式のドキュメントを読んでもRemoteでのコンテンツ管理の全体像がいまいち掴めない方の参考になれば幸いです。
今回使用しているバージョンは以下になります。
- Unity 2021.3.0f1
- Addressables 1.19.19
- 公式ドキュメントは 1.20 の方が情報が多いのでリンクは1.20を使っています
前提
まずAddressablesを採用することで得られるメリットを簡単におさらいします。
アセットのメモリ使用量を最適化することができる
Built-inのアセットはアプリケーション起動時に全てメモリ上に展開されますが、Addressablesでは使用しているアセットのみメモリに載せるような管理をすることができる。
アセットのロードを非同期的に行うことができる
Addressablesはストレージやネットワークからアセット(正確にはAssetBundle)をロードしますが、そのAPIはawait
で非同期*1に呼び出すことができます*2。
アセットをロードするときにそのアセットがLocalにあるかRemoteにあるかを意識しなくてよい
PlayModeScriptという機能を使用することで、開発中はEditor上で直接ロードし*3素早くデバッグしながら、本番ではLocalやRemoteにあるビルド済みのAssetBundleからロードすることができ、その使い分けをコードを変更することなく実現できます。
アプリケーション本体のデータ量を削減することができる
一部のアセットをRemoteから取得するようにすると、その分アプリケーション本体に同梱するアセットの量を減らすことができ、アプリのデータサイズを小さくすることができます。
アプリケーションのバージョンを上げずに外部からのコンテンツの追加や更新をすることができる
Remoteから取得するようにしたアセットはアプリケーションのリリース後に変更できる設定にすることができます。
それによりアプリケーションの機能開発とコンテンツの開発運用を切り分けた柔軟な運用をすることができます。
こちらのドキュメントには重要なことがたくさん書かれているため、機械翻訳でも良いので一読することをお勧めします。
ちなみに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)とそれに紐づくアセットの対応関係の情報を持っているものです。
ちなみにContentCatalogはメモリ上ではこの IResourceLocator
として扱われますので、Catalogを知っている方はどんな情報を持っているかイメージできるかと思います。*4
3の Locate(...)
の戻り値は IResourceLocation
というもので、アセットをロードするために必要な情報を持っているものです。
4の IResourceProvider
は実際にアセットをロードする機能を持っているものです。
アセットのロード処理に登場する主な登場人物は、
- key(Address)
- IResourceLocator
- IResourceLocation
- IResourceProvider
となりますので、これらの役割を理解しておくと良いと思います。
ResourceProviderのカスタマイズ
Remoteで管理しているコンテンツを実行時に取得して利用するということは、アプリケーションの実行時にネットワーク経由で特定のサーバーで管理されているAssetBundle(とCatalog)をダウンロードして利用するということになります。
Unityが提供しているクラウドサービスを利用することもできるのですが、自社のサーバーで管理したいというケースもあり、後者は自分でダウンロード処理をカスタマイズする必要があります。
ここでは後者の場合で、特にAssetBundleのダウンロード処理をカスタマイズする方法を紹介します。
前節で紹介したので予想ができるかもしれませんが、AssetBundleのダウンロード処理のカスタマイズは先ほど紹介した IResourceProvider
を自作することで可能になります。
ベースクラスの ResourceProviderBase
が用意されているので、これを継承して実装することができます。
基本的な操作はロード時に IResourceProvider.Provide(...)
、アンロード時に IResourceProvider.Provide(...)
が呼ばれるイメージです。
Provide(...)
メソッドの引数の ProvideHandle
には ProvideHandle.Location
というPropertyで IResourceLocation
を持っているため、ここに書かれている情報を元にAssetBundleのロード処理をカスタマイズすることができます。
特に IResourceLocation.InternalId
というPropertyはURLを書き込んだり、WebAPIのアクセスに必要なパラメータを持たせたりある程度柔軟に使って良いとされています。
例えばGroupの設定に独自のSchemaを用意してURLやパラメータを持っておき、カスタマイズしたBuild Scriptでビルド時にInternalIdを書き換えて使用することも可能です。
もう一つ IResourceLocation.Data
というobject型の一見どう使っていいか分からないPropertyもありますが、AssetBundleRequestOptions
というTypeにCastすることができ、HashやCRCなどの情報を取得できるので意外と重要です。
Remoteに置きたいGroupの設定を変更する
カスタマイズしたResourceProviderはAddressables Groups WindowのGroupの設定で使用するProviderに指定してあげる必要があります。
その設定も含めて、Groupの設定でRemoteに置く場合に気を付けるべき項目を紹介します。
Build & Load Paths
- ここは
Remote
に設定する必要があります
- ここは
Asset Bundle Compression
- Remoteから取得するGroupは圧縮率の大きい
LZMA
が推奨されています - docs.unity3d.com
- Remoteから取得するGroupは圧縮率の大きい
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をアプリのリリース後に更新したり追加したりするためにはこれだけでは不十分です。
アセットのロードフローを見返してみると、IResourceProvider
は IResourceLocation
で指定されているもので、それは IResourceLocator
(ContentCatalog)から取得できるものでした。
つまりContentCatalogに更新した、あるいは追加したAssetBundleの情報が含まれていないと原理的にロードできないことが分かります。
ですのでContentCatalogの運用もとても重要なのですが、これもあまりドキュメントの情報量がなく仕様が掴みづらいものですので、また次回の記事で詳しく紹介したいと思います。
プロジェクトの性質によって変わる部分は省略してしまったので、適宜設定や実装を補完してください。
本記事でAddressablesのアセットのロードフローの全体像、RemoteからAssetBundleを取得する方法のとっかかりを掴んでいただけると幸いです。