Real World Addressables(WIP)


概要

AddressableAssetsSystem(AAS)の機構を使いたいので、まず基本的にどんな使い勝手なのか、また、こんなことがしたい時どこをいじればいいのか、などを書いていく。

根本的にAAS自体がWIPなのだが、バージョンが変わってもどこを変えてどうすればいいのか、はあんま変わらんと思って書いている。(この行を書いたときはかなり楽天的だった。)



version

ver 0.1.2で書いてる。2018.2にそのまま入れると、AddressablesのGUIを開いたときにGUI生成できないエラーが出る。

で、0.0.27でいっぺんaddressableAssetのデータを作成 -> 0.1.2にアップデートすると、問題なく動く。WIP。



基本的な使い方

日本語だとここがわかりやすい

「Addressable Assets Systemを完全に理解する」

https://qiita.com/k7a/items/b4fd298bcb64dc968ad1


使用手順を超簡単に書くと、

1.Unity Editor上でカタログを作成する

2.ゲーム起動時に自動的にカタログを取得してくるので、ゲーム中でLoadAsset<T>とかやると1で作成したリソースが取得できる。

というような感じになる。



AASの機構の概念

UnityのAssetBundle(AB)は複数のAssetを一個のファイルに固めてダウンロードしやすくしたようなものなのだけれど、

AddressablesはABのビルド処理やロード処理を行う機構となっている。


ABの操作に関してかなり完璧に隠蔽できているため、ABについての専門的な知識がなくても扱えるようになっている。


つまり開発者はABについて理解するかわりに、AASについて理解をすれば、AssetBundleでできることを簡単に扱えるようになったと言えるような言えないような。

まあ理解する対象が変わっただけなんだけど、根底にあるのはABを生成、ロードする機構なので、AASの構造に不都合があったらもっと理解して書き換えていけばいいのだ、とかそんな感じ。


このへんは、HTTPを理解するためにTCPを理解する必要はない、みたいな関係性に似ていると思う(適当)



実装、構成、モード

AASは表層にあるAddressablesと、根底にあるResourceManagerの2段構成になっている。


基本的にはUnityEditor上から、コンパイル時に

「どんな外部リソースがあるか」

「外部リソースをどんな名前でロード可能にするか」

「外部リソースをどんなキーワードでグループ化するか」

「その外部リソースをどこからロードするか」


などの情報を集めておき、それらを入れた「カタログ」と呼ばれるデータベース = ファイルを生成する。


カタログには上記の情報がセットされており、ゲーム起動時にAASが自動的にカタログの取得を行う



外部リソースをどこからロードするか、については入り組んでいて、

AssetをAASのウィンドウに登録すると、Assetは必ずどこか一つの Address Groupというものに含まれるようになる。

このAddress Groupは、含まれるAssetをどのように扱うか、というのを決めるための集合で、次のような設定を行うことができる。

・Groupに含まれる全てのAssetをどんな感じにAssetBundle化( = 1ファイル化)するか

・どこから取得するか(Local or Remote)

・ビルドしたABをどこに吐き出すか


つまり自由なグループ単位でAssetのロード元を指定したりできるんだけど、

えーーっと、まあ大規模になったら手でやるのは無理だと思うんで観賞用かなあと。AssetGraphとかでいじれるようになるのを期待する。


話を戻して、起動時のカタログ取得以降は、AASは開発者のリソース取得リクエストに対して、


リクエストをキーとしてカタログ検索

-> 対象をどうロードするかなどの情報を取得

-> 実際にリソースを取得して開発者に返す


などの処理を行なっている。



先に書いとく、AASの超WIPなところ

多分この項が一番有用な話だと思う。なんらかの進捗があると安心して使えるようになる。



カタログ取得(自動)(強制)(毎回)に失敗すると死ぬ

リソースをWebから取得する設定の場合(というかAASの使用例の99%くらいそうだと思うが)、起動時にカタログをサーバから取得する。

この際、ひたすら早いタイミングでカタログ取得の通信が走り、その通信に失敗した場合、AASの機構は死ぬ。


失敗したらどうなる? 機構の初期化が途中で終わる。おま、、WIP、、、


このカタログ取得通信を任意のタイミングに変更することができるか?と言われればNoで、これはAASの起動 -> 通信機構にRuntimeInitializeOnLoadMethodアトリビュートが付いているため。

