UnityのAddressableアセットシステムにおけるコンテンツの更新フローまとめについてまとめました。
Unity2020.1.10
Addressables 1.8.5
はじめに
Addressableアセットシステムでは設定一つでコンテンツ(リソース)をプレイヤーに組み込んだり、ダウンロードコンテンツとして外に出したりできます。

ここでアプリをリリースした後にダウンロードコンテンツだけ更新することを考えます。

このようなコンテンツ更新の際には以下の二点を考慮する必要があります。
一点目として、リモートコンテンツカタログを同名で更新することが挙げられます。
使用するリモートコンテンツカタログの名前はプレイヤーに組み込まれているため、同名で中身だけ更新する必要があります。
ちなみにリモートコンテンツカタログの基礎知識については以下の記事にまとめていますので、必要に応じて参照してください。
二点目として、組み込みリソースの意図していない更新による不具合を防ぐための仕組みが必要です。
ダウンロードコンテンツだけ更新したつもりが実は組み込みリソースも更新されていて、
うまく更新が行われずに慌ててアプリもアップデートする・・といった事態を防がなくてはいけません。
Addressableアセットシステムではこのようなコンテンツ更新のワークフローを考慮した仕組みが用意されているので、本記事ではこれについてまとめます。
知っておくべき前提知識
コンテンツ更新フローをまとめる前に、必要な前提知識についてまとめます。
Content Update Restriction
Addressableアセットシステムのグループには、Content Update Restrictionというものが設定できます。

これをCan Change Post Releaseにするとそのリソースはアプリの更新なしに更新可能なリソースであるとマークされ、
Cannot Change Post Releaseにしておくとそのリソースはアプリを更新しないと更新できないリソースであるとマークされます。
基本的にはアプリに組み込むリソースだけCannot Change Post ReleaseとしてダウンロードリソースはCan Change Post Releaseとしておきます。
New BuildとUpdate Previous Buildの違い
さてAddressableアセットシステムにおいてリソースのビルドを行う方法は二つあります。
その一つ目がBuild > New Build > Default Build Scriptでビルドを行う方法です。

これは主にアプリの更新時(プレイヤービルド前)に行う処理で、以下のようにビルドを行います。
- 以前のローカルビルドパスのビルド結果のファイルを全て削除してビルド結果は新しいファイルとして作成
- リモートビルドパスは削除しない
- リモートコンテンツカタログの名前はビルド時のタイムスタンプを使った名前に更新される(
Player Version Override設定していない場合)
二つ目のビルド方法はBuild > Update a Previous Buildでビルドを行う方法です。

これはダウンロードコンテンツを更新する際に行う処理で、前回のビルド結果を参照して、以下のようにビルドを行います。
- 前回のビルド結果は削除しない
- リモートコンテンツカタログファイルは同じ名前で上書きする
Can Change Post Releaseなリソースのうち、更新があったものは別のファイル名(suffixのハッシュ値が別のファイル名)で新規作成Cannot Change Post Releaseなリソースは更新があったとしてもビルドされない- Addressablesのバージョンによってはビルドされるかもしれないがカタログからは結局参照されない
従って、このビルドを行った後に変更があったもの(更新されたリモートカタログファイルと新規生成されたリソース)をサーバに同期すれば、ダウンロードコンテンツの更新が完了するという仕組みになっています。
Check for Content Update Restrictions
さてUpdate a Previous Buildの挙動として一点気になるのが、Cannot Change Post Releaseなリソースは更新があったとしてもビルドされない、という点です。
この挙動ではダウンロードリソースのみ更新する際にもし間違えて組み込みリソースを更新してしまっても気づくことができず、結果重大なバグに繋がります。
このような事故を防ぐための仕組みがCheck for Content Update Restrictionsです。
Tools > Check for Content Update Restrictionsでツールを起動すると、前回のビルド結果を参照し、
Cannot Change Post Releaseなリソースのうち更新されてしまっているものを抽出できます。

ここでApply Changesを押下すると更新のあったリソースだけを切り分けてダウンロード用のグループに移動してくれます。
(ただし現実問題これをやり始めると組み込みのリソース設計がどんどん当初とずれていくので用法用量には要注意です..)
コンテンツ更新ワークフローまとめ
さてそれでは前述の前提知識を踏まえてコンテンツ更新のワークフローをまとめます。
Remote Content Catalogを有効にする
まずダウンロードコンテンツを取り扱う場合にはRemote Content Catalogを有効にしておく必要があります。
これに関しては以下の記事を参照してください。
プレイヤーバージョンについては設定しないほうが毎度変える手間が省けて楽です。
Content Update Restrictionを設定する
次にContent Update Restrictionを設定します。
組み込みリソース用のグループはCannot Change Post Releaseに設定し、ダウンロードコンテンツ用のグループはCan Change Post Releaseに設定します。

プレイヤー(アプリ)ビルド時のリソースビルド
ここまで設定が完了したらビルドを行います。
ビルドは プレイヤー(アプリ)をビルドするときとダウンロードコンテンツをビルドするときで方法が異なります。
プレイヤービルドを行う際にはBuild > New Build > Default Build Scriptでビルドします。

