AddressableAssetsSystemの前にAssetBundleを理解する！


# はじめに

今回の記事は前回の

[UnityのResourcesについて](https://qiita.com/Mizutamari/items/43897e8aaf30af3a82f5)

の続きになります。前回の記事を読んでない方は良かったら読んでみてください。

今回は**AssetBundle**についての説明していきます。

今回参考した記事はこちら

- ****[AssetBundleを完全に理解する](https://qiita.com/k7a/items/d27640ac0276214fc850)****

また、公式ドキュメントとサイバーエージェントさんのゲーム・エンターテインメント事業部から出ている[『Unity パフォーマンスチューニングバイブル』](https://developers.cyberagent.co.jp/blog/archives/37085/)からの引用もしながら説明していきます。

(Addressable AssetsはAssetBundleの機能を利用してより使いやすくしたものという認識です。それならAddressable Assetsを勉強すればいいかと思ったけどAssetBundleのこともちゃんとお勉強したいですよね。)

# AssetBundleの仕組み

AssetBundleは前回の記事であった処理(以下前回の記事の引用)

> Resourcesフォルダ内にあるアセットはビルド時に一つのファイルにまとめてシリアル化される。アプリの起動時にそれらのアセットを効率的に読み込むための処理が**必ず**走るためアセットの数が多くなればなるほど起動時の**ロード時間が長くなってしまう**。(起動時に必要なアセット以外にもすべてまとめてこの処理を行うため厄介。**ビルド時間も長くなる**。)
> 

を事前に行っておくことで、実行時に外部からアセットが読み込むことが出来る

という仕組み。

また、アセットを読み込むときに必要な***メタ情報**などを一緒に格納することで、格納したアセットを必要に応じた型で読み込むことが出来る。

詳しい話は後でまた話していく。

- ***メタ情報**
    
    あるデータそのものではなく、そのデータを表す属性や関連する情報を記述したデータのこと
    
    例えば、「エントリーシートメモ」をWordで作ろうとした場合、「ESMemo.docx」というファイルが出来上がる。そのファイルには
    
    - 作成者：僕
    - 作成日：2023年4月27日
    - ファイルサイズ：200メガバイト
    
    みたいな情報が付いてくる。この付いてくる情報の部分をメタ情報と呼ぶ。
    

# とりあえずAssetBundleを使ってみた

AssetBundleは

- サーバーに配置したAssetBundleをダウンロードして利用する
- StreamingAssetフォルダ内に配置したAssetBundleを読み込んで利用する

という風にリモートでもローカルでも使えるので今回はローカルの方(StreamingAssetを用意してそこに配置)でやってみる。

([Unity公式ドキュメント](https://docs.unity3d.com/jp/current/Manual/AssetBundles-Workflow.html)にあるサンプルコードをお借りしました)

まずはなんでもいいので画像を用意。(僕はサンプルとして過去の個人制作のタイトル画面を用意)

そしたらその画像をクリックしてInspectorを開くと、下の方にAssetBundleを設定する欄があるのでそこをクリックしてAssetBundle名を入力。今回は公式ドキュメントと同じで**myassetbundle**と名前を付ける。(この時MyAssetBundle/Sampleという風に「/」を用いるとフォルダ構造にできる)

![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2930062/5fe57db3-5d8d-7dc9-4326-0efb5e1b3d3f.png)

AssetBundleをビルド化するにはコードを書く必要があるので作成。

```csharp
#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.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2930062/baf2c344-9c24-cb88-876e-8a7c0548d170.png)
下から2番目に**Build AssetBundles**があるのがわかる

このスクリプトを書くと引用にもある通り、**Build AssetBundlesがAssetメニューに追加される**。それを押すことで関数が実行され、**BuildPipeline.BuildAssetBundles**でビルドが行われた後に、**Assets/AssetsBundlesフォルダ**内にビルド後のファイルが出力される。

BuildPipeline.BuildAssetBundles関数は**引数**にいろいろ設定できるが気を付けてほしいのが
**BuildTarget**
これは**どのプラットフォーム向けにビルドをするのか**を設定するもので、指定したプラットフォーム以外だと利用できなくなるので気を付けてほしい。

また、**BuildAssetBundleOptions**の部分では**AssetBundleの圧縮方法**を決めれる。

- None：今回設定したNoneではLZMA形式で圧縮。
- UncompressedAssetBundle：圧縮をしない
- ChunkBasedCompression：LZ4形式で圧縮

圧縮方法によってファイルサイズやロード時間が変わってくる。(詳細は[公式ドキュメント](https://docs.unity3d.com/jp/current/Manual/AssetBundles-Building.html)に書いてある。[『Unity パフォーマンスチューニングバイブル』](https://developers.cyberagent.co.jp/blog/archives/37085/)にも表付きで説明が書かれている)

早速**AssetメニューにあるBuild AssetBundles**を押して**Assets/AssetsBundlesフォルダ**内にビルドデータを作成し、

![3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2930062/ce0e7379-3999-ddd6-ff94-3f49fe06bd05.png)

それらを**Streaming Assetフォルダに移動**させる。

![Untitled (13).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2930062/761230a8-d310-22cf-fb12-a8a57e2486ab.png)

サンプルコードを参考にしながらImageにSpriteを張り付けるコードを作成

```csharp
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もアタッチする。

![4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2930062/219b4b6b-5aa3-51f1-d56f-c24c3fdd49aa.png)


そうしたらビルドをしてみて、用意したImageに画像が張られていたらOK。

![5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2930062/8aeba230-fb19-ccc7-95d8-da8d4fba6785.png)


読んでいる人はまだこの時点では(恐らく)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をロード。

```csharp
var assetBundleA = AssetBundle.LoadFromFile(Path.Combine(Application.streamingAssetsPath, "myassetbundle"));
```

上記の説目にもある通りこの段階ではAssetBundleの**メタ情報のみがUnityNativeメモリ上にロード**される。LoadFromFileの戻り値となるAssetBundleオブジェクトはMonoメモリ上に置かれていて、それがUnityNativeメモリにあるメタ情報の参照を持っているようなイメージ。

![6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2930062/25c4c0fe-855f-5cbb-7953-455b2b97b9e3.png)

次にLoadAssetを使用してassetBundleAからspriteをロードする。このときはじめて**ディスク上からアセットをUnityNativeメモリに読み込む**。

```csharp
var spriteA = assetBundleA.LoadAsset<Sprite>("AssetBundleSample");
```

![7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2930062/f44c985c-3109-731d-db18-f6197adc0eca.png)

この時にもう一度同じアセットをロードする(LoadAsset)と、**すでにメモリ上にロードされているアセットの参照**が返ってくる。そのため、**ディスクI/Oも発生せず、アセットがメモリ上に重複することもない**。

```csharp
var spriteA2 = assetBundleA.LoadAsset<Sprite>("AssetBundleSample");
```

![8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2930062/f9785090-801d-24e4-68de-65d4ffdd9dde.png)

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をアンロード出来るので**その場のメモリ負荷は低い**が、使い終わったアセットのアンロードを忘れると**メモリリーク**につながったり、同じアセットがメモリ上に**複数ロード**されたりする恐れがあるため**メモリ管理が難しくなる**。
  

```csharp
assetBundleA.Unload(false);
```

![9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2930062/2192a555-8d87-1066-d366-006794579901.png)

- **trueの場合**
    - **読み込み済みのアセットもメタ情報もすべてアンロード**される。trueの場合はアセットを使っている間はAssetBundleもロードし続けないといけないので**メモリ負荷が高い**が、アセットの破棄も同時に、確実に行えるので**メモリ管理の安全性が高い**。
    

```csharp
assetBundleA.Unload(true);
```

![10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2930062/702fb57a-552a-1aec-9981-e1741f840f2a.png)

メモリ管理の話にもつながってくるため、**falseを選ぶのかtrueを選ぶのかは開発初期の段階でどっちにするかを決定する必要があるくらい重要**なのだ。(メモリ負荷に余裕がある場合はtrueを推奨)

# AssetBundleの仕様上の注意点

### メタ情報は明示的にUnloadしないと解放されない

メタ情報はUnityNativeメモリで管理されるためGCによって解放されない。そのため、メタ情報は**AssetBundle.Unloadを呼び出さない限りUnityNativeメモリ上に残り続けてしまう**。さらにはメタ情報は**リソース扱いされない**ため**Resources.UnloadUnusedAssetsでも解放されない**。

メタ情報が残り続けると以下の問題にもつながってくる。

### メタ情報がロード済みのAssetBundleをもう一度ロードしようとするとエラーになる

```csharp
var assetBundle = AssetBundle.LoadFromFile(// pathの名前);

var assetBundle2 = AssetBundle.LoadFromFile(// 上と同じものを読み込む);
```

このようなコードを書いた場合エラーになる。

ただし、以下のコードはエラーにならない。

```csharp
var assetBundle = AssetBundle.LoadFromFile(// pathの名前);
// メタ情報のみをアンロード
assetBundle.Unload(false);
var assetBundle2 = AssetBundle.LoadFromFile(// 上と同じものを読み込む);
```

ロードした後にメタ情報をアンロードしているため次に同じものをロードしてもメタ情報がぶつからない。

が、

### 再びロードされたAssetBundleから同じアセットをロードすると別インスタンスとして生成されるためメモリ上に同じアセットが二重にロードされてしまう

```csharp
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メモリ上に重複してロードされてしまう。

![11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2930062/f7ce91df-639f-8494-a287-4e6642a12f56.png)

### 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**にする

の二点が考えられる。

![6b0dee0998ed42b1abaa02d21d4ede17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2930062/fecc3cb0-dfab-a8bd-0737-48bd57708ac7.png)


# まとめ

AddressableAssetsを理解するうえでAssetBundleの仕組みを知っておきたいなと思ったので今回はAssetBundleについて勉強してまとめてみました。なかなか理解できたと思うのですが間違っている所などありましたら教えていただきたいです。

AddressableAssetsが出たけどAssetBundleのまま開発を進めている現場などもあると思うのでAssetBundleの理解をするに越したことはないかなと思います。

AssetBundle、どっかでがっつり使ってみたい…。

次回はいよいよAddressableAssetsについての記事を書きたいと思います！