【Unity】動的なアセット管理についてざっくりまとめる

# はじめに
動的アセット管理について、ざっくり自分なりにまとめてみます。
また、Addressablesの使い方については今後別記事でまとめたいです。

なお、詳しく知りたい方は、内部的な仕組みまでまとめてあるこちらの記事がオススメです↓

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

# 動的・静的とは
### 静的
- **あらかじめ手動でアセットを参照したりすること。**
- インスペクターの`[SerializeField]`などでアセットを静的参照できる

<br>

- 小規模なら手動の方がコストかからない
- シーン内に利用するアセットが多くなってくるとめんどくさくなる


### 動的
- **アプリ実行中にアセットを読み込んで、参照する方法。**
- 特定のフォルダにファイルを入れて読み込んだりする。

<br>

- 大規模なゲームなどではほぼ必須
- ソシャゲなどでは、サーバー上からアセット情報を取得して読み込んだりする


# Unityでの動的読み込みの種類
動的アセット管理方法をいくつか挙げます

### Resources
- **"Resources"** という名前のフォルダにアセットを入れて読み込む方法
- 公式から利用を強く非推奨されている

https://docs.unity3d.com/ScriptReference/Resources.html

##### 用途
- 主にプロトタイプ版などですぐに実装したい場合などに利用

##### 注意点
- アプリ容量やロード時間が伸びる原因になる。
- 使わない方がいい

##### 使い方
1. Resourcesフォルダを作成
1. その中に読み込み対象のアセットを入れる
1. `Resources.Load("アセット名")`で読み込む

##### 使用例
```C#

var spriteRenderer= GetComponent<SpriteRenderer>();
var sprite = Resources.Load<Sprite>("Sprites/sprite");
spriteRenderer.sprite = sprite;
```


---


### StreamingAssets(+ System.IO)
- **"StreamingAssets"** という名前のフォルダにアセット情報を入れて、読み込む
    - StreamingAssetsフォルダのパスは、`Application.streamingAssetsPath`というプロパティで取得できる
- アセットのファイルを指定して読み込むには、**`System.IO`** を利用する
    - **Android, WebGLの場合は `UnityWebRequest` を利用する必要がある**

- また、AssetBundleを利用する際に、アセットバンドルをStreamingAssetsフォルダに格納して読み込むこともできる(後述)

https://docs.unity3d.com/ScriptReference/Application-streamingAssetsPath.html

<br>

##### 用途
- **ゲーム内のパラメータデータの読み取りなど(CSV,JSON,txtなど)**
    - 画像や音声などは他の方法の方がめんどくさくない
- **AssetBundleを格納して読み込む**

##### 注意点
- **アプリ内に格納されるので、容量が増える**
- **読み取りのみ**
書き込みはできないので、セーブデータの保存などには向いていない
- 容量は増えるが、起動時の読み込みにはあまり影響しない
- プラットフォームごとにアセットの圧縮形式などを変更したりすることはできない
-　プラットフォームごとに読み込み方法を変更する必要がある
    -　**Android, WebGLはStreamingAssetsフォルダーにSystem.IOでアクセスできないので、WebRequest経由でアクセスする必要がある**


##### 使い方
テキストファイルなどを読み込む場合
1. StreamingAssetsフォルダを作成
1. その中に読み込み対象のファイルを入れる
1. `System.IO.File.ReadAllText("ファイルパス")`などでファイルを読み込む


##### 使用例
```C#
var path = System.IO.Path.Combine(Application.streamingAssetsPath, "Data/gameData.json");
var json = System.IO.ReadAllText(path);
```

```Android, WebGL.cs
var path = System.IO.Path.Combine(Application.streamingAssetsPath, "Data/gameData.json");

var request = UnityWebRequest.Get(path);
yield return request.SendWebRequest();
var json = request.downloadHandler.text;
```

---

### AssetDataBase
- **Editor内限定でアセットを読み込める**

https://docs.unity3d.com/ja/2022.3/ScriptReference/AssetDatabase.html


##### 使用例
```AssetDataBase.cs
void OnEnable()
{
    var texture = AssetDataBase.LoadAssetAtPath<Texture>(path);
    EditorGUI.Button(texture);
}
```


### AssetBundle
- **ゲーム内で動的に参照したいアセットをアセットバンドルというファイルにまとめて、読み込む方法**
- アセットバンドルは、StreamingAssetsフォルダに配置したり、サーバー上に配置して読み込むことができる

https://docs.unity3d.com/ScriptReference/AssetBundle.html


##### 用途
- ゲーム内アセットの動的読み込み全般

##### 注意点
- **アセットバンドルの作成などは自分でコード作ったりしないといけない**
    - プラットフォームごとに変更、圧縮形式の設定なども自作コードで指定しないといけない
