1. まとめトップ

UnityのAssetBundleをじっくり理解する手順をまとめてみた。【アセットバンドル】

色んなサイトみながら、適当にAssetBundleを使おうとしたけど、一向にできない。ので、1つ1つひも解いて、行く事にした。今回は、学校の教科書式に進めて行こうと思う。そう。少しづつ例題を作って、一個一個検証していくのだ。

更新日: 2014年02月03日

GongGongさん

  • このまとめをはてなブックマークに追加
6 お気に入り 31485 view
お気に入り追加

色々やってみましたが、アプリ内においていいPrefabは結局当面Resourcesで対応することにしました。

同期で読み込むにはCreateFromFileを使うしかなく、それは非圧縮でないとダメだったので。

まず最初にいいたい。絶対上手くいくはずなのに、(今まで上手く言ってたのに)急にできなくなったら、Unityに原因がある可能性がある。まずは再起動を試みよう。これはマジで重要。ハマったら、時間が溶ける。

まず、ざっくり理解したプロセスと観点を書いてみる。この内容は最初に書いたもので、後からの考察で色々変わってる。。

【プロセス】
①アセットバンドル(A)をビルドする。
②アセットバンドル(A)をダウンロードする。
③アセットバンドル(A)をロードする。
④アセットバンドルからアセット(B)をロードする。
⑤アセットのインスタンス(C)を生成する。
⑥アセットバンドル(A)をアンロードする。
(アンロードされたアセットは他のアセットが依存できないので注意)
⑦インスタンス(C)を破棄する。
⑧アセット(B)をアンロードする。

【AssetBundleの配置場所】
①サーバ
②StreemingAssetフォルダ内

【AssetBundleのサイズを最小にする】
①他に依存しない(参照するアセットをAssetBundleに全て含める。)
②アプリ内スクリプトや基本コンポーネント等に依存する。
③別のAssetBundleにも依存する。


【プラットフォーム毎の対応】
①Editor(いちいちアセットバンドルみてると、テストしにくいから、何とかしよう。)
②Android
③IOS


【アセットバンドルのロード方法】
①キャッシュを使う。「WWW.LoadFromCacheOrDownload」
(注:150日削除かつキャッシュの削除ができない。)
②1度きり「WWW("http://..."),WWW("file://...")」
③ローカルに保存して、キャッシュを使わない。「AssetBundle.CreateFromFile,AssetBundle.CreateFromMemory」

補足1:アセットバンドルのまとめ方
①圧縮or無圧縮
②個別or大量

補足2
①個別同期ロード
②個別非同期ロード
③一括同期ロード


【ビルドの効率化】
①自動化
②フォルダ構造
③ビルド時にプロパティの調整・・・僕は使わないので割愛
(脇道:アセットインポート時にプロパティの調整)


【バージョンアップ時の対応】・・・僕は使わないので割愛
①URLのキャッシュ
②互換性

【その他】・・・僕は使わないので割愛
①バイナリデータを使うには。
②アセットバンドルにスクリプトを含める。(IOSで不可なので、割愛)

【入門①】とりあえず、サーバで使ってみよう。

最初に色んな事書いたのでうわ、ムズそうって感じだよね。
僕みたいに最初から全部一気に理解しようとするとつまづくので、
とりあえず、一番シンプルな奴をユニティー公式のドキュメントをみながらやってみよう。
ちなみに、設置するサーバはドロップボックスのPublicフォルダを使っているよ。
右グリックして、公開URLをゲットできるので、楽ちんだね。

まずは、アセットバンドルのビルドだ。
http://docs-jp.unity3d.com/Documentation/ScriptReference/BuildPipeline.BuildAssetBundle.html
のCsをEditorフォルダ内に作ってみよう。

2カ所、無駄な改行が入ってしまうので、コンパイルが通らないよね。
ここ MenuItem("Assets/Build AssetBundle From Selection - Track dependencies")]
せっせと、1行にまとめてみよう。

作ったら早速アセットバンドルを作ってみよう。
適当にCube作ってPrefabにして、そいつを右クリック
「Assets/Build AssetBundle From Selection - Track dependencies」ボタンを押す。