通信環境チェックやリトライすらないっぽいのでヤバい。Playerの起動時の不安定さもあいまって、エディタ + Packedプレイモードだとだいたい50%くらいの確率で失敗する。


また、これは大変ダサいが、エラーになってしまった後で手動でAASを再起動できるかというとNoで、AASのInitializeResourceManagerはprivate staticで定義されている。はい。



おまけで、カタログ取得のベースurlは変更不可能なApplication.dataPath下に置かれるため、ゲームのコンパイル時に完全固定となる。

これは例えば、ゲームの起動時にユーザーのリージョンを見て、一番近いリージョンのCDNから落とさせるためにurlを変更する、みたいな芸当ができないことを意味する。


あとリソース取得時に認証を云々も無理。速すぎる。ヒールアンドトゥの兄貴かよ。


今後このへんも可換になってほしい、、、

Unity IAPがかなりまともなハンドリングを提供してくれているので、最終的には期待している(あと大人なのでコントリビュートすればいいと思っている)



エラーの原因がわからない

WIPなのであれなんだけど、例えばLoadAsset<T> (string assetName) は、UnityではおなじみなasyncOperationを返してくる。

これが、例えばネットワークが死んでて失敗したり、指定したassetNameが違ったりして失敗するんだけど、

現在の段階では「成功か失敗かはわかる」が、「なんで失敗したのか」がわからない。


ざっくりいうとエラー情報が一切ない。


LoadAsset系の処理の完了を待つには、Completedイベントが着火するか、asyncOpそれ自体をisDoneになるまで回して、Statusを見ればいい。

Statusは、成功時にはSucceeded、失敗時にはFailedというenumを返して来る。


IsDoneは、LoadAssetが成功しても失敗してもtrueになる。

OperationExceptionはどんな失敗でもnullのまま。お前何。

IsValidは、成功しても失敗してもtrueが返ってくる。お前も何。


Status以外の他のパラメータは全く当てにならない。WIPなので、今後ちゃんと何か入るかもしれないが、全体のエラーを網羅したenumとかがないと大変な目に合うと思う。


エラーの内容がわからないので、プレイヤー = ゲームを遊ぶ人に対してどんな情報を与えればいいかがさっぱりわからん。

というかエラーに対してどんな処理走らせればいいかもわからんのよな。



ありえる厄介なエラーとしては、

・プレイヤーが平和にゲームを遊んでいるタイミングで

・開発者がCDN上のデータを更新して

・古いカタログ情報を元にプレイヤーがリソースにアクセスしてきて

・古いカタログ情報だと最新のリソースが取得できずに死ぬ

みたいなのとかはよくある話で。

こういうケースだと、「持っているカタログが古いです。最新のカタログを取得中..」とか表示を出して、プレイヤーを待たせている間にカタログを取得、再度落とし直す、とかをすればいいんだけど、

そのためには割と精緻なエラー情報が必要で。


例えば今使っているAssetに更新があったりなかったりとかでどこまで戻ればいいかが変わるし、

今は大事な画面なんでプレイヤーをこのまま遊ばせたい、戻らせたくない、みたいな選択だって立派に必要な選択肢だ。


エラー判定について一応確認してみたけど、Addressablesのテストの中だと、StatusではなくResultがnullではなければOK、というような判定をしていた。

そのうちOperationExceptionがマトモに入るんだと思う。


で、まあ、それに沿ったハンドリングをすることになるんだけど。



WIPのまとめとこのあとの記事の前提

以上を踏まえ、これ以降の記事は

・なんかいい感じのタイミングでカタログ取得が済んでいる

・カタログの内容、Web上に置いてあるリソースについては一点の非もなく、いつでもアクセスできる

・LoadAsset書く側が、Loadしたいアセットの名前をタイポするなどのミスを100%しない

という前提に立つものとする。



現状のAASはかなりセンシティブで、この前提を守らないと動かない。WIPだし。

まあダメだったらコントリビュートすればいいので!



どこをどう交換すれば何ができるようになるのか

起動タイミングのいろいろがAASの初期化コードにハードコーディングされてることは置いといて、

次のようなことをしたい場合、どこをどう変えれば(可換なプロバイダを作れば)いいのかをまとめていく。



