ホロライブアプリでのAddressable AssetSystem活用事例
WAH! It’s TAKOTIME! 🐙
こんにちは。カバー株式会社CTO室のKです。
今回は、ホロライブアプリにAddressable Asset Systemを導入して運用してきた事例を紹介します。Addressable Asset System(以下、AAS)はUnity公式のアセット管理ライブラリです。ホロライブアプリではキャラクターの3Dモデル、小道具、ステージなどのアセットをビルドしてサーバーに配置して使用するためにAASを活用しています。導入から3年ほどが経ち、運用の中で気づいた点もあったので、それらを一事例として紹介していきます。Unityのアセット管理について検討されている方の参考になれば幸いです。
Addressable Asset Systemとは
Addressable Asset Systemとは、Unity公式のアセット管理ライブラリです。AASの仕組みなどについては、既に公式のドキュメントや有志の方の記事が存在するため、ここではAASの利点について簡単に紹介するのみに留めます。
利点1:AssetBundleを簡単に扱える
UnityにはもともとAssetBundleというアセット管理システムがあり、かつてUnityで開発を行う際はそれを使っていました。
AssetBundle自体はアセットをパッキングし、アセット同士の依存関係を計算するというシンプルな機能しか持っていません。なので、素のAssetBundleのみでゲームの開発・運用を行うことは難しく、大体の場合はAssetBundleの仕組みをサポートするライブラリなどを用意する必要がありました。そのようなライブラリは有志が作っているオープンソースのものを使用するか、自前でライブラリのプログラムを書く必要がありました。
対して、AASはパッキングされたアセットを、カタログに登録するだけで生成してくれます。また、ロードに関しても
Addressables.LoadAssetAsync()
の関数を呼ぶだけで簡単にロードすることができます。ロード時のアセット間依存解決なども勝手にやってくれるので、利用側からは特に気にする必要がありません。
利点2:アセットのロード先をローカルとリモートで切り替えられる
また、AASの便利な機能として、Unityエディタ上での実行時に、アセットのロード先を
プロジェクト内に配置されたローカルのアセットファイル
サーバーに配置されたバンドル(本番環境と同等)
から簡単に切り替えることができます。
この機能によって、アセットをビルドすることなく、本番と同様のプログラムでアセットを動作させることができます。リリース前の動作確認などに大いに活用できる便利な機能です。
ホロライブアプリでの導入事例
ホロライブアプリにおいて、以前までは以下の構成でアセットを管理していました。
アプリ本体プロジェクト
アセットの管理方法:Addressable Asset System
配置されたアセット:ステージ、小道具、ローカライゼーションのテキスト、またアセットのサムネイル画像など
キャラクター3Dモデル用プロジェクト
アセットの管理方法:Addressable Asset System
配置されたアセット:キャラクターの3Dモデル
このように、プロジェクトを二つに分けて管理していました。キャラクターの3Dモデルだけは別のUnityプロジェクトに格納し、それ以外のアセットはアプリ本体のプロジェクトに格納しているという構成です。
なぜ分けているかというと、キャラクターの3Dモデルの量が多く、膨大なデータサイズであるため、アプリ本体とは分けて管理したかったからです。キャラモデルをアプリ本体に入れてしまうと、モデルに更新が入るたびにUnityの長いインポートが走り、開発効率が落ちてしまいます。具体的には、モデルが含まれたプロジェクトを一からインポートすると、最大10時間以上かかってしまうという状態でした。それを回避するためにプロジェクトを分けることにしました。
Addressableの仕様では、一つのUnityプロジェクトが同時に持てるカタログは一つだけなので、プロジェクトごと分ける必要がありました。一方で、カタログの読み込みについては実行時に複数のカタログを読み込んで共存させることができるため、ビルドはプロジェクトを分けて行い、アプリの実行時に両方のプロジェクトのカタログを読み込んで全てのアセットを使えるようにする、という方法で運用していました。
この構成を図で示すと以下のようになります。
複数のカタログを読み込む方法
上記で「カタログの読み込みについては実行時に複数のカタログを読み込んで共存させる」と書きましたが、実際のプログラムについて少し解説しておきます。
といっても特別手のかかる対応はなく、2つめのカタログを読み込む関数を呼ぶだけで実行環境にカタログが登録され、普通に Addressables.LoadAssetAsync() 系の関数にアドレスを渡すと普通にロードできるようになります。
2つめ以降のカタログを読み込む関数はこちら↓
//url:catalog.jsonが配置されたURL
await Addressables.LoadContentCatalogAsync(url);
こうして最初にカタログを読み込んでおくことで、その実行時はカタログに記載されたアセットをアドレスによってロードすることができるようになります。
別のプロジェクトから利用されるカタログをビルドする際の注意点としては、「AddressableAssetSettings.asset」の「Build Remote Catalog」をONにすることです。これをしなければそもそもcatalog.jsonが生成されません。
キャラクター3Dモデルの運用変更
このように、しばらくキャラクターのモデルのみを別プロジェクトに分けて運用していたのですが、プロジェクトが分かれていることによる弊害が目立ってきました。それは、アプリ本体でキャラクターモデルのプレハブを事前にテストすることができないということです。
Addressable経由でビルド前のアセットをロードする仕組みは、同じプロジェクトにアセットが配置されていないと利用できません(当然ですが)。
この点が運用上不便だったので、キャラクターモデルをアプリ本体のプロジェクトで管理する方法を探ることにしました。 前述の通り、Addressableでは一つのプロジェクトに一つのカタログしか持てないため、Addressable以外の方法を採用する必要がありました。
その候補としてはAssetBundleとScriptable Build Pipelineがありました。
Scriptable Build PipelineはAddressable Asset Systemを構成しているモジュールの一つで、それ単体で利用することも可能なものになっています。AASでは、運用しているプロジェクトの要件を満たせない場合などに、アセットのパッキングとビルドの部分を自前でカスタマイズして使用することができます。
AssetBundleで要件を満たすことはできるのですが、今はUnity公式のアセット管理がAsset BunldleからAddressableやScriptable Build Pipelineに移行しているという流れがあります。古いライブラリであるAssetBundleを今から改めて導入するのもなぁ…、ということで、Scriptable Build Pipelineによる解決を探ることにしました。
CompatibilityBuildPipeline
Scriptable Build Pipeline(以下、SBP)では、用意されたビルドのステップやパラメータを自由に組み合わせて使うことができます。SBPを使う開発者はその機能によって要件に合うようにパイプラインをカスタマイズして使用することが想定されています。
しかし、今回の改修では、SBPモジュールに含まれていた「CompatibilityBuildPipeline」を使用することにしました。
CompatibilityBuildPipelineは、旧AssetBundleと同様の挙動をScriptable Build Pipelineを使って再現しているという位置づけの機能になります。CompatibilityBuildPipelineを使うことで、Asset BunldeからSBPへと簡単に移行できるということが上記の公式ドキュメントで主張されていました。
今回の改修では、CompatibilityBuildPipelineの機能のみで十分そうだったので、自前のカスタマイズは行わず、こちらを採用しました。
実際に導入したところ、旧AssetBundleと同じAPIで利用することができたので、昔のコードを流用でき、特に問題なく実装することができました。
ただし、実装していく中で、AssetBundleと異なる点もいくつか見つかり、落とし穴となっていたので以下に紹介しておきます。
manifestファイルの仕様が異なる
旧AssetBundleでは、バンドルのバージョンや依存関係を記録するファイルとして、manifestというファイルがビルド時に生成されていました。CompatibilityBuildPipelineでもビルド時にmanifestファイルが生成されるのですが、その仕様にAssetBundleとは異なる点がいくつかあります。
まず、ファイルの名前と数が違います。旧AssetBundleではバンドルごとに同名のmanifestファイルが「バンドル名.manifest」という感じで生成されていました。一方、CompatibilityBuildPipelineのマニフェストは「プラットフォーム名.manifest」というファイルが一つのみ生成されるようになっており、そのファイルの中にビルド時の全バンドルの情報が記載されているというものになっています。
ファイル中のバンドルの情報をいったん抜き出し、別途バンドルごとになるように書き出すのは全然可能なのですが、自前で実装する必要があります。全アセットを一括ビルドしない場合は、manifestがバンドルごとになっていた方が取り回しが良いので、ここはバンドル別に分けるオプションも欲しかったところです。
また、アセットのバージョンを示すハッシュ値の算出方法が旧AssetBundleとは異なっているようです。このハッシュ値は後述するインクリメンタルビルドなどに使われる値です。旧AssetBundleではAssetFileHashとTypeTreeHashという二つの値の組み合わせで表現されていたのですが、CompatibilityBuildPipelineでは一つのハッシュ値になっています。
インクリメンタルビルド機能がない
旧AssetBundleには、前回のビルドから変更がないアセットについてはビルドを自動でスキップするという機能がありました。これはビルドの高速化に寄与する嬉しい機能でした。
↓インクリメンタルビルドの詳細についてはこちら(なぜか公式ドキュメントが見当たらないので、有志の方が書かれた記事を紹介させていただきます)
このインクリメンタルビルドですが、どうもCompatibilityBuildPipelineには実装されていないようで、前回からアセットの変更があってもなくても全て再ビルドされてしまいます。
旧AssetBundleのインクリメンタルビルドでは、manifestのハッシュ値を参照して照合することでビルドの要否を決定しています。
CompatibilityBuildPipelineでもmanifestとハッシュ値は生成されるので、ハッシュ値を照合してビルド対象から外す部分だけ自前で実装すれば、インクリメンタルビルドと同様の機能が実現できるのでは?と考え、調査しました。しかし、どうやらCompatibilityBuildPipelineのハッシュ値は旧AssetBundleのそれとは算出方法が異なっているらしく、そのままインクリメンタルビルドに使えるかどうかは微妙という結論になってしまいました。
CompatibilityBuildPipelineでインクリメンタルビルドを実現する方法はあるかもしれないのですが、ちょっと簡単にはいかなさそうだったので一旦諦めています。今後も引き続き調査はする予定なので、方法が見つかればまたブログなどで共有したいと思っています。
ちなみにScriptable Build Pipelineのハッシュ値を使わない方法でインクリメンタルビルド的なものを実現している事例もありました。参考までにリンクを掲載させていただきます。Unity内の情報ではなく、Gitのログを参照して差分を検出するという手法のようです↓
これらのように、単純にAssetBundleの代替とはならない部分もいくつかありましたが、無事にビルド方法の移行を終え、キャラクターモデルもアプリ本体のプロジェクトで管理できるようになりました。
以上の改修の結果、現在は以下のような構成でアセットを管理しています。
まとめ
今回はAddressable Asset Systemをホロライブアプリで導入した事例と、部分的にCompatibilityBuildPipelineに置換した経緯を紹介させていただきました。
Addressable Asset Systemは高機能なアセット管理システムとして簡単に利用できる一方で、大きなデータ量のプロジェクトではそのまま使うことが難しいという欠点もあります。Unityはそのような場合のためにScriptable Build Pipelineというカスタマイズ可能なシステムも用意しています。Scriptable Build Pipelineは公式ドキュメントがあまり充実しておらず、実際使う時にはソースコードを直接読むしかない場合が多いのがちょっとしんどいところです。
しかし、AssetBundleを各々でカスタマイズしていた時代よりはかなりマシになったと思っています。Scriptable Build Pipelineの活用事例がより多く公開され、プロジェクトの事情に応じてより活用しやすくなることを願っています。