- どのアセットが含まれているかや、アセット同士の参照関係などが分かりづらい
- アセットを変更する度にビルドする必要がある
- パスの指定をコード内で指定する必要がある
- プラットフォームごと処理を変更するのがめんどい
    - Android, WebGLはStreamingAsssetsフォルダにアクセスできないため、別途処理を分ける必要がある
- マテリアルやスプライトなどが必要なプレハブなどを読み込む場合、スプライトやマテリアルもアセットバンドルに入れる必要がある
    - プレハブが使用するスプライトやマテリアルが別のアセットバンドルに登録されている状態でプレハブのアセットバンドルを読み込んでインスタンス化すると、依存しているマテリアルなどが表示されない状態になる(ピンク状態や非表示)
        - 同じバンドルに登録しておくのが手っ取り早い

##### 使い方
1. 目的のアセットを選択して、インスペクターの一番下のAsseetBundleという項目に、アセットバンドルを指定する
    - これ
        ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2419795/d9c17aac-30ad-698b-a0eb-1220eb2e8f80.png)
    - `/`で区切ることで、階層分けできる
        - 例：`prefabs/enemy/slime`


1. アセットバンドルを作成するコードを作成して、エディター上などでアセットバンドルを作成する
1. 作成したアセットバンドルを、StreamingAssetsフォルダ内かサーバー上に配置する
1. 作成したアセットバンドルを読み込む
1. 読み込んだアセットバンドルから、登録されたアセットを読み込む
1. 読み込んだアセットをアンロードしてメモリ解放する

##### 使用例
- 基本的には以下のようにアセットバンドルを作成しますが、
プラットフォームごとにメソッド切り分けて、エディター上でプラットフォーム選択とかできるようにしといたほうがいいです
    ```アセットバンドル作成コード.cs
    // パス
    const string assetBundleDirectory = "Assets/StreamingAssets";
    
    [MenuItem("Assets/Build AssetBundles")]
    static void BuildAssetBundles()
    {
    
        if (!System.IO.Directory.Exists(assetBundleDirectory)) {
            System.IO.Directory.CreateDirectory(assetBundleDirectory);
        }
    
        BuildPipeline.BuildAssetBundles(assetBundleDirectory, BuildAssetBundleOptions.None, BuildTarget.WebGL);
    }
    ```
<br>

- 以下のコードでは、非同期で読み込みをして、
読み込みが完了してからロード完了ログを表示するようにしています
    ```アセットバンドル読み込みコード.cs
    private IEnumerator Start()
    {
        yield return StartCoroutine(Loading());
        
        print("Load Completed");
    }
    
    IEnumerator Loading()
    {
        // アセットバンドル読み込み
        var path = System.IO.Path.Combine(Application.streamingAssetsPath, assetBundlePath);
        var req = AssetBundle.LoadFromFileAsync(path);
        yield return req;
        var assetBundle = req.assetBundle;
    
        // アセット読み込み・生成
        var prefabReq = assetBundle.LoadAssetAsync<GameObject>(assetName);
        yield return prefabReq;
        var prefab = prefabReq.asset as GameObject;
        Instantiate(prefab);
    }
    ```

<br>

- Android, WebGLでアセットバンドルを利用したい場合、このように`UnityWebRequestAssetBundle`クラスの`GetAssetBundle()`を利用して、取得した`DownloadHandlerAssetBundle`クラスの`assetBundle`プロパティからアセットバンドルを取得します
    ```Android, WebGL.cs
    var path = Path.Combine(Application.streamingAssetsPath, assetBundlePath);
    var req = UnityWebRequestAssetBundle.GetAssetBundle(path);
    yield return req.SendWebRequest();
    
    var assetBundle = (req.downloadHandler as DownloadHandlerAssetBundle).assetBundle;
    ```

<br>