(まだひみつ)(というかエラー処理考えると将来にわたって変わらないはずがない確信が得られたのでとてもつらい)




以下おまけ


シーンとかってLoadできるの?

できる。モードも指定できる。

var sceneLoading = Addressables.LoadScene("NextScene", LoadSceneMode.Additive);

sceneLoading.Completed += op =>

{

    Debug.Log("op is succeeded?:" + op.Status);

};



AssetをダウンロードするURLってどこに入ってんの?

PackedモードであればStreamingAssetsにセッティング情報を保持している。

それ以外のモードであれば、"dataPath/Library/Addressables_settings_" + mode + ".json" にセッティング情報を保持している。


これはカタログのurlも同じで、そちらは "dataPath/Library/Addressables_catalog_" + mode + ".json" に保存されている。


この部分はreadonlyなんで、アプリケーションを更新しないと更新されなそう。

データの更新は実機ビルド時、プレイボタンを押した時の2つ。


動的に書き換える方法はあるんだろうか。

-> dataPathはreadonlyなんで、リビルド以外なさそう。UnityEditorでAddressable Assetの設定を行う時点で覚悟を決めるか、サーバ側でURLを捻じ曲げるか。



LoadAssetとかのCompletedが着火するタイミングはLateUpdate

Completedイベントが着火するのはLateUpdateに固定されている。知らないと地味にハマりそう。


これは、asyncOpの監視をLateUpdateメソッドよりも実行順が前なメソッド内(Updateとか)に置いた場合、

その発火はCompletedの発生よりも1f遅れる = 次のフレームになる、ということを意味している。


例えば次のようなコードの実行順になる。

IEnumerator Start()

{

    // この時点で0 frame

    var prefabLoad = Addressables.LoadAsset<Material>("Mat");

    prefabLoad.Completed += op =>

    {

        // ここが発生するのが0 frame(のLateUpdate)

    };


    while (!prefabLoad.IsDone)

    {

        yield return null;

    }


    // ここにこれるのが1 frame !

}


太字の部分だけ見るとわかりやすいと思う。

Completedが着火するのはLateUpdateなのだ。なので、処理が十分に早いと同じフレームの別メソッド呼び出しで完了する可能性がある。


asyncOpを監視するスクリプトのexecution orderを0(デフォルト)にしてLateUpdate関数内でasyncOpを監視すると、Completedの着火よりも速くイベントを認識することは可能。

まあだから何って感じなんだけど。



LateUpdateで実行すると同じフレームで返ってくるんだろうか?

-> 次のフレームのLateUpdateで返ってきた。必ず非同期。



こんなことしたい、の書き方をテストから探す

ドキュメント見ても、「実際こういうのどう書いたらいいんだろう?」みたいな疑問は出てくる。そりゃやりたいことが全部載ってるわけじゃないからね。

そういう時はテストを見てみるといいかもしれない。

UnityのPackagesに入っているものには見た感じすべてテストが含まれており、それらのテストをUnity上で動かすのは割と簡単にできる。

スクリーンショット 2018-07-13 14.13.05.png


1. UnityのProjectウィンドウ > Packages > テストを走らせたいパッケージを選択 > Reveal in Finder 

2. 表示されたフォルダをデスクトップとかにコピー

3. 新規プロジェクトを作成

4. 新規プロジェクトのAssetsフォルダに、2でコピーしたフォルダを放り込む

5. プロジェクトを開くと、テストがあるパッケージであればそのテストがTest Runnerに表示される。


5の時点でコンパイルエラーが出る場合、おそらく持ってきたパッケージ自体が依存している別パッケージがある。

2で開いたフォルダからなんとなく「これかな?」って持ってくるとうまく行ったりする。


新規プロジェクトを作るのはオススメ。なぜオススメかというと、へんなファイルがあるところにパッケージを持っていくとものすごいカオスが出来上がってしまうため。



さて、ここまでしたパッケージのテストが通るのか?というと、前提条件が異なる場合はもちろんエラーが出るので動かないケースも多いが、

自分が見た範囲だと大体動いている気がする。特にResourceManagerのテストは凄まじい。


扱う対象のAssetをこの一件のテストのためだけにコード経由でimportし、テストが終わる頃にはUnityのAssetDatabaseから消す、ということをやっている。

ぶっちゃけすごい。こういうテストすごく大事なので参考にしたい。