[Unity] Resources.Loadの代わりにAddressable Asset Systemを使ってみる

# はじめに
Unityで動的にアセットを読み込む際、Resources.Loadの使用はベストプラクティスでないという話があります。
（ただし、Resources.Loadは手軽で便利という側面もあり、例えばゲームジャムやプロトタイプなど、ケースバイケースで採用する局面は多々あると思います。）

プロジェクト内のローカルアセットを、Addressable Asset System（以下Addressables）でとりあえず読み込むには、最低限どうするのかまとめました。

# 想定する読者層

- 非同期とか苦手な初心者
- サーバーとかの用意なく、Addressablesをとりあえず試したい人
- Resources以外のフォルダに散らばったアセットを、ロードしたい人
- 昔の方法であるAssetBundle（自前ツール的な用意がいろいろ必要なやつ）は、よく知らない人

# 利用できるシチュエーション
- 配布したゲームをバージョンアップする際に、更新したアセットに相当する最低限の差分ファイルを、別途パッチとして配布するなど？ （スマホ以外のプラットフォームで）

# 記事で利用した動作環境
- Unity2018.4.2f1 / 2019.2.17f1
- Addressables 1.2.4 / 1.5.0

# 題材のアプリ例
下図のように、人の絵のSpriteをスクリプトで動的にロードし表示する。
![MyProjectCapture20191223101339.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275766/6a5ca938-cd99-ff4f-85fc-775aee2359b5.png)

#Resources.Loadでロードする（Addressablesとの比較用）

- Projectウィンドウにhito.pngを配置する。

<img width="217" alt="スクリーンショット 2019-12-23 10.40.10.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275766/21e4e2ad-d5f1-70aa-6d7b-610ddfbe9b08.png">

- 任意のSpriteオブジェクトをシーンに用意
- 以下のスクリプトを任意のオブジェクトにアタッチ
- アタッチしたスクリプトのtargetSpriteにシーンのSpriteをアタッチ

```C#:GameController.cs
using UnityEngine;

public class GameController : MonoBehaviour
{
    // シーン上にSpriteオブジェクトを用意し、以下にアタッチしておく
    [SerializeField] SpriteRenderer targetSprite = null; 

    Sprite mySprite = null;

    void Start()
    {
        LoadSprite();
    }

    void LoadSprite()
    {
        // 画像をロード
        mySprite = Resources.Load<Sprite>("hito");
        // シーンに配置したSpriteオブジェクトにセット
        targetSprite.sprite = mySprite;
    }
```

# Addressablesでロードする
### 前準備
Window > PackageManagerからAddressablesを選びインストール
<img width="703" alt="スクリーンショット 2019-12-23 10.48.41.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275766/f5ed229b-4d9c-222f-ca79-bd636b6f643c.png">

Window > Asset Management > Addressables > Groups
<img width="513" alt="スクリーンショット 2019-12-22 17.19.16.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275766/f5c83ef4-59bf-190f-36de-69529589f8c3.png">

「Create Addressables Settings」のボタンが表示される場合は、押す
<img width="744" alt="スクリーンショット 2019-12-22 17.19.30.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275766/ad0c0ad9-50d0-0e0d-e728-bb1db893b858.png">

「Default Local Group」は、任意にわかりやすい名前に変えて良い。
（複数のアセットを、グループごとに管理するなら）
名前の変更は右クリック > Renameか、Macなら選択してEnterキー、WindowsならF2。
<img width="255" alt="スクリーンショット 2019-12-22 17.21.39.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275766/99ee5826-8915-eecf-de08-254d9c538242.png">

上記Addressables Groupウィンドウで、使うGroupを選択した状態で、Inspectorを確認。
Content Packing & Loading > Advanced Optionsを展開し、下から4行目あたり。
<img width="250" alt="スクリーンショット 2019-12-22 17.20.50.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275766/c199ecf2-fac5-a510-96fa-58980c85d1b5.png">

さきほどのGroupごとに1つのアセットを固めたBundleファイルが最終的に作成されて良ければ、Bundle Modeを「Pack Together」（デフォルト）。
１アセット（= この記事のhito.pngといった１画像ファイル）ごとに、1 Bundleファイルを作成したければ「Pack Separately」。

