Posted at

知っておきたいAddressable Asset Systemの仕組み

More than 1 year has passed since last update.


はじめに


Addressable Asset Systemとは

Unity 2018.2が先日公開されました。

このバージョンになるといくつか使えるようになる機能があり、そのうちの一つにAddressable Asset Systemというものがあります。

これはAssetBundleのビルドやロードなど、いままで複雑だったところをユーザーが全く意識することなくシンプルに実装できるようにしたものです。それこそResourcesと同じくらいに。

例を挙げてみます。

画像データをダウンロードし、それを表示するコードを見てみましょう。

public class Sample : MonoBehaviour

{
public Image image;

void Start()
{
// SpriteをロードしてImageに表示する
Addressables.LoadAsset<Sprite>("character").Completed += op => { image.sprite = op.Result; };
}
}

なんとたったこれだけです。

いままでユーザー側で書く必要があったAssetBundleのダウンロード処理などは、Addressables.LoadAssetを使えば内部で勝手にやってくれるので一切気にする必要はありません。

また、このほかにも様々なメリットがあり、まとめると以下のようになります。


  • ロードするために書くべきコードが少なく楽

  • ローカル、リモート、どちらからロードする際にもコードは同じ

  • アセットの参照を元にロードできるため変更に強い

  • 依存関係を気にしなくて済む


  • Asset Bundle Browserのように管理ツールを使ってアセットを整理できる

  • エディタ上でAssetBundleのシミュレーションができる(AssetDatabaseがそれっぽく振る舞う)

  • 何がロード、アンロードされているか可視化できるプロファイラがある


知っておきたいこと

さて、このAddressable Asset Systemですが、シンプルゆえに内部の仕組みに対して理解があいまいになりがちです。

なので、本記事ではその内部の仕組みについて解説していきたいと思います。

なお、今回仕組みを解説していくにあたって、Unite 2018 Tokyoであった講演とAddressable Asset Systemのソースコードを参考にさせていただいています。

講演動画:【Unite Tokyo 2018】そろそろ楽がしたい!新アセットバンドルワークフロー&リソースマネージャー詳細解説


環境


  • Unity 2018.2.0f2

  • Addressables 0.1.2-preview(自前でパッケージを組み込む必要があります)

{

"dependencies": {
"com.unity.addressables": "0.1.2-preview"
}
}

詳細:Unity Package Managerについて

※Addressable Asset Systemのバージョンによって内容が変わっている可能性があります


Addressable Asset Systemの仕組み


Resource Managerについて

Addressable Asset Systemは2つのシステムから成り立っていました。Addressable AssetsResource Managerです。

Addressable1.jpg

このうち、ロードを担当するのがResource Managerです。

どうロードするか見ていきましょう。

Addressables2.jpg

まず、Resource Managerに対してロード処理の依頼をするにはResource Location(以下Location)を渡す必要があります。

Locationとは、ロードするアセットに関する情報(配置場所や適したProvider情報、依存関係など)が入っているものです。

次にResource Managerは、受け取ったLocationを元にそのアセットに適したProviderを探して、見つかり次第ロードを依頼します。

このProviderというものが実際にロード処理を行うものになります。

例えば、AssetBundleProviderというものがありますが、この中ではダウンロード処理が行われていたりしています。

そして最後にProviderはロードの処理をAsyncOperationとして返し、それが完了まで待つ、というのがロードの一連の流れになります。


Addressable Assetsについて

さて、ロードの仕組みはわかりましたが、このままではユーザー側からLocationを渡す必要があります。

Locationは少し複雑なので、それではシンプルさからは遠ざかってしまいます。そこで登場するのがもう一方のAddressable Assetsです。

Addressables3.jpg

Addressable Assetsは上記の画像のように、ユーザーとResource Managerの間に入ってアドレス(文字列)からLocationへの変換を行います。

これによって、ユーザーはアドレスを渡すだけでロードすることができるようになります。

このアドレスからLocationへの変換の役割を担っているのがResource Locatorです。

Resource Locatorは自身の持つアセットとアドレスの対応表を元に変換処理を行っています。この対応表はコンテンツカタログと呼ばれ、ビルド時にjsonファイルとして生成されるようになっています。

Addressables5.jpg

アプリ実行時にはそのjsonファイルがデシリアライズされ、得られたデータを元にResource Locatorが作られます。

その後、Resource Locatorはユーザーにとって窓口となるAddressable Assets(Addressablesクラス)に登録され、ロード依頼が来るたびに中で変換処理を行うことになります。


ビルドされるまで

ここまででAddressable Asset Systemの大まかな仕組みは理解できたと思います。

次は、実際にビルド&ロードして、その理解をより深めていきましょう。


導入

まずは、

Window -> Asset Management -> Addressable Assets

で管理用のWindowを開きます。

create_menu.PNG

ここから、Create Addressables Settingsをクリックすると設定ファイルを作成することができます。

そうすると、Assets/AddressableAssetsData以下に設定ファイルが作成されます。

