AssetBundle のラッパークラスです。以下の機能を持っています。
- 一旦読み込んだアセットバンドルは、Unload するまで全てメモリにキャッシュします。内部で参照カウントを持っているので、Load と Unload の回数が一緒になった時のみアンロードされます
- 一旦キャッシュされるとバンドル名は不要、ファイル名だけでアクセスすることも可能です
- UnityEditor 上で AssetBundle を作成せずファイルにアクセスする AssetBundle Simulator モードが使えます
- GameObject にアタッチせずに、どこからでも使えます
また、CreateAssetBundles(の生成する BundlePath.cs)と同時に使うことで、以下の便利機能も追加されます。
- 暗号化したアセットバンドルも読み込めます
- 依存関係のあるファイルを全て自動的に読み込むようになります
作ったばかりのヒヨッコードなので、動作がアヤシイ部分は多々あるかもしれません。
大変申し訳ありませんが、これを使用したことによる責任は、全て自己責任でお願いします。
(前向きなご意見などありましたら、コメント欄にお待ちしています)
ソースコード (BundleAssets.cs)
BundleAssets は CoroutineAccessor を内部で使用しています。
(Monobehaviour なしにコルーチンをアクセスするための仕掛けです)
使い方
ファイル構成
- Assets/StreamingAssets_RawData/ にアセットバンドルする前の通常ファイルを入れます
- Assets/StreamingAssets/ というフォルダを作成します(こちらに作成されたアセットバンドルが入ります)
- StreamingAssets_RawData のファイルを選択し、Inspector でアセットバンドルを設定してください
- BuildPipeline.BuildAssetBundles() でアセットバンドルを Assets/StreamingAssets/ に作成
(CreateAssetBundles を使うと GUI でアセットバンドルを作成できます)
4. はもちろん Asset Bundle Browser などで作っても構いません。
以下の状態で AssetBundle を作成したものとします。
バンドル名 | fonts |
ファイル名 | NotoSansJP-Black ~ NotoSansJP-Thin (6 files) |
アセットバンドルロード
BundleAssets.LoadAsync("fonts");
アセットバンドルロード完了待ち
// IEnumerator なコルーチンで使う yield return BundleAssets.WaitLoad("fonts"); // 全部読み終わるまで待つ yield return BundleAssets.WaitLoad(); // これでも可能だが、結局同じ while (BundleAssets.GetLoadCount() > 0) { yield return null; }
ファイル取得
BundleAssets.LoadAsync() が完了していれば、使えるようになります。
// フォント6種類を全て取得。string ... [ファイル名] Dictionary<string, Object> files = BundleAssets.GetAllAssets<TMP_FontAsset>("fonts"); // NotoSansJP-Thin だけ取得。ロードは完了していること var files = BundleAssets.GetAssets<TMP_FontAsset>("NotoSansJP-Thin");
シーンをアセットバンドル化して管理する場合、この「ファイル取得」は不要ですので(ヒエラルキーに最初から入っている)、とても楽にアセットバンドルを管理できるようになります。
アンロード
// 今オブジェクトで使われているファイルリソースはそのまま残す BundleAssets.Unload("fonts", false); // オブジェクトで使われていても強制的に全部クリア(使われていた部分は Missing になる) BundleAssets.Unload("fonts", true);
アセットバンドルシミュレーターの使い方
BundleAssets.cs の ASSETBUNDLE_SIMULATOR を有効にしてください。
// Unity Editor でのみ、アセットバンドルを作成しなくてもファイルにアクセスできるようにします #define ASSETBUNDLE_SIMULATOR
Scripting Define Symbols に直接指定して頂いても大丈夫です。
CreateAssetBundle (BundlePath) と同期させるには?
BundleAssets.cs の INCLUDE_BUNDLEPATH を有効にしてください。
Scripting Define Symbols に直接指定して頂いても大丈夫です。
技術的なこと
CreateAssetBundle (BundlePath) とは
アセットバンドル作成ツールですが、作成時にアセットバンドルを暗号化したり、バンドル同士の依存関係を詳細に調べて BundlePath にテーブル化します。
このテーブルを使うことでBundleAssets は暗号化したファイルを、それに気づかせず呼び出したり、シーンファイルをロードすると関係のあるバンドルも全て自動的に呼び出したり…といった機能を拡張することができます。
詳しくはこちらをご覧ください。
1ファイル名1オブジェクトではないことに注意
これは結構、仕組みを知らないと「あれ?」となる部分ですが、アセットバンドルとして読み込んだオブジェクトは、複数のアセットが含まれている可能性があります。
BundleAssets.LoadAsync("fonts"); BundleAssets.LoadAsync("images"); // 読み込み待ち yield return BundleAssets.WaitLoad(); var files = BundleAssets.GetAssets("NotoSansJP-Thin"); // 3 objects var images = BundleAssets.GetAllAssets("images"); // 7 objects
BundleAssets.GetAssets("NotoSansJP-Thin") は、バンドルしたファイルの下についているオブジェクトも含めて、3 種類のオブジェクトが取得されます。
- Font (TMP_FontAsset)
Font (Material)
Font (Texture2D)
BundleAssets.GetAllAssets("images"); は images 全てを取得するので、7 オブジェクトです。
- texture0 (Texture2D / Sprite Mode = multiple で 4 つに分割)
texture0_0 (Sprite0)
texture0_1 (Sprite1)
texture0_2 (Sprite2)
texture0_3 (Sprite3) - texture1 (Texture2D / Sprite Mode = Single)
texture1 (Sprite0)
Sprite などは大抵 1 テクスチャ 1 スプライトとして使われるので、複数読み込まれる事に違和感があるかもしれませんが、texture0 のように Sprite Editor でエリアをいくつかにわけたりすると 1 テクスチャ複数スプライトとなりますし、これは必要な所作と言えそうです。
ただし、プログラムで扱いたいのは Sprite だけで、Texture2D を返されても…という場合は以下のように記述することで Sprite のみを取得できます。
(BundleAssets 内部で、必要な型のみフィルタリングして返します)
var images = BundleAssets.GetAllAssets<Sprite>("images");
この仕組みは Resources.Load でも問題を引き起こすことがあります。
「Resources.Load Spriteが取れない」などでググるとその問題に当たった人の記録が出てきます。
Object sp = Resources.Load("texture") // Texture2D
Sprite sp = Resources.Load("texture") as Sprite; // 結果は null
Sprite sp = Resources.Load<Sprite>("texture") // 正しく Sprite が取れる
3つ目の書き方が正しいのですが、分割したテクスチャや、Resources.Load() の汎用ロードクラスを設計する時など、3つ目の書き方が出来ない事もあり、ちょっと沼にハマりそうですね。
参照カウンタの仕組み
それぞれ単体で動かしていたプログラムを後で結合した、その時 LoadAsync & Unload を複数個所でやっていたため、残ってしまった。
これは通常無駄にメモリを消費したり、アセットバンドルの場合はエラーとなってしまう行為ですが、結合の度にそれらのコードを修正するのも手間がかかります。
BundleAssets では、既に1度読み込んだアセットバンドル(とファイル)については、もう1度 LoadAsync されても何もせず、参照カウンタを +1 します。
また、それが Unload された場合参照カウンタを -1 し、0 になった時(本当に誰も参照しなくなった時)に初めて実際のアンロード処理が走ります。
このため、結合する際に Load & Unload を残していても、特に問題なく動作します。
そもそも Load & Unload の組み合わせがちゃんとしていないソースコードは上手く動きません…悪しからず。
シミュレータモードのメモリ管理を信頼してはいけない
毎回アセットバンドルを作るのが面倒、Unity Editor ではアセットバンドルを作らずに実行したい場合、BundleAssets の ASSETBUNDLE_SIMULATOR を有効化してください(コメントを外す)。
(たとえ有効にしていても、ビルドした環境では自動的にアセットバンドル読み込みに変わります)
#define ASSETBUNDLE_SIMULATOR
ただし、シミュレーターモードは「アセットバンドルの依存関係の管理」まで行っていないようです(元々そういう設計ではないみたい?)
Unity Editor での動作確認時は通常より大きなメモリ確保を行ってしまいますし(同じアセットを重複して確保したりする)、容量の大きなアセットを読み込むシーンは、起動まで少々時間がかかることもあります。
これらの問題を理解した上で、気軽にテストを試すために使うのであれば、シミュレーターは便利な機能だと思います。
シミュレーターに限らず、Unity Editor で動作させている時はメモリ管理が予想外の動きをすることが多いので、メモリチェックが必要な時は、ビルドした状態で確認することをお勧めします。
LoadAsync() で読み込み完了のコールバックを指定したい
以下のようにコールバックを指定し、使うことができます。
コールバックでは全てのオブジェクトリストを Dictionary<string, Object> 型で返します。
この時、BundleAssets.FindAssetsByType() でフィルタリングすると便利です。
(string の内容から自前でフィルタリングしても、もちろん構いません)
BundleAssets.LoadAsync( "images", (bundle) => { // Sprite のみにフィルタリング var sprites = BundleAssets.FindAssetsByType<Sprite>(bundle); } );
LoadAsync() とかあるのに、オブジェクトにアタッチしなくて平気なの?
CoroutineAccessor が DontDestroyOnLoad なオブジェクトを生成し、そこからコルーチンを呼び出します。こうすることで、Load() の呼び出しに GameObject インスタンスが不要になります。
なお、UniRx をお使いのプロジェクトであれば CoroutineAccessor.cs の以下のコードを有効にすることで、UniRx 版に切り替えることもできます。DontDestroyOnLoad が1つ減りますので、パフォーマンス的にはこちらの方がいいと思います。
#define USING_UniRx
同時ファイルアクセス数
記憶媒体は優しく扱った方がいいと思うので、どんなに LoadAsync() をしても基本的には 1 ファイルしか同時に読まないようにしていますが、スパルタモードにしたい、した方がいいのであれば、以下の数字を増やしてください。
/// <summary> /// 最大同時ロード数 /// </summary> const int MAXIMUM_LOADING = 1;
増やしすぎた事によってなにか問題が起こっても知りませんよ…!