Sumzap Engineering Blog

サムザップのエンジニアブログです。Unity, PHP, AWS/GCPなどの技術情報や会社のエンジニア文化などを日々発信していきます。

サムザップのエンジニアブログです。
技術情報や会社のエンジニア文化などを日々発信していきます。

Sumzap Engineering Blog

甘くて苦いAddressable Assets System

f:id:sumzap_engineer_blog:20200324193204p:plain

目次

はじめに

弊社運用タイトルにてインゲームやその他雑用を担当している中山です。
昨年は、Unite Tokyo 2019にてAddressable Assets System(以下AAS)についての講演をさせていただきました。
今回は、当時お話していたタイトルがリリースされて運用に入ったので、現在はどのような形でこの機能を取り扱っているか、軽く記していこうと思います。

AASの概要や基本的な使い方につきましては、公式ドキュメントフォーラムを読まれるといいと思います。

リモートアセットの読み込みついて

殆どの場合、アセットはアプリケーションに内蔵されるのではなく、ネットワークを介しての取得になると思います。
昨今では、Appleの規約も相まって大きなダウンロード前には、サイズを表記しなければならないなど、我々の仕事は増えています。

利用しているAPI
幸いAASには、リモートアセットの都度取得や、まとめてのダウンロード、更にはアセットのサイズ取得などの機能が備わっています。
当プロジェクトでは、バトル用のアセットや音声データなどの大きなものは、明示的にタイトル画面にてダウンロードしています。
それらに当てはまらないUI用のプレハブやアイコン類は、必要になったときに自動的にダウンロードされるようになっています。
上記のようなことを行うのに、複雑な実装は必要ありません。
グループ設定さえ適切に行われていれば、たった2つのAPIで実現できます。

  • 明示的なダウンロードにはAddressables.DownloadDependenciesAsync(ラベル or List<IResourceLocation>)
  • アセットのロードにはAddressables.LoadAssetAsync<T>(アドレス)

これらを使用するだけで、対象のアセットをリモートから取得し、ストレージキャッシュに自動的に書き込み、そしてメモリにロードします。
もちろん、アセットの依存関係も自動的に解決されます。
ストレージに書き込まれるデータはアセットバンドルキャッシュに準じます。保存先も一緒です。

パスの設定
カタログや各アセットのグループ設定にはロードパスとして取得先のURLを入れておきます。
このロードパスの設定ですが、純粋な文字列だけでなく変数も入れることができます。
{フルパスの変数名}で記しておくと実行されたときにパスが決定されます。
実際、運用タイトルではバージョン管理の関係でURLが可変になることもあると思いますので、是非活用してください。


f:id:sumzap_engineer_blog:20200318143920p:plain
{}は実行時評価、[]はビルド時に評価される。

アセット更新について

アセット更新は、アプリケーションアップデートに比べるとカジュアルに行える更新ですが、AASの場合その取り扱いはちょっと繊細です。

カタログの取り扱い
AASは、アセットがもつ複雑な情報をたった一つのcatalog.jsonに記しています。
正確には、jsonファイルを読み込んだ後に、内部でResourceLocatorという形で保持しています。
理屈の上では、catalog.jsonを差し替えれば、アセットの状態を新しくするだけでなく過去の状態にもできるということです。
このとき問題になるのは、catalog.jsonの再読み込みを行ったときに、ResourceLocatorが最新の状態に更新されるかどうかです。
AASでは、CheckForCatalogUpdates()UpdateCatalogs()という形で、提供されています。
このAPIはリモートとローカルに存在するhashファイルを比較して、必要に応じてcatalog.jsonをダウンロードした後にResourceLocatorの更新を行います。
しかし、何らかの理由でリモートにあるhashの取得に失敗した場合、エラーを介さずにローカルのcatalog.jsonを利用する動きになっています。
さらに、hashのチェックに成功しても、リモートにあるcatalog.jsonの取得で失敗した場合はコルーチンが無限ループに陥ります

LoadContentCatalogAsyncの強行利用
以上のような問題があったので、当プロジェクトでは、LoadContentCatalogAsync(パス)を利用しています。
このAPIは任意のパスを引数に、catalog.jsonを取得してResourceLocatorにすることができます。
しかし、hashファイルを検知する能力がないので、hashファイルの差異を検知する部分は自前で実装しています。
処理の流れは、hashファイルの比較→カタログのダウンロード(UnityWebRequest)→LoadContentCatalogAsync(DL後のローカルパス)といった形になっています。

※↓例です

//ローカルに保存したhash
string localHash = "";
//リモートにあるhash
string remoteHash;

//中略 双方の取得

// リモートとローカルのハッシュを比較
if (localHash.Equals(remoteHash, StringComparison.Ordinal))
{
 return true;
}

//カタログの取得
//ダウンロードハンドラを利用する
 var handler = new DownloadHandlerFile(保存先)
 {
  removeFileOnAbort = true
 };

using (var req = new UnityWebRequest(リモートカタログのURL, UnityWebRequest.kHttpVerbGET, handler, null))
{
 //中略 エラーハンドリングとか
}

// 取得したカタログをメモリにロード
AsyncOperationHandle<IResourceLocator> op = Addressables.LoadContentCatalogAsync(保存したカタログのパス);

await op.Task; //実はAsyncOperationHandleはTaskを備えています。

//中略 エラーハンドリングとか

// 読み込んだカタログでResourceLocatorを差し替える
var newLocator = op.Result;

Addressables.ClearResourceLocators();
Addressables.AddResourceLocator(newLocator);

//2回以上叩くと起こすエラーを回避するためにリリースする。
Addressables.Release(op);

現時点で遭遇した不具合

上記のような方法をとって運用を行っていますが、案の定すんなりとは行きませんでした。
まずDownloadDependenciesAsyncですが、巨大な依存関係を持つラベルを引数で渡してしまうと、その分のファイル書き込みとリクエストを同時に生み出してしまうため、ストレージへの書き込み不能エラーを引き起こします。
なので、ラベルからIResourceLocationを依存関係分引き出してから、ひとつひとつDownloadDependenciesAsyncに渡してダウンロードを行っております。
LoadContentCatalogAsyncについては、2回以上連続で実行すると必ずエラーを起こしてしまうバグが有りました。
かなり致命的なのですが、実行する際にAsyncOperationHandleを変数に格納して、releaseすれば回避できることがわかっています(この問題はUnityに問い合わせ済みです)。

最後に

市場に存在するゲームの分だけアセット管理の仕組みも存在すると思いますが、当プロジェクトではUnity純正という道を選びました。
導入もお気軽で、機能もなかなかに強力です。
ただ・・・一年前に比べれば遥かにマシですが、時々素直に動いてくれない箇所があります。
どれも回避法はあるので、Unityに問い合わせをして修正を気長に待っています。