addressable_setting.PNG

ここでは管理ツールで設定できるグループを見ることができるほか、圧縮などのビルド設定もここで確認できます。

デフォルトでは「Lz 4HC」となっているようですね(ここで変更はできないっぽい?)。


ビルド

今回はビルド対象としてUnityChanを使用します。

UnityChanをビルドに含めるためには、UnityChanのインスペクターの上にあるAddressableにチェックを入れるか、管理ツールにドラッグすればできます。

unitychan.PNG

unitychan_window.PNG

この際、ユーザーはアセットをどこかのグループに所属させることになります。

このグループ単位で、ロード先やAssetBundleのパッキング方法を設定できるようになっています。

もし、設定を行いたい場合は、

buildpath.JPG

上記の画像のようにグループを選択することでそれが可能になります。

また、選択できるパスごと変更したい場合は、

manager.jpg

左上のProfile設定場所からManage Profilesを選択すれば変更できるようになります。

ちなみに、今回の設定は以下の通りです。

名前
パス

ビルド先
Assets/ServerData

ロード先
http://localhost/StandaloneWindows64

これでビルドの準備ができました。

右上の設定アイコンからPlayModeを変更します。

playmode.jpg

このModeの各々の意味としては、

モード
説明

Fast Mode
AssetDabaseからAssetをロードする。ビルドはしない。

Virtual Mode
Fast Modeと挙動は同じだが、速度制限などシミュレート機能がある。

Packed Mode
実際にビルドしてAssetBundleのデータをロードする。実機の動作と同じ。

となっています。

今回はビルドしたいためPacked Modeを選択しています。

これでUnityを実行してみましょう。

先ほど設定したビルド先にファイルが生成されているはずです。

デフォルトのままならAssets/StreamingAssets以下にあると思います。

create_file.JPG

Projectビューに反映されない!という人は一度Unityとは別のソフトに移ってからもう一度Unityに戻ると反映されるかもしれません。

それでも反映されない人は、

using UnityEditor.AddressableAssets;

public static AddressableAssetsUtil
{
[UnityEditor.MenuItem("Addressable/Build")]
public static void Build()
{
// BuildTargetはよしなに設定すること
BuildScript.PrepareRuntimeData(true, false, true, true, false, UnityEditor.BuildTargetGroup.Standalone, UnityEditor.BuildTarget.StandaloneOSX);
}
}

このようなスクリプトを用意して、直接APIを叩きましょう(正直面倒くさい)。

参考:Addressable Assets Systemを完全に理解する


生成されるもの

ServerData

├ catalog_1.hash
├ catalog_1.json
└ remotepackedcontentgroup_assets_all_a1e480f5f16db68498dc14c993eb060e.bundle
StreamingAssets
├ Addressables_catalog.json
└ Addressables_settings.json

ビルドすることでいくつかのファイルが生成されました。

まずはServerData以下を見ていきましょう。


  • remotepack...060e.bundle:言わずもがな、UnityChanのデータが入ったもの

  • catalog_1.json:コンテンツカタログにあたり、管理ツールで設定したアセットの情報が入っている

  • catalog_1.hash:catalog_1.jsonのhash値で、コンテンツカタログの更新を検知する

以上が生成されるファイルの正体です。

ただここで疑問が生じます。

コンテンツカタログはアプリ実行時にResource Locatorを作るためにデシリアライズされるわけですが、どうやってユーザーによって配置場所が様々なcatalog_1.jsonのパスを特定しているのでしょうか?

もしAddressables Assetsのソースコードに直接パスが書かれていれば別ですが、管理ツールでロード先のパスを先に変更できるため、それはありません。

ということは、コンテンツカタログのパスの情報を別にどこかで保存しているということになります。

もうお気づきですね?

そう、その情報を持ったものがStreamingAssets以下に生成されたjsonファイルです。これをAddressable Asset SystemではResourceManagerRuntimeDataと呼んでいて、Addressables_settings.jsonがそれにあたります。

ちなみに、もう一つのAddressables_catalog.jsonは、Play ModeがPacked Modeではないときに代用で使われるコンテンツカタログになります。

まとめると以下のようになります。

ファイル名
ビルド先パス
役割

Addressables_catalog.json
ハードコーディング
代用のコンテンツカタログ

Addressables_settings.json
ハードコーディング
ResourceManagerRuntimeData

catalog_1.hash
管理ツールにて指定
ファイルが更新されたか検知する

catalog_1.json
管理ツールにて指定
コンテンツカタログ

remotepack...060e.bundle
管理ツールにて指定
UnityChanのデータが入っている


ロードされるまで


下準備

次はロードしていきましょう。

まずはロードするファイルを適切な場所に配置します。

今回はロード先をhttp://localhost/StandaloneWindows64としているため、これに沿ってローカルにサーバーを立て、ビルド先に生成されたファイル(今回であればAssets/ServerData以下のファイル)を配置します。

詳細:お手軽なWebサーバーの立て方

files.JPG

次にロードするためのコードを用意します。