ファイルの保存先を聞かれるので、ドロップボックスのPublicフォルダ内に保存しよう。
ファイル名は何でもいいんだけど、「Test1」とでもしておこう。

Test1.unity3dがPublicフォルダ内に作成されたかな?

さて、さっき配置したアセットバンドルを使ってみよう。
適当にスクリプトを作って、(Test1Loaderとでもしておこう。)

https://dl.dropboxusercontent.com/u/67049135/Test1.unity3d
のurlをさっき作ったファイルの公開Link先の文字列に変更するだけだ。

これだけだと、オブジェクトが生成されず分かりにくいので、
GameObject obj = request.asset as GameObject;
Instantiate(obj);

にしてみよう。

あ、このスクリプトをシーン内のオブジェクトに張るのを忘れないようにね。

上手く言ったかな?僕は上手く行かなかったよ。何度も試してみたけどね。絶望の縁だ。
そんな時救世主が現れた。
http://docs-jp.unity3d.com/Documentation/Manual/DownloadingAssetBundles.html

のページだ。
ようは、
AssetBundleRequest request = bundle.LoadAsync ("myObject", typeof(GameObject));

myObjectを実際のPrefabの名前(Cube)にしたら動くようになった。
(名前を指定しない時は、bundle.mainAsset で取る事もできるみたい。)

あと、冒頭に
while (!Caching.ready) {
yield return null;
}

をつけた方がいいのかもしれない。(つけなくてもEditor上行けるからよく分からないけど。)

ようは、横着して、
「アセットバンドルからのオブジェクトをロード、アンロード」
だけみてるとハマる可能性がある。
「アセットバンドルのダウンロード」

もしっかりみようね。と。

入門としては、個々まで。
次から、色んなパターンを試して行こう。

【入門②】 StreemingAssetに配置してみよう。

いや、俺のゲームオフラインで動かしたいんだよね。通信中で止まるとか、あり得ない。

そんな、あなたはローカルにアセットバンドルをおかないとね。
まずは、
Assetフォルダ直下(Projectビューの直下)にStreamingAssetsというフォルダを作成し、
その中にAssetBundleを入れてみよう。

さて、呼び出し先のURLをどうするかが問題だけど、

Editor上はこれ Application.streamingAssetsPath + "/" + fileName;

プラットフォーム毎に切り替える
http://docs-jp.unity3d.com/Documentation/Manual/StreamingAssets.html

必要がありそうだ。

さて、やってみよう。

そしてハマる俺。
むっちゃハマった結果、StreamingAssetsフォルダの名前が間違ってたことが発覚。ショックすぎる。
ついでにStreemingAssetにフォルダを作って、その下に配置しても、
パスにきちんと記載すれば、動く事も確かめておこう。

【基本①】AssetBundleのサイズをできるだけ小さくしよう。

さて、まずは依存するって言うのがどういう事かを理解する実験をやって行こう。

適当な画像を表示するSpriteをPrefab化して、画像に依存しているPrefabを作る。

そしてビルドだが、ここで2種類作ってみる。
Track Dependenceで作ったものと、
No Dependency trackingで作ったものだ。

そして、全然上手く行かない。
No Dependency trackingの挙動がTrack Dependenceと変わらない。

個人的な結論として、

依存性を排除したアセットバンドルを作には
BuildPipeline.PushAssetDependencies ();を利用して、
他のアセットバンドルに依存するアセットバンドルを作るしか無いようだ。

この際、DeterministicAssetBundleオプションをつけないと、依存しているアセットバンドルのみ更新した際にインデックスがズレて上手く動かなくなるっぽい。

また、スクリプトについては、そもそもProjectビューに突っ込まれたスクリプトは
全て実行時に読み込まれるようだ。なので、スクリプトについて、同一プロジェクトで行っている限りあまり気にする必要はなさそうだ。

BuildPipeline.PushAssetDependencies ()を利用した方法については、割と難しいので、別の章で記載する。