- 依存関係にあるアセットバンドルを取得する場合、`AssetBundleManifest`というクラスを利用して、そのアセットバンドルを読み込んでから対象のアセットのアセットバンドルを読み込めます
([参考サイト](https://light11.hatenadiary.com/entry/2019/06/12/215952))
```依存関係にあるアセットバンドルの読み込み.cs
[SerializeField] string rootBundleName;        // アセットバンドルがビルドされたフォルダのパスの名前のアセットバンドル名
[SerializeField] string assetBundlePath;       // 読み込み対象のアセットが登録されたアセットバンドルのパス
[SerializeField] string assetName;             // 読み込み対象のアセットの名前

IEnumerator LoadManifest()
{
    // AssetBundleManifestの読み込み
    var rootBundlePath = System.IO.Path.Combine(Application.streamingAssetsPath, rootBundleName);
    var rootBundleReq = AssetBundle.LoadFromFileAsync(rootBundlePath);
    yield return rootBundleReq;
    var rootBundle = rootBundleReq.assetBundle;

    var manifestReq = rootBundle.LoadAssetAsync<AssetBundleManifest>("AssetBundleManifest");
    yield return manifestReq;
    var manifest = manifestReq.asset as AssetBundleManifest;

    print("manifest;" + manifest);

    rootBundle.Unload(false);

    // 依存関係のあるアセットバンドルを読み込み
    var dependencies = manifest.GetAllDependencies(this.assetBundlePath);
    var dependencyBundles = new List<AssetBundle>();
    foreach (var dependency in dependencies) {
        var path = System.IO.Path.Combine(Application.streamingAssetsPath, dependency);
        var bundleReq = AssetBundle.LoadFromFileAsync(path);
        yield return bundleReq;
        dependencyBundles.Add(bundleReq.assetBundle);

        print("dependency: " + dependency);
    }

    // アセットバンドルを読み込み, アセットを生成
    var assetBundlePath = System.IO.Path.Combine(Application.streamingAssetsPath, this.assetBundlePath);
    var assetBundleReq = AssetBundle.LoadFromFileAsync(assetBundlePath);
    yield return assetBundleReq;
    var assetBundle = assetBundleReq.assetBundle;
    var prefab = assetBundle.LoadAsset<GameObject>(assetName);
    var obj = Instantiate(prefab);
    assetBundle.Unload(false);

    // 依存関係のあるアセットバンドルをアンロード
    foreach (var bundle in dependencyBundles) {
        bundle.Unload(false);
    }
}
```

    



---

### Addressable
- **AssetBundleを便利にしたもの**
- 読み込んだアセットの参照数などを視覚化したりできるツールが提供される
- プラットフォームごとに自動でフォルダを作成したりしてくれる
- ローカルパスやリモートパスを簡単に指定できる
    - ローカル・リモートの切り替えも楽

https://docs.unity3d.com/Packages/com.unity.addressables@2.0/manual/index.html

##### 用途
- ゲーム内アセットの動的読み込み全般


##### 注意点
- アセットを変更する度にビルドする必要がある
    - **エディタ上では、Addressables GroupsウィンドウのPlay Mode ScriptのUse Asset DataBaseでわざわざビルドしなくていい**

##### 使い方
Unity Package Managerから追加する必要があります

1. 読み込み対象のアセットを選択して、インスペクターの上部の"Addressable"というチェックボックスにチェック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2419795/08f1c445-e4f3-7efc-b2b3-0284a270fb8a.png)

1. Addressables Groupsでアセットのキーを設定(任意)
    - Addressable Nameでアクセスするための名前を指定できる
    - ラベルを指定してグループ化できる
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2419795/7c8e3702-da6d-332d-57f3-bb2b5dd444db.png)

1. `Addressables.LoadAssetAsync()`でキーを指定して読み込む
    - キーにはAddressable Name、Path、Labelを指定できる
    - `AssetReference`を`[SerializeField]`でインスペクター上に表示させることで、直接指定することもできる


##### 使用例
```AddressablesTest.cs
    [SerializeField] string prefabAssetKey;

    private async void Start()
    {
        await LoadAsset();

        print("Load Completed");
    }

    async Task LoadAsset()
    {
        var assetBundlePrefab = await Addressables.LoadAssetAsync<GameObject>(prefabAssetKey).Task;
        Instantiate(assetBundlePrefab);
    }
```



<br>

# AssetBundleとAddressablesの違い
**全体的に、Addressableの方が優れてます**



### 1. 非同期
##### AssetBundle
- コルーチン
- `LoadAssetAsync()`などのメソッドの戻り値の`AssetbundleRequest`などを yield returnして待機する
- コルーチンに戻り値を設けたい場合、引数に`System.Action<T>`などコールバックで渡す必要がある

```AssetBundle.cs
IEnumerator Loading()
{
    // アセットバンドル読み込み
    var path = System.IO.Path.Combine(Application.streamingAssetsPath, assetBundlePath);
    var req = AssetBundle.LoadFromFileAsync(path);
    yield return req;
    var assetBundle = req.assetBundle;

    // アセット読み込み・生成
    var prefabReq = assetBundle.LoadAssetAsync<GameObject>(assetName);
    yield return prefabReq;
    var prefab = prefabReq.asset as GameObject;
    Instantiate(prefab);
}
```


##### Addressables
- `await/async`を使う
- `AsyncOperationHandle`という戻り値を返す
    - プロパティに`Task`を持ってるので、これを`await`する