今回用いるのは以下のコードです。

public class Sample : MonoBehaviour

{
void Start()
{
// UnityChanをロードして表示する
Addressables.Instantiate<GameObject>("Assets/UnityChan/Prefabs/unitychan.prefab");
}
}

このAddressables.Instantiateですが、基本的にはAddressables.LoadAssetと同じでAssetBundleのダウンロード処理などを内部で勝手にやってくれるものになります。

そうやってロードしたものをInstantiateするまでがこの処理となります。

このコードをScene上の適当なGameObjectに張り付けて、Unityを実行させてみます。


実行直後

上記のコードではStartの中で処理をしているので実行後すぐに呼ばれますが、その処理の前に行われる処理があります。

それがAddressablesクラス内でRuntimeInitializeOnLoadMethod属性がつけられているAddressables.InitializeResourceManagerです。

では、Addressables.InitializeResourceManagerの中では何をやっているのでしょうか?

これは名前から推測できますね。そう、ビルド時に生成したjsonファイルを読み込むなどして、Resource Managerを初期化しています。

また、同時にResource Locatorの登録なども行われています。

具体的には、


  1. Addressables_settings.jsonをデシリアライズしてResourceManagerRuntimeDataを取得

  2. ResourceManagerRuntimeDataを元にResource Managerを初期化

  3. ResourceManagerRuntimeDataからコンテンツカタログのLocationを取得

  4. catalog_1.hashを見てコンテンツカタログの更新が必要ならダウンロードする

  5. catalog_1.jsonをデシリアライズしてResource Locatorを登録する

という処理が行われています。


Instantiateの中

次はAddressables.Instantiateについてです。

Addressables.LoadAssetと同じで、内部にてロード処理が走っていますが、それはResource Managerが"unitychan.prefab"に適したProviderを選択してロードしているのは説明しました。

しかし、実はこれだけだと問題があります。実際に例を挙げてみましょう。

"unitychan.prefab"を読み込む場合、適したProviderはBundledAssetProviderにあたります。

BundledAssetProviderではロード処理(AssetBundle.LoadAssetAsync)が行われています。

この処理自体は何も間違っていません。ただし一つ問題があります。

それはAssetBundleがダウンロードされていないということです。

"unitychan.prefab"をロードするにはbundleファイルのダウンロードが必要、つまりbundleファイルに依存しているので、このままではロードすることができません。

この問題を解決する機能がResource Managerには備わっています。

Addressables6.jpg

いままでResource Managerがロード対象を知るために使っていたLocationですが、この中には依存関係の情報も内包されています。

Providerは依存先がある場合にはそちらの処理を優先し、それが完了次第自分の処理に移るようになっています。

Addressables7.jpg

今回の場合だと、"unitychan.prefab"のLocationは依存先にbundleファイルのLocationを持っています。

bundleファイルをロードするのに適したProviderはAssetBundleProviderにあたり、これは先ほど少し説明した通りダウンロード処理(UnityWebRequestAssetBundle.GetAssetBundle)をやっています。

これによって、先にダウンロード処理を行ってロード処理に移ることができるようになっています。

具体的には、


  1. 先ほど登録したResource Locatorが"unitychan.prefab"というアドレスをLocationに変換する

  2. Resource ManagerにLocationを渡す

  3. Locationから適したProviderを探しロードを依頼する(BundledAssetProvider)

  4. ロードする前にLocationから依存先があるか調べる

  5. bundleファイルに依存していたので、先にそちらのProvider処理を行う(AssetBundleProvider)

  6. ロードして得られたGameObjectをInstantiateする

という処理が行われています。


おまけ

Providerはロード処理を行う前に依存先があるか見ていますが、実はもう一つ見るものがあります。

それはLocationが持つDataというobjectパラメータです。Providerにもっと情報を渡したい場合に使われるようです。

例えば、bundleファイルのLocationのDataにはCacheInfoというものが含まれています。

これは名前からわかる通りダウンロード時のキャッシュ情報にあたり、AssetBundleProviderではその情報を元にデータをキャッシュするように作られています。


AssetBundleProvider.cs

var cacheInfo = (Context as IResourceLocation).Data as CacheInfo;

if(cacheInfo != null && !string.IsNullOrEmpty(cacheInfo.m_hash) && cacheInfo.m_crc != 0)
m_requestOperation = UnityWebRequestAssetBundle.GetAssetBundle(path, Hash128.Parse(cacheInfo.m_hash), cacheInfo.m_crc).SendWebRequest();
else
m_requestOperation = UnityWebRequestAssetBundle.GetAssetBundle(path).SendWebRequest();



まとめ

以上がAddressable Asset Systemの仕組みとなります。

とてもシンプルな機能で、今後のデータの主軸になっていくと思うので、Unity 2018.2を触っている方は是非使ってみてください。

(あとはたまにおかしな挙動をするのさえどうにかなれば最高なのですが...)

感想、意見などお待ちしております。

https://twitter.com/_ykringo


参考