【現段階で把握しているバイナリに含まれるアセット】
①Scene in buildで指定したシーンが静的に依存しているアセット
②Resources/StreamingAssets/Pluginフォルダに配置されたアセット(とそのアセットが静的に依存してるアセット)
③Projectビュー内(Assetフォルダ内)の全てのスクリプト


【途中ドハマりしたこと】
アセットバンドル作った時、前と同じ名前のアセットバンドル名を使った為に、キャッシュが使われて、更新できてない事に気づくのに時間がかかった。versionを変えるか名前を変える必要がある。これで、本当よくつまづく。ビルド中はversionをオートインクリメント

スプライトを含める事はできない。

【ここで1つ手抜き方法:ExportAssetBundlesのSaveFilePanelの第2匹数に保存したいフォルダのパスを入れると一々設定しなくていいから楽だよ。】

【基本②】 Android/IOS/Editorで使えるようにしよう。

まずはAndroidだ。
今まで作ったプロジェクトをAndroid向けにビルドして、端末でみてみよう。
動いたかな?

僕は動かなかった。

2点修正する必要があるようだ。
①StreamingAssetsを使っている場合、URLを修正する必要がある。(サーバ使ってる場合は不要だ。)
WWW.LoadFromCacheOrDownload ("jar:file://" + Application.dataPath + "!/assets"+"/name.unity3d", version);

②BuildAssetBundleのビルドターゲットをBuildTarget.Androidに指定してあげる必要がある。
デフォルト引数ではwebplayerが入ってるので注意が必要だ。

次にEditorでの最適化だ。
UnityEditorで実行する時に、一々アセットバンドルを作ってからじゃないと実行できないって言うのは、もの凄い不便だ。
Prefabを変更するたびにアセットバンドルを作らないといけない。

そんな事やってたら、日が暮れる。

という事で、Editorで実行する時は、
AssetBundleを見に行くのではなく、プレハブを直接見に行くようにする。

具体的にはこんな感じだ。
#if UNITY_EDITOR
Obj = Resources.LoadAssetAtPath("Assets/" + path, typeof(T));
#else

さあ、できたかな?
Prefabの値を直接変更して、アセットバンドルを一々作らなくても変更が反映されてる事を確認しよう。

うん。普通にできたっぽい。これはかなり重要だね。
一点注意点は、pathに拡張子を含めないといけない事ぐらいかな。

【基本③】アセットバンドルからのロード方法

さて、依存性をゴリゴリやる前に、
アセットバンドルのロード方法について、理解しよう。

関連プロセスはここだ。
②アセットバンドル(A)をダウンロードする。
③アセットバンドル(A)をロードする。
④アセットバンドルからアセット(B)をロードする。

まず、
前提として、
アセットバンドル化する単位についての論点を考察してみよう。

個別:1つのアセットを1つのアセットバンドルに
大量:複数のアセットを1つのアセットバンドルに
全部:全てのアセットを1つのアセットバンドルに

が考えられる。

多くのアセットバンドルをまとるメリットは、
ロード速度の向上であり、アセットバンドル自体のメモリ効率も上昇、コンテンツの重複問題も少ない、合計ファイルサイズも少なくなる等メリットがある。

がしかし、特定のアセットをロードするために、無駄なアセットまでロードしてしまうという
決定的なデメリットがあると思う。

理想的には、同時に使うアセットの単位でアセットバンドル化して行くのが理想だろうが、
正直、どのアセットを同時に使うかを管理するのは大変そうだ。
という事で、
今回僕が採用する方針は、
「Prefab毎にアセットバンドル化していく。」というものだ。
ただし、色んなPrefabが共通で使うアセットがある。
こいつを管理するのはめんどいので、1つのCommonプレハブから参照を持たせて、
全てのAssetBundleがそのCommonプレハブのアセットバンドルに依存する形をとる。

さらに、Prefab化しない素材達がいる。正直こいつらはAssetBundleにせずにResoursesで管理したいのだが、SpritePackerがResoursesフォルダ内にあるTextureをパックしてくれず、ビルド時に毎回、一回フォルダから出して、パックして戻すという、クソだるいことをしなければいけないので、AssetBundle化したい。けど、今回はここまで手が回りそうにないので、後回しだ。