### 画像アセットをAddressablesに登録する
- Projectウィンドウで、該当の画像ファイルを選択する
- Inspectorの最上部に、Addressablesというチェックボックスがあるので、チェック。
なお、画像ファイルの場所はResourcesフォルダ以外でもよい。

<img width="283" alt="スクリーンショット 2019-12-23 12.05.36.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275766/63b3725c-45a7-7f4d-bad6-d18941606e20.png">

チェックを付けると、Addressables Groupsウィンドウで(Default)と付いているGroupsに追加される。
複数Groupを用意している場合、Addressables Groupsウィンドウで右クリックからDefaultのGroupを変えられる。
また、アセットをProjectウィンドウから目当てのGroupへドラッグ、あるいは、Group間でドラッグ＆ドロップし、どのGroupに属するかを変えても良い。

- Addressableの名前がパスのままなので、必要に応じて短くする（右クリックから「Simplify〜」が便利）

<img width="482" alt="スクリーンショット 2019-12-23 12.19.39.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275766/19f2b021-cb8e-eda6-f187-e6724c777e91.png">

名前の変更も可能。
（ここではファイル名は「hito1」だが、Addressable Nameは「hito」とした）。
<img width="381" alt="スクリーンショット 2019-12-23 13.33.08.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275766/6e3caf23-2919-e8b1-8eb8-02fbd6b47c2b.png">

### コードの記述概要
Addressablesの場合は、ロード後の処理は非同期、すなわち、ロードが裏で行われ完了後に呼ばれる。
この記事ではLoadAssetAsync[^1]を使った、2パターンの記述方法を記載する。
UniTaskなどを使えば、よりスマートな記述もできるかも（この記事では記載しない）。

### Addressablesでロードする記述（その１）

```C#:GameController.cs
using UnityEngine;
using UnityEngine.AddressableAssets; // 追記を忘れずに

public class GameController : MonoBehaviour
{
    [SerializeField] SpriteRenderer targetSprite = null;

    Sprite mySprite = null;

    void Start()
    {
        LoadSprite();
    }

    void LoadSprite()
    {
        // Addressablesによる読み込み
        Addressables.LoadAssetAsync<Sprite>("hito").Completed += handle =>
        {
            // ロードに成功した場合の処理をここに
            mySprite = handle.Result;

            if (targetSprite != null)
                targetSprite.sprite = mySprite;
        };
    }
```

画像のロード完了前に、例えばユーザーがステージ上の「戻る」ボタンなどを押し、シーンがすでに遷移しているケースもありうる。
そのため、上記ソースでは、シーン上のSpriteオブジェクトであるtargetSpriteをnullチェックしている。

### Addressablesでロードする記述（その2）
```C#:GameController.cs
using UnityEngine;
using UnityEngine.AddressableAssets; // 追記を忘れずに

public class GameController : MonoBehaviour
{
    [SerializeField] SpriteRenderer targetSprite = null;

    Sprite mySprite = null;

    void Start()
    {
        StartCoroutine(LoadSprite());
    }

    IEnumerator LoadSprite()
    {
        // Addressablesによる読み込み
        Addressables.LoadAssetAsync<Sprite>("hito").Completed += handle =>
        {
            //ロードに成功した場合
            mySprite = handle.Result;
        };

        // ロード完了のフレームまで待つ
        do
        {
            yield return null;
        }
        while(mySprite == null);

        // シーンに配置したSpriteオブジェクトにセット
        if (targetSprite != null)
            targetSprite.sprite = mySprite;

        // （このあとに何か行いたい次の処理があれば、記述）
        ...
    }
```

やってることは（その１）と同じで、コルーチン内で処理順に書いている。

こうすることで、例えば仮に、

- ステージの出題データα（Addressablesデータα）を読み込み
　　　 ↓

- データαをもとに、それに適したSpriteデータβ（Addressablesデータβ）を読み込み

と、前の処理の完了を必ず待ってから、次の処理を行うシチュエーションであっても、処理順に記載すればよい。

※ // ロード完了のフレームまで待つ  do{ 〜 } while...;の4行は、以下のように書いても構いません。（2022年5月6日追記）
```C#
        // ロード完了のフレームまで待つ
        yield return new WaitUntil( ()=> mySprite != null);
```


