はじめに
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"
}
}
※Addressable Asset Systemのバージョンによって内容が変わっている可能性があります
Addressable Asset Systemの仕組み
Resource Managerについて
Addressable Asset Systemは2つのシステムから成り立っていました。Addressable AssetsとResource Managerです。
このうち、ロードを担当するのがResource Managerです。
どうロードするか見ていきましょう。
まず、Resource Managerに対してロード処理の依頼をするには**Resource Location(以下Location)**を渡す必要があります。
Locationとは、ロードするアセットに関する情報(配置場所や適したProvider情報、依存関係など)が入っているものです。
次にResource Managerは、受け取ったLocationを元にそのアセットに適したProviderを探して、見つかり次第ロードを依頼します。
このProviderというものが実際にロード処理を行うものになります。
例えば、AssetBundleProviderというものがありますが、この中ではダウンロード処理が行われていたりしています。
そして最後にProviderはロードの処理をAsyncOperationとして返し、それが完了まで待つ、というのがロードの一連の流れになります。
Addressable Assetsについて
さて、ロードの仕組みはわかりましたが、このままではユーザー側からLocationを渡す必要があります。
Locationは少し複雑なので、それではシンプルさからは遠ざかってしまいます。そこで登場するのがもう一方のAddressable Assetsです。
Addressable Assetsは上記の画像のように、ユーザーとResource Managerの間に入ってアドレス(文字列)からLocationへの変換を行います。
これによって、ユーザーはアドレスを渡すだけでロードすることができるようになります。
このアドレスからLocationへの変換の役割を担っているのがResource Locatorです。
Resource Locatorは自身の持つアセットとアドレスの対応表を元に変換処理を行っています。この対応表はコンテンツカタログと呼ばれ、ビルド時にjsonファイルとして生成されるようになっています。
アプリ実行時にはそのjsonファイルがデシリアライズされ、得られたデータを元にResource Locatorが作られます。
その後、Resource Locatorはユーザーにとって窓口となるAddressable Assets(Addressablesクラス)に登録され、ロード依頼が来るたびに中で変換処理を行うことになります。
ビルドされるまで
ここまででAddressable Asset Systemの大まかな仕組みは理解できたと思います。
次は、実際にビルド&ロードして、その理解をより深めていきましょう。
導入
まずは、
Window -> Asset Management -> Addressable Assets
で管理用のWindowを開きます。
ここから、Create Addressables Settingsをクリックすると設定ファイルを作成することができます。
そうすると、Assets/AddressableAssetsData以下に設定ファイルが作成されます。
ここでは管理ツールで設定できるグループを見ることができるほか、圧縮などのビルド設定もここで確認できます。
デフォルトでは「Lz 4HC」となっているようですね(ここで変更はできないっぽい?)。
ビルド
今回はビルド対象としてUnityChanを使用します。
UnityChanをビルドに含めるためには、UnityChanのインスペクターの上にあるAddressableにチェックを入れるか、管理ツールにドラッグすればできます。
この際、ユーザーはアセットをどこかのグループに所属させることになります。
このグループ単位で、ロード先やAssetBundleのパッキング方法を設定できるようになっています。
もし、設定を行いたい場合は、
上記の画像のようにグループを選択することでそれが可能になります。
また、選択できるパスごと変更したい場合は、
左上のProfile設定場所からManage Profilesを選択すれば変更できるようになります。
ちなみに、今回の設定は以下の通りです。
名前 | パス |
---|---|
ビルド先 | Assets/ServerData |
ロード先 | http://localhost/StandaloneWindows64 |
これでビルドの準備ができました。
右上の設定アイコンからPlayModeを変更します。
このModeの各々の意味としては、
モード | 説明 |
---|---|
Fast Mode | AssetDabaseからAssetをロードする。ビルドはしない。 |
Virtual Mode | Fast Modeと挙動は同じだが、速度制限などシミュレート機能がある。 |
Packed Mode | 実際にビルドしてAssetBundleのデータをロードする。実機の動作と同じ。 |
となっています。
今回はビルドしたいためPacked Modeを選択しています。
これでUnityを実行してみましょう。
先ほど設定したビルド先にファイルが生成されているはずです。
デフォルトのままならAssets/StreamingAssets以下にあると思います。
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以下のファイル)を配置します。
次にロードするためのコードを用意します。
今回用いるのは以下のコードです。
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の登録なども行われています。
具体的には、
- Addressables_settings.jsonをデシリアライズしてResourceManagerRuntimeDataを取得
- ResourceManagerRuntimeDataを元にResource Managerを初期化
- ResourceManagerRuntimeDataからコンテンツカタログのLocationを取得
- catalog_1.hashを見てコンテンツカタログの更新が必要ならダウンロードする
- 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には備わっています。
いままでResource Managerがロード対象を知るために使っていたLocationですが、この中には依存関係の情報も内包されています。
Providerは依存先がある場合にはそちらの処理を優先し、それが完了次第自分の処理に移るようになっています。
今回の場合だと、"unitychan.prefab"のLocationは依存先にbundleファイルのLocationを持っています。
bundleファイルをロードするのに適したProviderはAssetBundleProviderにあたり、これは先ほど少し説明した通りダウンロード処理(UnityWebRequestAssetBundle.GetAssetBundle
)をやっています。
これによって、先にダウンロード処理を行ってロード処理に移ることができるようになっています。
具体的には、
- 先ほど登録したResource Locatorが"unitychan.prefab"というアドレスをLocationに変換する
- Resource ManagerにLocationを渡す
- Locationから適したProviderを探しロードを依頼する(BundledAssetProvider)
- ロードする前にLocationから依存先があるか調べる
- bundleファイルに依存していたので、先にそちらのProvider処理を行う(AssetBundleProvider)
- ロードして得られたGameObjectをInstantiateする
という処理が行われています。
おまけ
Providerはロード処理を行う前に依存先があるか見ていますが、実はもう一つ見るものがあります。
それはLocationが持つDataというobjectパラメータです。Providerにもっと情報を渡したい場合に使われるようです。
例えば、bundleファイルのLocationのDataにはCacheInfoというものが含まれています。
これは名前からわかる通りダウンロード時のキャッシュ情報にあたり、AssetBundleProviderではその情報を元にデータをキャッシュするように作られています。
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
参考
- 【Unite Tokyo 2018】そろそろ楽がしたい!新アセットバンドルワークフロー&リソースマネージャー詳細解説
- UnityChan © Unity Technologies Japan/UCL