たぶんではあるが、アセットビルド時にPackしてくれるらしいので、
正直PackageTag毎にアセットバンドルを作るのが唯一解な気がする。

うん。以外と複雑だね。

【さて②ダウンロード・ロードについて、】
①サーバからどこにも保存せつロード(一度きり)
②サーバから直接キャッシュ
③サーバからローカルに保存してキャッシュ
④サーバからローカルに保存して直接ロード
⑤アプリ内から直接ロード
⑥アプリ内からキャッシュ

という選択師があると思う。
どれがいいか、状況によるし、それぞれメリット・デメリットがある。

①サーバからどこにも保存せつロード(一度きり)
本当に一回しか使わない動的なデータならこれでいいかもしれないけど、通信エラー処理が面倒な気がするから僕は使わない。

②サーバから直接キャッシュ
について、これは、キャッシュが150日間しか、持たないという欠点があるらしい。(150日もユーザは使ってくれないと思うけど)もしもの為の時に、キャッシュが無い場合の処理の実装が必要になってくる。1つならまだしも大量のアセットバンドルを使う場合、色々な場所で、それを気にしなければいけないので、僕が使う手段としては現実的でない。

③サーバからローカルに保存してキャッシュ・⑥アプリからキャッシュ
ローカルに圧縮が乗る+キャッシュに無圧縮が乗るというストレージ上、とても非常な状況になる。
が、ローカルorアプリ内には圧縮アセットバンドルをおける。かつロード速度、ロード時のメモリ消費は効率的だ。

容量的にサーバから追加or動的に追加するコンテンツはサーバにおき、
そうでない場合はアプリ内のAssetStreamに配置する。

最初にローカルに必要な素材が全て保存されているかどうかを、起動時にチェックすれば、それ以降、ネットワークエラーをチェックする心配はなくなるし、チュートリアル以外に必要な素材をサーバにおいて、チュートリアル中にダウンロードすれば、シンプルかつユーザフレンドリーに構築できるので無いかと思う。

これが現状ベストではないかと考えている。
ただ1点、注意点としては、キャッシュは意図的に消す事ができない。ということだ。
つまり、バージョンアップに伴い、頻繁に修正するようなデータをキャッシュしてしまうと非常に非効率的だし、バージョンアップ毎にLoadFromCacheOrDownloadのversion値をきちんと管理する必要が出てくる。

僕は、キャッシュなんて、ガンガン削除して、リロードされてもいいと割り切っているので、
バージョンアップで修正する可能性があるアセットは全てアプリのバージョンでキャッシュのバージョンを管理し、極めて修正する可能性が低いアセットのみ、固定値でキャッシュのバージョンを管理するのがいい気がした。

ということで、使うのはWWW.LoadFromCacheOrDownload("file://~~",version)のみになりそうだ。
他のメソッドの説明は、高橋さんの動画で勉強してほしい。

そうでない場合は、
④サーバからローカルに保存して直接ロード
⑤アプリ内から直接ロード
を検討した方がいいだろう。

【圧縮or無圧縮について】
ローカルに保存して直接ロードの場合のみ、無圧縮にした方がいいんだと思うけど、(CreateFromFileを使うため)
上記より、僕は常にキャッシュを使うので、常に圧縮でいいと思う。

さて、長くなってきたか、次で最後だ。
アセットバンドルからアセットのロードについて

①個別同期ロード
②個別非同期ロード
③一括同期ロード

止まっていいなら、同期であり、常に一緒に使うものだけがまとまっているはずなので、通常は一括同期ロードだろう。管理が簡単だしね。

容量の大きいデータでどうしても止まっちゃ嫌だって時だけ、非同期ロードだろう。
(よっぽどで無い限り、とりあえず同期ロードで実装して、パフォーマンスチューニング時に非同期に変えて行けばいいと思う。)
なんで非同期一括ロードが無いのかは、謎だ。

さて、例題を作りたい所だけ、忙し過ぎて心が折れたので、割愛。自分のプロジェクトでやってみよう。

【応用①】俺の考えた簡単にできる最強のビルド自動化とお手軽ロード

