はじめに
Unityを利用して大規模なプロダクトや運用を想定したゲームを開発していると必ずと言っていいほど向き合わないといけない物の1つが AssetBundleです
AssetBundleを使うことで特定のリソースをアプリ内でダウンロードして利用できます。
また、同じ様に特定のコンテンツを更新したり、リソースの追加配信なども出来るのが強みです。
昨今のソーシャルゲームなどでは当たり前のように利用されているこのAssetBundleですが、最近ではAssetBundleをラップした形のライブラリである AddressableAssetSystem (Addressables)を利用することが多いです
Addressablesを使うことでAssetBundleのビルド、ダウンロード、更新フローなどを簡単に実現する事ができますが、依然として情報が少ない印象はあります。(おそらく2018年頃のリリース時点で既に大多数のプロジェクトでは独自にAssetBundleの基盤を開発していたのでAddressablesへの移行はそこまで活発じゃないのかなと邪推しています)
利用方法などは初心者向けの記事などもQiitaを始めとして多く存在しますが、結局リソースダウンロードと更新のワークフローどうしたら良いんだ?と悩んでしまいます(実際私もかなり頭を悩ませました)
そこで本記事では
- リソースのビルド(AssetBundleビルド)
- リソースの配信
- リソースのダウンロード
- 配信されているリソースの修正・更新
これらの実現方法について紹介します
まぁ、ざっくり言ってしまえば 「細かいことは良いからリソースダウンロードと更新出来る方法教えてくれよ」っていう人向けの記事です。
注意事項
- あくまで自分なりに考えた推奨設定、手順の紹介になります。実際の運用プロダクトに使う際はCICDなどの構築含め考えることは山程あるので注意してください(本記事ではそこまでは解説しません。)
環境
今回の記事では以下の環境で検証を行っています
- Unity 6000.0.29f1
- Addressables 2.2.2
- Mac OSXアプリケーションとしてバイナリをビルドします
環境構築
まずはUnityで新規プロジェクトを作成し、PackageManagerからAddressablesをインストールします
Window -> AssetManagement -> Addressables → Groupを選択すると、設定ファイルの作成を促されるのでそのまま作成します
GroupウィンドウにDefault Local Groupが表示されている状態になればOKです
Addressablesを設定する
Groupの作成
今回の記事では2つのGroupを作成し、1つは内部リソース、もう1つは外部リソース というわけ方をしてサンプルを実装します。
GroupとはAddressables上でのアセットの管理単位のようなもので、このグループ単位でリソースを何処から読み込むのか、どのような単位でAssetBundleを作成するのか、 といった設定を行うことができます。
詳細はドキュメントを参照してください。
https://docs.unity3d.com/ja/Packages/com.unity.addressables@1.20/manual/Groups.html
今回は BuiltInAssetsというグループとRemoteAssetsという名前のグループの2つを作成します。
Groupウィンドウで右クリック→ Create New Group → Packed Assetsを選択しグループを2つ作成します。
作成後は名前を変更し、BuiltInAssetsとRemoteAssetsにします
この2つのグループにアセットを登録していくことで、Addressablesで読み込めるようになります。
名前の通り、BuiltInAssetsはアプリのビルド時に最初から入るリソースで、StreamingAssetsに配置される(ように設定します)ものです。
RemoteAssetsはWeb上からダウンロードして取得するグループとして設定を行います。
実際のプロダクト開発では BuiltInにはUI素材や更新を想定していないPrefabなどの素材を入れ、
Remoteにはイベント画像やキャラ絵などの運用素材を入れるようなイメージになります。
Groupの設定 BuiltInAssets
ではここからはGroupの設定を変更していきます。
前述の通り、Groupにはいくつかの設定項目があり、これらを変更することでリソースの読み込み方やBundleの単位などを制御することができます。
GroupウィンドウでBuiltInAssetsのグループを選択するとInspectorに設定が表示されます
Addressablesのグループ設定は、Schemaと呼ばれる設定のコンポーネントが複数個ついている形で表現されています。
ここに独自に実装したSchemaなどを設定することでビルドや読み込み時の処理等をカスタマイズすることも可能です。
例えばAndroidのPlayAssetDeliveryに対応するUnity公式のサンプル等では独自のSchemaを実装し、利用していたりします。
まずBuiltInAssetsのGroup設定を本記事ではこのように設定します
といっても基本的にBuiltInAssetsに関しては設定をほぼ変更していません。
個人的には以下を確認しておいたほうが良いと思っています
-
Build & LoadPathはLocal
- ここの設定でリソースの読み込み先を制御します。Localだとアプリ内に入る という挙動になります
-
Bundle Naming ModeはAppendHash
- AssetBundleのファイル命名に関するオプションです。基本的にはAppendHashで困ることは無いです
-
Include Address / GUIDs / Labels In Catalogオプションは全部ON
- このグループのアセットをアドレス指定 or GUID指定 or Label指定で読み込む事は無い みたいなシチューエーションにおいてはOFFにしてもいいですが基本的にONの方が無難だと思われます。(恩恵カタログファイルの容量節約になるくらい?)
-
Bundle Mode 今回はPackSeparately
- AssetBundleの単位を設定できます。Togetherだとこのグループをまるっと巨大な1バンドルとしてビルドします。
- Groupをつくる単位、ファイルサイズなどで設定を変えるのをおすすめします。
- 一見、Separatelyにして1アセット1バンドルにしときゃ良いやん!って思うかも知れないんですが、そのアセットが他のシェーダーを参照していたり別のアセットを参照しているような実装になっている場合、その分も一緒にバンドルされてしまい、容量が増えたりする可能性があります。
- これらの事象に関してはUnity公式のブログ記事がとてもわかり易いので参考にしてください!
https://unity.com/ja/blog/technology/tales-from-the-optimization-trenches-saving-memory-with-addressables
- これらの事象に関してはUnity公式のブログ記事がとてもわかり易いので参考にしてください!
-
Content Update RestrictionのPrevent UpdatesをON
- これはこのグループのコンテンツを後から更新できるか(アプリをビルドした後に後から変更できるか)を設定します。
- BuiltInAssetsは内部リソースとして利用するグループになるのでONにして後からの更新を許可しないようにします。
Groupの設定 RemoteAssets
続いてRemoteAssetsも設定を行います。
基本的には一緒ですが、以下の部分だけ変わっています
- Build & Load PathをRemoteに
- Prevent UpdatesをOFFに
Build & LoadPathは今回はRemoteに設定しています。これはリソースを外部ストレージからダウンロードするためです。
Prevent Updatesは、RemoteAssetsに関してはアプリビルド後にも変更可能であってほしいためOFFにしています。
これでGroup自体の設定は完了です。
続いてAddressables自体の設定を変更していきます
Addressables自体の設定を変更する
Assets/AddressableAssetsData/AddressableAssetSettings.asset
にあるAddressables自体の設定ファイルを開き、設定をいくつか変更します
- まず、CatalogのBuild Remote CatalogをONにします。これをしないとリソースDLに必要なカタログファイルが生成されません。Build & Load PathはRemoteに設定します。
- Internal Asset Naming ModeをDynamicにします。これは内部で利用されるアセットのIDの割り振り方についての設定になります。デフォルト設定であるFull Pathは開発時はデバッグの助けになることがあるらしいですが 一番パフォーマンス出るのはDynamic とのことです
- Dynamic: グループ内のアセットに基づいて作成できる最も短い ID。このモードは、リリースに使用することを推奨します。AssetBundle およびカタログ内のデータ量が小さくなり、ランタイム時のメモリオーバーヘッドも削減できるためです。
( https://docs.unity3d.com/ja/Packages/com.unity.addressables@1.20/manual/GroupSettings.html )
- Internal Bundle Id Modeも同様の理由によりGroup Guidに変更します
- Group Guid: グループの一意の ID。このモードは変化しないため、これを使用することを推奨します。
最後にBuildセクションの中のUnique Bundle IDsを有効にします
このオプションはアプリ内で読み込み済みリソースが更新された際に、リソースの開放を待たずに更新を可能にするた
めのオプションです。
コンテンツを動的に (アプリケーションの起動時ではなく) 更新する場合に考慮するべきもう 1 つの設定として、Unique Bundle IDs (一意のバンドル ID) 設定があります。このオプションを有効にすると、アプリケーションセッションの最中に、更新された AssetBundle を簡単にロードできるようになりますが、通常はビルドにかかる時間が長くなり、更新のサイズも増大します。[Unique Bundle IDs 設定] を参照してください。
(https://docs.unity3d.com/ja/Packages/com.unity.addressables@1.20/manual/ContentUpdateWorkflow.html)
デメリット等を考えるとOFFが望ましいですが厳格なリソース管理を行う必要があるため、ONにしておいたほうが運用事故は減らせるかなぁと私は思っています。
参考:https://light11.hatenadiary.com/entry/2021/01/12/220442
コンテンツのアップロード先を準備する
今回、RemoteAssetsGroupのアセットはアプリの中でダウンロードして取得→表示を行う想定です。
それを実現するためには、当然ですがビルドされたAssetBundleを外部のストレージサービスなどにアップロードし、そのダウンロードURLに向けてアプリ内で通信を行う という事が必要になります。
Addressablesは内部で勝手にダウンロード処理自体はやってくれますが、DL先のURLは事前に設定しておく必要があります。
基本的にはAWS-S3などのクラウドサービスのストレージに配置することが多いかなと思います。
CloudFrontなどでキャッシュ効かせるとかももちろんやってもいいですが、今回はシンプルにS3にリソースを配置します。
ストレージ自体はどのサービスを利用してもいいですが、ストレージへのURL/assetBundleのファイル名.bundle
というURLでファイルがDL出来る状態になっている必要があります(認証もなし)
例:https://XXXXXXXXXXXXXXXXXXX.s3.ap-northeast-1.amazonaws.com/{Platform}/hogehoge.bundle
ストレージサービスを準備したらAddressablesにURLを設定します。
Window -> AssetManagement -> Addressables -> Profilesを開きます
このようなウィンドウが出てきます。
ここでは、Build & LoadPathで設定した項目に対応する読み込み先、ビルド先を指定することができます。
Remote.BuildPathが 配信対象のアセットバンドルのビルド先になります。この設定だとUnityのプロジェクト直下にServerData/{プラッフォーム名のフォルダ}というディレクトリが作成され、そこにBundleファイルが配置されます(ここは基本的に変更しなくても大丈夫です。)
Remote.LoadPathはDL先のURLを入れます。
今回の例ではこのように設定しました
つまりビルドされたときに生成されるServerDataフォルダをそのままS3の直下にアップすれば良い というURL構造です。
リソースを登録する
ここまで準備できればあとはAddressablesにアセットを登録し、読み込むコードを書くだけです。
今回はいらすとや からいくつかの画像をお借りして使ってみます。
Assets/AddressableResourcesというディレクトリを作成し、
その中にBuiltInAssets、RemoteAssetsフォルダを作成し画像を配置していきます。
BuiltInAssetsには箱の画像
https://www.irasutoya.com/2017/08/blog-post_450.html
RemoteAssetsには鳥の画像を配置することにします
https://www.irasutoya.com/2014/03/blog-post_7676.html
AddressablesのGroupウィンドウを開き、アセットをGroupにドラッグすることで登録できます
デフォルトの設定だとアセットへのパスがそのままAddressとして登録されます。
AddressablesはこのAddressと呼ばれる文字列をコード上で指定することでリソースの読み込みが可能なのですが、このままだと少し管理が大変です
右クリックし、Simplify Addressable Namesをクリックすることでファイル名をAddressにすることが出来るので利用します
これでbox,bird_01,bird_02の3つのアセットが登録されました
リソースを読み込んで表示するコードを書く
続いて、リソースを出す実装を行います。
今回はサンプルとしてImageコンポーネントに画像をセットして表示させる というシンプルなサンプルを実装します
こんな感じで画像名を入力してLoadボタンを押せば画像が表示される というUIにしました。
スクリプトを実装します
using System;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.UI;
public class Controller : MonoBehaviour
{
[SerializeField] private Text _logText = null;
[SerializeField] private InputField _inputField = null;
[SerializeField] private Button _loadButton = null;
[SerializeField] private Image _image = null;
private void Start()
{
var busy = false;
_loadButton.onClick.AddListener(async () =>
{
if (busy)
{
return;
}
busy = true;
var downloadSize = await Addressables.GetDownloadSizeAsync(_inputField.text).Task;
if (downloadSize > 0)
{
_logText.text = $"ダウンロードが必要\nSize{downloadSize}";
//logTextの文章読ませるための2秒待機
await Task.Delay(TimeSpan.FromSeconds(2),this.destroyCancellationToken);
}
_logText.text = "読み込み開始";
var loadedSprite = await Addressables.LoadAssetAsync<Sprite>(_inputField.text).Task;
_image.sprite = loadedSprite;
_logText.text = "読み込み完了";
busy = false;
});
}
private void OnDestroy()
{
_loadButton.onClick.RemoveAllListeners();
}
}
ボタンをクリックしたらInputFieldに入っているテキストを元にリソースの読み込みをするというコードです。
GetDownloadSizeAsyncをコールし、DLサイズを確認しています。
BuiltInAssetsのグループのアセットを読み込む際はこのタイミングで0が返ります。
RemoteAssetsかつ、DLを行っていない場合は1以上の数値が返る という流れです。(なおEditor実行時は基本的に0が返ります)
var downloadSize = await Addressables.GetDownloadSizeAsync(_inputField.text).Task;
if (downloadSize > 0)
{
_logText.text = $"ダウンロードが必要\nSize{downloadSize}";
//logTextの文章読ませるための2秒待機
await Task.Delay(TimeSpan.FromSeconds(2),this.destroyCancellationToken);
}
それでは実際にアプリをビルドして試してみましょう
アプリとAssetBundleのビルド
まずはアプリのビルドを行います。
Addressablesはデフォルト設定ではアプリをビルドする際に自動でAssetBundleのビルドも実施してくれるので、何も考えずにアプリをビルドするだけでOKです
ビルドが完了したらアプリのバイナリファイルと、ServerDataフォルダが生成され、その中にAssetBundleの実体が生成されていることを確認します
build.appがアプリ本体(Macで検証しているので.appになっている。Winだと.exe)
ServerData/StandaloneOSX/にある .bundleがAssetBundle本体です
catalog_0.1.0.binは所謂カタログファイルと呼ばれるもので、リソースのダウンロード先などの情報が記載されています。アプリ起動時にAddressablesは自動でこのカタログファイルを取得し、そのカタログを元にリソースを読み
込みます。
.hashはカタログファイルのハッシュ値を記したファイルです。
このファイルを最初に見ることでAddressablesはカタログが更新されているかを判定します。
そして、しれっと
Assets/AddressableAssetsData/OSX/
というディレクトリにaddressables_content_state.bin
というファイルが生成されています
これが超大事なファイルになります Addressablesはアプリをビルドしたタイミング(厳密にはAssetBundleを新規ビルドした時)でこのcontent_state.binというファイルを生成します。
配信対象のリソースを更新・追加した際に、このbinファイルを指定してAssetBundleをビルドすることで更新用のAssetBundleビルドを行うことができます
そのため、運用型のプロジェクトにおいては、このファイルはアプリビルドの度にちゃんと保管しておく必要があります。とにかく大事なファイルなので忘れないようにしましょう。
AssetBundleをS3にアップロードする
生成されたServerDataフォルダをS3にアップロードします
(実際のプロダクト開発においては手動アップロードなんてせずにaws cliのaws s3 syncなどを使ってアップするのをおすすめします。今回は手動でアップしてます)
これでリソースの配置も完了したのでアプリが動くようになりました!実際に動作確認をしてみましょう。
動作確認
ちゃんとRemoteAssetsのアセットを指定した際にダウンロードサイズが表示され、ダウンロードが走っていることが確認できます🎉
一度ダウンロードした画像を再度読み込んだ際は(ダウンロードが終わっているので)即時表示されます
リソースの追加・更新をしてみる
さて、ここまでで「リソースをアプリ実行時にダウンロードして表示する」という部分は実現できました。
実際のゲーム開発で例えるとリリースまでできた状態です。
ここからは実際にリソースの追加、更新を行ってみましょう
リソースを用意する
検証として、以下の変更をRemoteAssetsに加えることにします
- bird_01(青い鳥)を赤い鳥の画像に変更する
- bird_03を追加する
既存リソース(bird_01)の上書き更新と新規追加(bird_03)ですね。
このような形になりました。
青い鳥が赤くなり、緑色の鳥が追加されました。
忘れずにGroupへの登録も行います
AssetBundleの更新用ビルドをする
これでリソースの準備はできたのでAssetBundleの更新ビルドをします。
アプリのビルドではなくAssetBundleのビルドだけを行います。(既にアプリはリリースされているので)
手順は簡単で、
GroupウィンドウのBuild→Update a Previous Buildをクリックすることで自動で更新用ビルドを行ってくれます。先ほど説明したaddressables_content_state.binを自動で読み込み、差分ビルドを実施してくれます。
addressables_content_state.binのファイルパスを指定したい場合はAddressablesの設定ファイル Assets/AddressableAssetsData/AddressableAssetSettings.asset
の中にあるUpdate a Previous Buildの中のContent State Build Pathを弄ってあげることで任意のパスから読み込むことが可能です。
運用型アプリにおいてはEditorのメニューをクリックして毎回ビルドするよりはJenkinsなどからスクリプト経由でビルドすることが多い(かつaddressables_content_state.binの場所も独自にカスタマイズしている事が多い)と思われるのでスクリプト上でここを書き換えてビルドさせるのがオススメです。
ServerDataフォルダを確認すると新たにbird_03のbundleファイルが増えていることが確認できます。
これをS3にアップすればリソースの反映がアプリにも行く という訳です。
更新用ビルドをした際は
- catalog_XXX.bin
- catalog_XXX.hash
- 変更・追加されたAssetのbundleファイル
に変更が加わります。すべてを必ずアップロードしてください。
更新されるか確認する
まず、S3にアセットをアップしてない状態でのアプリの挙動を念の為確認します
bird_01は青いままですし、bird_03はまだS3に存在しないので読み込めず、白いImageがそのまま表示されてますね。
では実際にS3にリソースをアップします
アップが完了したらアプリを再度起動してみましょう。
ちゃんとリソースが更新されました!
- boxは元々builtInなのでDLなしでそのまま表示
- bird_01の画像が変わっているので再DLが走り赤い鳥が表示
- bird_02は変更無いので(Assetのハッシュも変わってないので)再DLが走らずそのまま表示
- bird_03は新規追加されてるのでDLが走る
ということでバッチリデータが更新されました🎉
このようにaddressables_content_state.binを利用することで更新用アセットバンドルをビルドすることができます。ユーザーのアプリ自体はそのままで更新のあるコンテンツだけアプリ内ダウンロードという形で提供することができましたね。
まとめ
Addressableのセットアップから簡単なリソース更新までの手順をざっくりと紹介しました。
Addressables何も分からんという人がこの記事をきっかけにリソース更新と向き合えるようになると幸いです。
なお実際のプロダクトで使う際はもっと考慮することがいっぱいあります
- リソースのブランチ運用どうするのか
- AddressablesのGroupの単位どうするのか
- リソースダウンロード中の表示制御どうしよう?
- addressables_content_state.binを保管したうえでリソース更新ビルドするCICDの構築
- ダウンロードが失敗したときのエラーハンドリング
- アプリアップデート時に過去のリソースがおかしくならないか?
- アプリアップデートしたときに古いアプリと新しいアプリが共存して存在しリソースDLできるような実装になっているのか?
- カタログ取得に失敗した場合に復帰できるのか?
などなど…
これらはプロダクトの性質などにより変わってくるところではありますが、考慮しておく必要があります。
自分ならどうするか っていうのはそのうち記事にするかもしれません(きっと...)
それでは皆様良きAssetBundleライフを....