はじめに
今回の記事は前回の
の続きになります。前回の記事を読んでない方は良かったら読んでみてください。
今回はAssetBundleについての説明していきます。
今回参考した記事はこちら
また、公式ドキュメントとサイバーエージェントさんのゲーム・エンターテインメント事業部から出ている『Unity パフォーマンスチューニングバイブル』からの引用もしながら説明していきます。
(Addressable AssetsはAssetBundleの機能を利用してより使いやすくしたものという認識です。それならAddressable Assetsを勉強すればいいかと思ったけどAssetBundleのこともちゃんとお勉強したいですよね。)
AssetBundleの仕組み
AssetBundleは前回の記事であった処理(以下前回の記事の引用)
Resourcesフォルダ内にあるアセットはビルド時に一つのファイルにまとめてシリアル化される。アプリの起動時にそれらのアセットを効率的に読み込むための処理が必ず走るためアセットの数が多くなればなるほど起動時のロード時間が長くなってしまう。(起動時に必要なアセット以外にもすべてまとめてこの処理を行うため厄介。ビルド時間も長くなる。)
を事前に行っておくことで、実行時に外部からアセットが読み込むことが出来る
という仕組み。
また、アセットを読み込むときに必要な*メタ情報などを一緒に格納することで、格納したアセットを必要に応じた型で読み込むことが出来る。
詳しい話は後でまた話していく。
-
*メタ情報
あるデータそのものではなく、そのデータを表す属性や関連する情報を記述したデータのこと
例えば、「エントリーシートメモ」をWordで作ろうとした場合、「ESMemo.docx」というファイルが出来上がる。そのファイルには
- 作成者:僕
- 作成日:2023年4月27日
- ファイルサイズ:200メガバイト
みたいな情報が付いてくる。この付いてくる情報の部分をメタ情報と呼ぶ。
とりあえずAssetBundleを使ってみた
AssetBundleは
- サーバーに配置したAssetBundleをダウンロードして利用する
- StreamingAssetフォルダ内に配置したAssetBundleを読み込んで利用する
という風にリモートでもローカルでも使えるので今回はローカルの方(StreamingAssetを用意してそこに配置)でやってみる。
(Unity公式ドキュメントにあるサンプルコードをお借りしました)
まずはなんでもいいので画像を用意。(僕はサンプルとして過去の個人制作のタイトル画面を用意)
そしたらその画像をクリックしてInspectorを開くと、下の方にAssetBundleを設定する欄があるのでそこをクリックしてAssetBundle名を入力。今回は公式ドキュメントと同じでmyassetbundleと名前を付ける。(この時MyAssetBundle/Sampleという風に「/」を用いるとフォルダ構造にできる)
AssetBundleをビルド化するにはコードを書く必要があるので作成。
#if UNITY_EDITOR
using UnityEditor;
using System.IO;
public class CreateAssetBundles
{
[MenuItem("Assets/Build AssetBundles")]
static void BuildAllAssetBundles()
{
string assetBundleDirectory = "Assets/AssetBundles";
if (!Directory.Exists(assetBundleDirectory))
{
Directory.CreateDirectory(assetBundleDirectory);
}
BuildPipeline.BuildAssetBundles(assetBundleDirectory,
BuildAssetBundleOptions.None,
BuildTarget.StandaloneWindows);
}
}
#endif
このスクリプトは、 アセット メニューの下部に
Build AssetBundles
というメニュー項目を作成し、そのタグに関連付けられた関数のコードを実行します 。
Build AssetBundles をクリックすると、プログレスバーがビルドダイアログとともに表示されます。これにより、AssetBundle 名でラベル付けしたすべてのアセットを取得し、assetBundleDirectory
が定義するパスのフォルダーに置きます。
下から2番目にBuild AssetBundlesがあるのがわかる
このスクリプトを書くと引用にもある通り、Build AssetBundlesがAssetメニューに追加される。それを押すことで関数が実行され、BuildPipeline.BuildAssetBundlesでビルドが行われた後に、Assets/AssetsBundlesフォルダ内にビルド後のファイルが出力される。
BuildPipeline.BuildAssetBundles関数は引数にいろいろ設定できるが気を付けてほしいのが
BuildTarget
これはどのプラットフォーム向けにビルドをするのかを設定するもので、指定したプラットフォーム以外だと利用できなくなるので気を付けてほしい。
また、BuildAssetBundleOptionsの部分ではAssetBundleの圧縮方法を決めれる。
- None:今回設定したNoneではLZMA形式で圧縮。
- UncompressedAssetBundle:圧縮をしない
- ChunkBasedCompression:LZ4形式で圧縮
圧縮方法によってファイルサイズやロード時間が変わってくる。(詳細は公式ドキュメントに書いてある。『Unity パフォーマンスチューニングバイブル』にも表付きで説明が書かれている)
早速AssetメニューにあるBuild AssetBundlesを押してAssets/AssetsBundlesフォルダ内にビルドデータを作成し、
それらをStreaming Assetフォルダに移動させる。
サンプルコードを参考にしながらImageにSpriteを張り付けるコードを作成
using System.IO;
using UnityEngine;
using UnityEngine.UI;
public class LoadFromFileExample : MonoBehaviour
{
[SerializeField, Tooltip("AssetBundleに格納されているスプライトを張り付けるImage")]
private Image _image = default;
private void Start()
{
// AssetBundleのメタ情報をロード
var assetBundle = AssetBundle.LoadFromFile(Path.Combine(Application.streamingAssetsPath, "myassetbundle"));
if (assetBundle == null)
{
Debug.Log("Failed to load AssetBundle!");
return;
}
// テクスチャをメモリにロード
var sprite = assetBundle.LoadAsset<Sprite>("AssetBundleSample");
_image.sprite = sprite;
// AssetBundleのメタ情報をアンロード
assetBundle.Unload(false);
}
}
そしたら空のオブジェクトを作成してこのコードをアタッチし、Imageもアタッチする。
そうしたらビルドをしてみて、用意したImageに画像が張られていたらOK。
読んでいる人はまだこの時点では(恐らく)AssetBundleについて深く理解できていないと思うがやったことが思い通り動くとやはり楽しい。
次はAssetBundleのロードAPI、アンロードAPIについて説明しながらAssetBundleの話を深掘りしていく。
AssetBundleのロードAPI
ロードをAPIを紹介するうえでまずUnityNativeメモリとMonoメモリの二つの用語について説明する。
-
UnityNativeメモリ
- C#管理外の、Unity側が管理しているメモリ領域。ResourcesやAssetBundleからロードしたアセットはここに読み込まれる。GC(ガベージ・コレクション)によるメモリの解放はされないので自分で明示的に解放してあげる必要がある。
-
Monoメモリ
- C#側で管理されているメモリ領域。C#側で管理されているためGCによるメモリ解放が行われる。
とりあえず、
GCによるメモリ解放がされるかされないか
の違いくらいを覚えていてほしい。
ではAssetBundleからアセットをロードする際のAPIを説明していく。
以下の3つがある。
-
AssetBundle.LoadFromFile
- サンプルコードでも使われていたAPI。ストレージにあるファイルパスを指定してロードする。最速かつもっとも省メモリのため、基本的にこれを使う。このAPIを使うことでAssetBundleをディスク上からロードすることが出来る。この場合はAssetBundleのメタ情報のみがロードされ、アセット本体はLoadAssetされたタイミングで初めてロードされる。
-
AssetBundle.LoadFromMemory
- メモリにロード済みのAssetBundleデータを指定してロードする。これを使う場合はAssetBundleのデータをMono側にbyte[]で確保する必要があり、AssetBundleを使っている間は非常に大きなデータをメモリに維持する。そのためメモリ不可が非常に大きい。よってこれは基本的に使わない。
-
AssetBundle.LoadFromStream
- AssetBundleのデータを返すStreamを指定してロードする。暗号化されたAssetBundleの複合処理をしながらロードをする場合はメモリ負荷を考慮してこのAPIを使う。
もう少しAssetBundleのロード処理の仕組みについて実際に作ったやつを例に深掘りしていこう。
AssetBundle.LoadFromFileを使ってAssetBundleをロード。
var assetBundleA = AssetBundle.LoadFromFile(Path.Combine(Application.streamingAssetsPath, "myassetbundle"));
上記の説目にもある通りこの段階ではAssetBundleのメタ情報のみがUnityNativeメモリ上にロードされる。LoadFromFileの戻り値となるAssetBundleオブジェクトはMonoメモリ上に置かれていて、それがUnityNativeメモリにあるメタ情報の参照を持っているようなイメージ。
次にLoadAssetを使用してassetBundleAからspriteをロードする。このときはじめてディスク上からアセットをUnityNativeメモリに読み込む。
var spriteA = assetBundleA.LoadAsset<Sprite>("AssetBundleSample");
この時にもう一度同じアセットをロードする(LoadAsset)と、すでにメモリ上にロードされているアセットの参照が返ってくる。そのため、ディスクI/Oも発生せず、アセットがメモリ上に重複することもない。
var spriteA2 = assetBundleA.LoadAsset<Sprite>("AssetBundleSample");
AssetBundleオブジェクトはMono側で、メタ情報とロードされたアセットはUnityNative側で管理される。
これがわかったところで次にアンロードAPIの説明をしていく。
AssetBundleのアンロードAPI
アンロードするときのAPIには
- AssetBundle.Unload(bool unloadAllLoadedObject)
が用意されている。
AssetBundleは不要になったタイミングでアンロードをしないとメモリを圧迫してしまう。なのでこのAPIが用意されているのだが引数の扱いが非常に大切になってくる。
(ここでUnityNativeメモリとMonoメモリのメモリ解放の話になってくる。前者はGCによってメモリ解放がされないので明示的に解放してあげなければならない。後者はGCがメモリ解放をやってくれる。)
引数が
-
falseの場合
- AssetBundleをアンロードする際にそのAssetBundleからロードされたメタ情報のみがアンロードされる。以降、assetBundleAからLoadAssetすることはできなくなる。残ったアセットはSpriteからの参照が切れた後にResources.UnloadUnusedAssetsを呼び出すなどで解放してあげることができる。falseの場合はアセットをロードし終わったタイミングでAssetBundleをアンロード出来るのでその場のメモリ負荷は低いが、使い終わったアセットのアンロードを忘れるとメモリリークにつながったり、同じアセットがメモリ上に複数ロードされたりする恐れがあるためメモリ管理が難しくなる。
assetBundleA.Unload(false);
-
trueの場合
- 読み込み済みのアセットもメタ情報もすべてアンロードされる。trueの場合はアセットを使っている間はAssetBundleもロードし続けないといけないのでメモリ負荷が高いが、アセットの破棄も同時に、確実に行えるのでメモリ管理の安全性が高い。
assetBundleA.Unload(true);
メモリ管理の話にもつながってくるため、falseを選ぶのかtrueを選ぶのかは開発初期の段階でどっちにするかを決定する必要があるくらい重要なのだ。(メモリ負荷に余裕がある場合はtrueを推奨)
AssetBundleの仕様上の注意点
メタ情報は明示的にUnloadしないと解放されない
メタ情報はUnityNativeメモリで管理されるためGCによって解放されない。そのため、メタ情報はAssetBundle.Unloadを呼び出さない限りUnityNativeメモリ上に残り続けてしまう。さらにはメタ情報はリソース扱いされないためResources.UnloadUnusedAssetsでも解放されない。
メタ情報が残り続けると以下の問題にもつながってくる。
メタ情報がロード済みのAssetBundleをもう一度ロードしようとするとエラーになる
var assetBundle = AssetBundle.LoadFromFile(// pathの名前);
var assetBundle2 = AssetBundle.LoadFromFile(// 上と同じものを読み込む);
このようなコードを書いた場合エラーになる。
ただし、以下のコードはエラーにならない。
var assetBundle = AssetBundle.LoadFromFile(// pathの名前);
// メタ情報のみをアンロード
assetBundle.Unload(false);
var assetBundle2 = AssetBundle.LoadFromFile(// 上と同じものを読み込む);
ロードした後にメタ情報をアンロードしているため次に同じものをロードしてもメタ情報がぶつからない。
が、
再びロードされたAssetBundleから同じアセットをロードすると別インスタンスとして生成されるためメモリ上に同じアセットが二重にロードされてしまう
var assetBundle = AssetBundle.LoadFromFile(// pathの名前);
var sprite = assetBundle.LoadAsset<Sprite>("AssetBundleSample");
// メタ情報のみをアンロード
assetBundle.Unload(false);
var assetBundle2 = AssetBundle.LoadFromFile(// 上と同じものを読み込む);
var sprite2 = assetBundle.LoadAsset<Sprite>("AssetBundleSample");
このコードではAssetBundleSampleをロードして、一度メタ情報のみをアンロード。そのあと再びAssetBundleSampleを読み込んでいるため同じSpriteのデータがUnityNativeメモリ上に重複してロードされてしまう。
AssetBundleの依存関係
とあるアセットが複数のアセットから依存されている場合、AssetBundle化をする際に注意が必要になる。
例えばマテリアルAとマテリアルBがテクスチャCに依存している場合、テクスチャCをAssetBundle化しないでマテリアルAとBだけAssetBundle化すると生成されるマテリアルAとBの2つのAssetBundleにテクスチャCが含まれるため、重複して無駄になってしまう。(容量的にもメモリ的にも無駄)
同一アセットが複数のAssetBundleに含まれるのを避けるための方法としては
- テクスチャCも単体でAssetBundle化してマテリアルAとBのAssetBundleから依存される形にする
- マテリアルAとBとテクスチャCを1つにまとめたAssetBundleにする
の二点が考えられる。
まとめ
AddressableAssetsを理解するうえでAssetBundleの仕組みを知っておきたいなと思ったので今回はAssetBundleについて勉強してまとめてみました。なかなか理解できたと思うのですが間違っている所などありましたら教えていただきたいです。
AddressableAssetsが出たけどAssetBundleのまま開発を進めている現場などもあると思うのでAssetBundleの理解をするに越したことはないかなと思います。
AssetBundle、どっかでがっつり使ってみたい…。
次回はいよいよAddressableAssetsについての記事を書きたいと思います!