これで、組み込みリソースとダウンロードコンテンツの両方がビルドされます。
ちなみにスクリプトからビルドする場合には以下のような感じです。
// 必要に応じてClean AddressableAssetSettings.CleanPlayerContent(AddressableAssetSettingsDefaultObject.Settings.ActivePlayerDataBuilder); // Addressableをビルド(New Buildと同じ処理) AddressableAssetSettings.BuildPlayerContent(); // Playerをビルド BuildPlayerWindow.DefaultBuildMethods.BuildPlayer(options);
ダウンロードコンテンツ更新時に組み込みリソースに更新がないかチェックする
次にアプリの更新をせずにダウンロードリソースだけ更新することを考えます。
ダウンロードリソースをビルドする際には、組み込みリソースが間違えて更新されてしまっていないかチェックする必要があります。
これはTools > Check for Content Update Restrictionsから行います。

これを選択するとファイル選択パネルが表示されるので、前回のビルド情報を指定します。
このビルド情報はAssets/AddressableAssetsData/(Platform)/addressables_content_state.binにあります(StreamingAssetsCopyにも生成されます)。

addressables_content_stateにはCannot Change Post Releaseなアセットのハッシュ情報と、プレイヤーのバージョン情報が保持されています。
この情報を元に組み込みリソースの変更を検知する仕組みになっています。
ビルド情報を設定するとウィンドウが表示されます。
組み込みリソースに変更がない場合にはウィンドウに何も表示されず、変更があった場合にはそのアセットが表示されます。

組み込みリソースが変更されてしまっていた場合にはその変更が起こらないようにアセットを修正します。
もし意図的な変更であればApply Changesをすればそのアセットをダウンロードリソースに変更できますが、前提知識で書いた通りあまりお勧めはしません。
なおこれをスクリプトから行うには以下のようにします。
// addressables_content_state.binを取得 // ファイル選択パネルを出したい場合は引数をfalseに var path = ContentUpdateScript.GetContentStateDataPath(false); // 変更があった組み込みリソースを取得 var settings = AddressableAssetSettingsDefaultObject.Settings; var modifiedEntries = ContentUpdateScript.GatherModifiedEntriesWithDependencies(settings, path); foreach (var modifiedEntry in modifiedEntries) { // 変更があったアセットのアドレスを出力 Debug.Log(modifiedEntry.Key.address); }
ダウンロードリソース更新時に組み込みリソースの更新を許さないなら例外を投げる仕様にしたほうが良いかもしれません。
var path = ContentUpdateScript.GetContentStateDataPath(false); var settings = AddressableAssetSettingsDefaultObject.Settings; var modifiedEntries = ContentUpdateScript.GatherModifiedEntriesWithDependencies(settings, path); if (modifiedEntries.Count == 0) { throw new Exception("組み込みリソースが更新されてます"); }
ダウンロードリソースだけビルドする
次に実際にダウンロードリソースのビルドを行います。
この場合、Build > Update a Previous Buildでビルドします。

これを選択するとファイル選択パネルが表示されるので、前節のようにビルド情報(addressables_content_state.bin)を指定します。

なおスクリプトから行うには以下のようにします。
// addressables_content_state.binを取得 // ファイル選択パネルを出したい場合は引数をfalseに var path = ContentUpdateScript.GetContentStateDataPath(false); if (string.IsNullOrEmpty(path)) { return; } // ダウンロードコンテンツをビルド ContentUpdateScript.BuildContentUpdate(AddressableAssetSettingsDefaultObject.Settings, path);
ランタイムでカタログを更新する
次にビルドしてアップロードしたリソースやコンテンツカタログをランタイムで扱うことを考えます。
カタログに更新があるかをチェックするにはAddressables.CheckForCatalogUpdates()を、
カタログを更新するにはAddressables.UpdateCatalogs()を以下のように使用します。
// 変更されたカタログのロケータID一覧を取得 var checkUpdatesHandle = Addressables.CheckForCatalogUpdates(false); await checkUpdatesHandle.Task; var updates = checkUpdatesHandle.Result; Addressables.Release(checkUpdatesHandle); if (updates.Count >= 1) { // カタログを更新する // 引数を指定しない(catalogをnullに指定する)とすべてのカタログを更新 Addressables.UpdateCatalogs(); // 特定のカタログだけフィルタリングして渡すこともできる //Addressables.UpdateCatalogs(updates); }
ダウンロードリソースを更新する
カタログをダウンロードしたらあとは新しくリソースがリクエストされたときにダウンロードされます。
既に読み込み済みのリソースを更新する場合にはメモリを綺麗にするかUnique Bundle IDを有効にする必要があります。
また明示的にダウンロードだけを行いたい場合にはAddressables.DownloadDependenciesAsync(key)などを使用します。
var key = "someAddress"; // 既にダウンロード済みのものを消したい場合 //Addressables.ClearDependencyCacheAsync(key); // ダウンロードサイズを取得 var getDownloadSizeHandle = Addressables.GetDownloadSizeAsync(key); await getDownloadSizeHandle.Task; // ダウンロードが必要だったらダウンロード if (getDownloadSizeHandle.Result > 0) { // ダウンロード // 内部的にはLoadAssetAsyncしてReleaseしてるだけ var downloadDependencies = Addressables.DownloadDependenciesAsync(key); await downloadDependencies.Task; }