さて、とうとう最後だ。長かった。1日で学習終えるつもりが4日ぐらいかかった。(他の事やってたのが大きいけど。)

今まで、ちびちびやり方をみてきたが、全てのアセットバンドルでいちいちこんな事してたら、日が暮れる。一気に最高の形で簡単に管理できる俺の考えた最強のやり方を考える必要がある。

正直今から考えるので、期待して待っててくれ。

Prefabについてまず達成したい事を書いとく。動的に読み込む画像は最悪、今回は対応しなくていいので、後回し。

①既存のフォルダ構成は変えたくない。(Prefab毎にフォルダを分けて必要な素材もその中に入れてる。共通で使う素材のみ、共有フォルダに)
②アセットバンドルは全部StreamingAssets内に圧縮でいれる。(画像とかサーバ経由で持ってきたいけど、まだ、50M以内でいけるので、次回に回す。)
③一発でIOS版とAndroidをビルド バイナリ出力直前に必要ないフォルダを外だしする事で、対応
④Editor上ではPrefab本体を参照する。

サーバ使わない要件だからシンプルだね。(アセットバンドルの使用に伴い、容量が肥大化しない事を祈るばかり。)

前提条件
Prefabの名前は1意

俺のやり方

ビルド時
まず、根本となるシーンをアセットバンドル化し、
後のアセットバンドルは全てそれに依存させる。

と思ったけど、そもそもシーンはスクリプト以外の独自アセットの参照を持ってるゲームオブジェクトがあんまり無かったので、
やっぱり共通素材の参照を保持したプレハブを作る事にした。

つまりこういう方針で行く。
①共通素材プレハブのビルド
②プロジェクトフォルダを走査し、全てのPrefabを割り出し、1つ1つStreamingAssets/AndroidとStreamingAssets/Iosに①に依存した形でPrefab名をバンドル名にして、放り込んで行く。
③ロードするときはLoadFromCacheOrDownloadを使い、versionは常にアプリのバージョンを使用する事で、最新のアセットをキャッシュさせる。ライブラリの引数はPrefab名のみ
④EditorではPrefab名からPrefabを直接インスタンス化でできるように頑張る。

サンプルプロジェクトで試した結果、

超注意事項が1つ

③について、依存先のアセットはunloadしてはいけない。これはかなり重要だ。

あと②の仕方が分からなかったので、PrefabControllerというスクリプトを貼付けて、
PrefabController[] list = Resources.FindObjectsOfTypeAll<PrefabController>();
こんな感じで一覧を取得した。

それ以外は概ねこのやり方でOKな気がする。
さて今から本番プロジェクトで適用だ。こえぇ〜〜〜

さて、既存のプロジェクトにこれをやっている訳だが、色々詰まったので、メモ

まず、Resources.FindObjectsOfTypeAll<T>()のメソッド、
ロードされてるものしか拾って来れないという仕様に気をつけねばならない。
つまりアセットバンドル化したいものは通常ロードされてないので、
認識されない。

これを解決するために、一旦アセットバンドル化したいプレファブの参照を持ったオブジェクトをアセットバンドルをビルドにだけシーンに追加するようにした。

次に、今まで参照を持ってたスクリプトから参照の代わりにプレハブ名を持つ修正をした。
こんな感じで、一気に修正したんだけど、

FreCrePart[] list = Resources.FindObjectsOfTypeAll<FreCrePart>();

foreach(FreCrePart part in list){
part.prefabName = part.partController.gameObject.name;
}

Unityを再起動したら、修正がとれてる。
どうやらPrefabのコードでの修正はapplyしないと反映されないっぽい。
いったん全てのPrefabをシーンにいれて、いちいちapplyしていくという絶望的な作業をした。
誰か、いい方法教えてください。

所が、今
foreach(FreCrePart part in list){
part.prefabName = part.partController.gameObject.name;
EditorUtility.SetDirty(part); // アセットデータベースに変更を通知
}
こうすれば、OK!ってことを、Unity助け合い所で教えてもらった。
Hiromasa Suzukiさん
まじでありがとうございます!!

1





GongGongさん

このまとめに参加する