### ロードしたリソースを解放する記述
```C#:GameController.cs
    void OnDestroy()
    {
        if (mySprite != null)
        {
            Addressables.Release(mySprite);
        }
    }
```

Addressables.Releaseでリソースを解放する。
[ドキュメント](https://docs.unity3d.com/Packages/com.unity.addressables@1.5/manual/index.html)によると、実際には、Addressables.LoadAssetAsyncで+1された参照回数が、Addressables.Releaseで-1され、あとは適切なタイミングて勝手に判断されて解放される模様。
ここでは、スクリプトをアタッチしたオブジェクトが破棄されるタイミングでAddressables.Releaseできるよう、OnDestroy()に記載。

### ビルド前に必須の手順
Editor上でPlayする限りでは、上記までの手順で良い。
（正確には、Addressables Groupsウィンドウの「Play Mode Script」が一番下のUse Existing Build（= 旧 Packed Play Mode）以外であれば。）
<img width="553" alt="スクリーンショット 2019-12-23 14.35.03.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275766/b4e7c896-ae92-3bc5-f746-cea78c542221.png">

ビルドし実行可能ファイルを作成する際は、その前にAddressables GroupsウィンドウのBuild > New Build > ... （旧 Build Player Content）を実行し、Bundleファイルを作成する。
<img width="706" alt="スクリーンショット 2019-12-23 14.17.59.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275766/bc6308d0-d1e4-89f0-27ca-f0948e6056dd.png">
この手順は、**Addressablesに含めるアセットが変更されるたびに必要**となる。

その後のBuild Settingsからのビルド手順は、通常のアプリ作成と変わらない。
以上で、Addressablesでロードしたアプリは作成できる。

# まとめ
Addressablesを利用したロードは、Resources.Loadと比べて使い勝手に以下の違いがある。

- ロード対象のアセットをResourcesフォルダに置かなくてて良い。
- ファイル名とは異なる別名を付けられる
- 読み込み後の処理について、非同期なコード記述が必要
- ビルド前に、Addressables GroupsウィンドウのBuild > New Build の手順を忘れずに（Addressablesに含めるアセットを変更した場合）

# おまけ: 画像を変更したバージョンアップをする
題材のアプリで、絵を新たに差し替えたバージョンアップ版をリリースするシチュエーションを想定する。
![MyProjectCapture20191223164043.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275766/a1ac34a6-2b2d-2b23-74a7-4696e3a79ab3.png)
（表示する絵が変わったバージョンアップ版）

バージョンアップ版の作成手順としては、以下。
1. 画像アセット「hito1.png」について、レタッチソフトなどで中身を差し替える。
2. Addressables GroupsウィンドウのBuild > New Build > ...
3. Build Settingsでビルド

あるいは、上記1.の代わりに、次の手順でもよい。
1'. 画像アセット「hito2.png」を新たに用意し、これについてAddressables Groupsウィンドウで「hito」の名前を指定する。
（最初のhito1.pngについては、Addressablesのチェックを外す）

例えばMac OS X用のビルドでは、できた実行ファイル○○.appを右クリック > 「パッケージの内容を表示」で、ファイル群が確認できる。
ファイル差分ツールで、バージョンアップ前後で変更されたファイルを見比べてみると、以下のファイルが変わっている。
（ファイルの細かなパスについては省略。また、ファイル名は、前述したBundle Modeの設定が「Pack Separately」の場合）

- catalog.json
- spritesgroup_assets_hito_○○.bundle （○○部分はGuid文字列）

バージョンアップ後のビルドで作成された上記2つの差分ファイルを、バージョンアップ前のappにコピー（catalog.jsonは上書き）してみると、確かにバージョンアップ後の動作になる。

# 参考リンク

- [Unityドキュメント Unity Addressable Asset system (1.5.0)](https://docs.unity3d.com/Packages/com.unity.addressables@1.5/manual/index.html)
- [SECTION31 【Unity】2019年Addressable System の使い方【Preview0.8.6版】](https://section31.jp/gamedevelopment/unity/addressablesystem-4/)

[^1]: 今回取り上げないが、次のロードメソッドもそれぞれ便利そう。InstantiateAsync → Prefabをロードしインスタンス化する、DownloadDependenciesAsync → 指定したラベルをもとにロード（特に依存関係のあるアセットで）、LoadSceneAsync → Addressablesなシーンをロード