```Addressables.cs
async Task LoadAsset()
{
    var assetBundlePrefab = await Addressables.LoadAssetAsync<GameObject>(prefabAssetKey).Task;
    Instantiate(assetBundlePrefab);
}
```


### 2. アセットをアセットバンドルに追加する方法
##### Asset Bundle
- アセット選択時のインスペクターの下側のAssetbundleという項目に文字列を指定してアセットバンドルに追加する
- アセットバンドル一覧を可視化する場合、追加のモジュールのAssetbundle Browserを追加する必要がある


##### Addressables
- アセット選択時のインスペクターの上側のAddressableというトグルを切り替える
- Addressables Groupsからアセットバンドルに追加されたアセットの一覧がまとめられてる



### 3. アセットバンドルのビルド方法
##### AssetBundle
- 自分で組み込む必要がある
- プラットフォームごとにBuildTargetを分ける必要がある


##### Addressables
- ボタン一つ押すだけでいい
- **BuildTargetもBuildSettingsでSwitch Platformするだけで自動で変えてくれる**
- **エディタ上ではわざわざバンドルをビルドしなくても動作確認できる**
    - Addressables GroupsウィンドウのPlay Mode ScriptのUse Asset DataBase
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2419795/815f05fe-e699-8215-876d-660443e9daea.png)


### 4. ロード先のパスやビルド先のパスの指定
##### AssetBundle
- 自作
- プラットフォームごとに分けたりするのが面倒

##### Addressables
- Addressables Profilesウィンドウでグループごとに割り当てることができる
- ローカルとリモートの切り替えも楽にできる
- プラットフォームごとにパスを自動で変更してくれる

Addressables Profilesウィンドウから、ローカルとリモートで異なるパスを指定して読み込んでくれる
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2419795/b471f162-f754-8c47-225b-834630f5e347.png)



### 5. 依存関係にあるアセットの読み込み
##### AssetBundle
- AssetbundleManifestから参照関係にあるアセットバンドルを取得して読み込んでから、目的のアセットを読み込む

##### Addressables
- 自動



### アセットの指定
##### 
- `string`型でアセットバンドル名と対象のアセット名を指定する

##### 
- `AssetReference`型で直観的に指定できる
    - `AssetReferenceGameObject`型でプレハブのみに制限したり、
`AssetReferenceSprite`型でスプライトのみに制限したりできる
- `AssetLabelReference`型でラベルを指定できる
- `string`でキーやパスやラベルを指定することもできる

例えば、こんな感じでGroupsを設定すると…
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2419795/bf118788-6832-d517-7c14-aaa5a247b25a.png)

インスペクター上でこんな感じで指定できる
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2419795/96f11124-3fe8-8ed9-85eb-38e0d1edf714.png)


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2419795/23622bd0-a721-b8eb-e8b3-7818f7e1ed6b.png)

# その他知識
### キャッシュについて
キャッシュとは、リモートでアセットを読み込む場合に、いちいち何度も同じアセットの読み込みを通信せずに済ませる処理のことです。
Webとかでもよく目にするやつです。

AssetBundleやAddressablesではキャッシュが使えます。
僕自身もまだ理解してないのと、結構ボリューミーなんでこちらの記事を。

https://qiita.com/k7a/items/23d909ffeea3bab7dfcb

https://docs.unity3d.com/Packages/com.unity.addressables@2.0/manual/remote-content-assetbundle-cache.html


### リモートに配置したい場合
Addressablesの場合はリモートにバンドル配置してダウンロードしたい場合でもコード内容を変更せずに、Addressablesの設定でローカルとリモートを変更するだけで簡単に実装できます。

AWSというAmazonが提供するWebサービスのストレージサービスである**S3を利用するのが主流かつ楽です。**

アクセス権限とかの設定がちょっと面倒ですが、他の方法よりは楽です。
料金も余程大容量とかめちゃくちゃダウンロードする訳でなければ発生しないです。

ディレクトリ構成は、プラットフォームごとにフォルダ分けしておいて、Unity側でBuildTargetで分けるようにしておくのがおすすめです。




# さいごに
特に強い理由がなければ、全部Addressablesでいいと思います

アセット管理は結構機能が豊富で難しいですが、扱えるとものすごく便利なので使いたいですね〜

###  参考

##### 細かくて分かりやすい

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

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

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

##### 公式

https://docs.unity3d.com/ja/2022.1/Manual/AssetBundlesIntro.html

https://docs.unity3d.com/ja/Packages/com.unity.addressables@1.20/manual/LoadingAddressableAssets.html

https://learn.unity.com/tutorial/assets-resources-and-assetbundles?uv=2017.3#

##### その他

https://www.hanachiru-blog.com/entry/2022/05/12/120000

https://kingmo.jp/kumonos/unity-addressables-brief-summary/