Addressable Assets Systemを完全に理解する

# はじめに

Unite Toyko 2018で紹介のあったAddressable Assets Systemについて、触ってみて**完全に理解**しよう！という記事です。
講演資料：[【Unite 2018 Tokyo】そろそろ楽がしたい！新アセットバンドルワークフロー&リソースマネージャー詳細解説](https://www.slideshare.net/UnityTechnologiesJapan/unite-2018-tokyo-96499382/UnityTechnologiesJapan/unite-2018-tokyo-96499382)

# Addressable Assets Systemをなんとなく理解する

まず全容を把握するには、上記の講演資料をざっと見てみるのが一番良いと思います！
ここではもう少し掘り下げ、体系的にまとめてみます。

## Addressable Assets Systemでできること

今までのResources & AssetBundleの仕組みと比較して何ができるの？という観点からまとめると、以下のような感じです。

- できること
  - 任意の文字列（アドレス）によるアセットのロード
  - ResourcesとAssetBundleを等価的に扱うことができる（アドレスに対応するアセットの配置先を自由に変更することができる）
  - 参照カウンタによるアンロード管理
  - AssetBundleのシミュレーションモード（AssetBundleをビルドせずに動かす）
  - プロファイラによるロード状況の可視化
- できないこと
  - Variantsには非対応（アドレス解決時に工夫することで対応できるため、そもそも不要になった）

まさに、今まで各人が独自で実装してきたような所が全て解決できそうですね…！

## Addressable Assets Systemの内部構造

- Addressable Assets Systemは**Addressables**と**ResourceManager**の2つから成り立つシステム
  - AddressablesとResourceManagerは別パッケージになっている
  - AddressablesはResourceManagerに依存している
    - PackageManagerでAddressablesを導入すると、ResourceManagerが自動的についてくる
  - 全てC#で書かれている
- ResourceManagerはリソースのロード・アンロードを適切に管理するためのフレームワーク
  - 参照カウントによるアンロード管理を行う
  - DIによってロード処理などをカスタマイズできる
  - リソースのロード状況が見られるプロファイラなど、便利な機能が付いている
- Addressablesはアセットをアドレスを使って管理する為のシステム

前回Uniteの前に[こんな記事](https://qiita.com/k7a/items/8b0c6faa57f2bb8a68b5)を書きましたが、この時はResourceManagerについての調査のみしかしておらず、片手落ちな状態となっていました…。（ResourceManager内部の仕組みを知れたので有意義ではあったのですが）

## Addressablesの諸概念

Addressablesは、アセットを**Addressable Assets**として扱い、既存のワークフローを大きく変えるシステムです。
一旦これまでのAssetBundleの知識は捨ててしまって、以下の概念を覚えておきましょう。

- 全てのアセットは**グループ**に属する
  - グループごとにパッキング方法（1つのbundleにまとめる or 個別に分ける）や配置場所（ローカル、リモート等）などを変更できる
  - グループを移動してもアドレスが変わらなければコードを変更する必要は無い
- 全てのアセットは**アドレス**を持つ
  - アドレスは任意に指定可能である
  - アセットをスクリプトからロードする際にはアドレスを用いる
  - デフォルトではパスがアドレスとなる
- **AssetReference**を使うことで、アドレスを使わずにインスペクタ上から参照を設定できる
  - アドレスが変更されても追従できる
- アドレスとアセットの対応は**コンテンツカタログ**に記録する
  - jsonでシリアライズされる
  - 依存関係情報などが含まれる
  - 初期化時にロードされる
- **プロファイル**で設定を変更できる
  - ビルド先やロード元などの設定をプロファイル化でき、切り替えることができる
  - 例えばアセットのロード元を、ローカルのストレージ/ローカルのサーバー/ステージングのサーバー/本番のサーバなどそれぞれに切り替えるといった事ができる
- **プレイモード**で動作方法を変更できる
  - AssetBundleのビルドが不要なモード、ビルドされたAssetBundleからロードするモードなど

## ResourceManagerの設計

ResourceManagerはAddressablesのバックエンドにあるシステムで、以下のクラスから成り立ちます。

- ResourceLocation: アセットの場所と依存関係を持つ
- ResourceProvider: リソースのロード・アンロードの実装を持つ
- SceneProvider: シーンのロード・アンロードの実装を持つ
- InstanceProvider: インスタンスの生成・管理の実装を持つ

ResourceManagerは初期化時に使いたいProviderを登録し、ロード時にResourceLocationを渡すことで適切なProviderを用いてロード処理を行うという仕組みになっています。
Providerの実装は組み込みの物の他、自前で実装したものを登録して使う事も可能です。

Addressablesは以下のようにしてResourceManagerと連携します。

- 初期化時に適切なProviderを登録する
- アドレスをResourceLocationに変換して渡す

## 動作の流れ

一連の動作の流れを、分かりやすく図で表してみます。

### 初期化処理

![image.png](https://qiita-image-store.s3.amazonaws.com/0/97014/c3c5cd17-dff3-606e-222c-24860afb5d69.png)

Addressablesは、アプリの起動時（RuntimeInitializeOnLoadMethod）に

- コンテンツカタログのロード
- ResourceManagerへの各種Providerの登録（DI）

を行います。

### アセットのロード

![image.png](https://qiita-image-store.s3.amazonaws.com/0/97014/cf519b45-13d7-2b5a-350f-a5510720b129.png)

アセットのロードは、以下の流れで行われます。

1. Addressablesにアドレス（stringまたはAssetReference）を渡す
2. Addressablesは渡されたアドレスをResourceLocationに変換し、ResourceManagerへ渡す
3. ResourceManagerは渡されたResourceLocationを適切にロードするために、Providerを呼び出してAsyncOperationを組み立てて返す
4. 受け取ったAsyncOperationにロード完了時の処理を登録する

つまり、私達が実際に実装する部分は1と4になります。

# Addressable Assets Systemを完全に理解する

ここからは、実際に触ってみて**完全に理解**していきましょう。

## 導入方法

Unity 2018.2以降で新規プロジェクトを作成し、PackageManagerを使って導入します。
但し現在はpreview版なため、PackageManager上からインストールすることは出来ません。
そのため、manifest.jsonを以下のように編集します。

```manifest.json
{
  "dependencies": {
    "com.unity.addressables": "0.0.22-preview"
  }
}
```

現時点（2018/05/21）での最新版が0.0.22-previewなため、以降はこのバージョンを元に解説を行います。

なお、PackageManagerで導入したパッケージは特定のディレクトリに配置されているので、そこから**ソースコードを読むことができます**。
参考： [Unity Package Managerについて](https://qiita.com/k7a/items/cc5d09bc1000551b203f)

## アセットの用意

なんでもいいので、テスト用にアセットを用意します。
~/Assets/AddressableAssets以下に、以下のようなアセットを配置しました。
※ Resourcesフォルダ以下に置くと、Unityの仕様上全てアプリのバイナリに含まれてしまうため、Addressable Asset Systemを利用する場合は例えバイナリに含める場合でも他のフォルダ名を付ける必要があります。

- boys
  - boys_01.png
  - boys_02.png
  - boys_03.png
  - boys_04.png
  - boys_05.png
- girls
  - girls_01.png
  - girls_02.png
  - girls_03.png
  - girls_04.png
  - girls_05.png
- ui
  - buttons
    - ok.png
    - cancel.png

<img src="https://qiita-image-store.s3.amazonaws.com/0/97014/08d694f1-033e-7a92-3d04-5696c0463bab.png" width="400">

画像は[いらすとや](https://www.irasutoya.com/)さんからお借りしました。

## アセットにアドレスを付与する

### 初期設定

Window -> Asset Management -> Addressable Assetsから、管理ツールを開くことができます。
<img src="https://qiita-image-store.s3.amazonaws.com/0/97014/99c57967-2884-355a-746b-17b3b7cc6d5c.png" width="600">

Create Addressable Settingsをクリックすると、Addressable Assetsの設定が作成されます。
設定はAssets/AddressableAssetsData以下に保存されます。

<img src="https://qiita-image-store.s3.amazonaws.com/0/97014/b2ef0de7-b9aa-02bc-d648-a05aae53003d.png" width="600">

このデータを削除するとアドレスの設定などが消えてしまうため注意。
データが作成されると、以下のような表示になります。

<img src="https://qiita-image-store.s3.amazonaws.com/0/97014/ffd798b5-959f-3bc0-4b83-f9213ad55c6a.png" width="600">

### Addressablesウィンドウから設定する

Default Local GroupツリーにアセットをD&Dします。

<img src="https://qiita-image-store.s3.amazonaws.com/0/97014/e8f27425-3eb0-4406-637b-f070e3371fbb.png" width="600">

ディレクトリを入れるとこのようになりますが、**この状態では扱えません**[^1]。
灰色になっているアセットはアドレスが付与されておらず、アクセスする手段がありません。
アドレスを付与するには、グループ直下に各アセットを配置する必要があります。

[^1]: ディレクトリにアドレスが付与できるのは何の意味があるのか分かりませんが、正式リリースにおいて何らかの変更があるのかもしれません

現在の所、「特定ディレクトリ以下の全てのアセットを一括でAddressableにする」という機能が用意されていないため、手動でやるか、スクリプトで設定する必要があります。
面倒なのでディレクトリのエントリをいい感じに変換するスクリプトを作りました。アセットのルートディレクトリを入れて下記スクリプトを実行すると、全てのアセットがグループ直下に移動されます。

```cs
using UnityEditor.AddressableAssets;
using System.IO;

public static AddressableAssetsUtil
{
    [UnityEditor.MenuItem("Addressable/MoveSubEntryToRoot")]
    public static void MoveSubEntryToRoot()
    {
        var aaSettings = AddressableAssetSettings.GetDefault(false, false);

        // サブエントリを全てグループ直下へ移動
        var entries = aaSettings.GetAllAssets(true, true);
        foreach (var entry in entries)
        {
            entry.address = entry.assetPath;
            aaSettings.CreateOrMoveEntry(entry.guid, entry.parentGroup);
        }

        // 空になったディレクトリのエントリを削除
        entries = aaSettings.GetAllAssets(true, false);
        foreach (var entry in entries)
        {
            var isDirectory = Directory.Exists(entry.assetPath);
            if (isDirectory) aaSettings.RemoveAssetEntry(entry.guid);
        }
    }
}
```

以下のようになればOKです。

<img src="https://qiita-image-store.s3.amazonaws.com/0/97014/567a4cfd-1946-c024-8076-03851fd8841b.png" width="600">

但し、まだ一つ問題があり、**アドレスに2つ以上の /（スラッシュ）が含まれる場合、特定のケース[^2]で上手く動かない事がある**バグがあります。
その為、全てのアセットのエントリを選択した状態で右クリックし、**Simplify Entry Names**を実行しておきましょう。

[^2]: スラッシュが2個以上で含まれた状態でPacking ModeをSeparatelyにすると、AssetBundleのビルド時にこける

<img src="https://qiita-image-store.s3.amazonaws.com/0/97014/914a1fca-0092-7ae9-2cc1-8f0ddb43aef0.png" width="600">

これで、アセットにアドレスを使ってアクセスする準備が整いました。

### Inspectorからアドレスを設定する

今回は使用しませんでしたが、Inspector上からアセットのアドレスを編集することも可能です。

<img src="https://qiita-image-store.s3.amazonaws.com/0/97014/f81277c8-6d0a-f039-308e-a4e54c4d2cf4.png" width="600">

## アセットをロードする

適当にImageを配置したシーンを作ります。

<img title='image.png' src='https://qiita-image-store.s3.amazonaws.com/0/97014/5c10ca2f-21bc-785e-2f16-16ac7b2e6c01.png' width="300" data-meta='{"width":778,"height":808}'>

このImageを、Addressablesを使ってロードした他のアセットに差し替えてみましょう。

### アドレスを使ってロードする

まずImageに新規スクリプトをアタッチします。
以下のようなスクリプトにします。Startした瞬間に `boy_02` をロードし、m_imgのspriteを差し替えるだけのシンプルな例。

```cs
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.UI;

public class TestMonoBehavior : MonoBehaviour {
    public Image m_img;

    void Start()
    {
        // アドレスを指定して読み込む
        Addressables.LoadAsset<Sprite>("boy_02")
                    .Completed += op => { m_img.sprite = op.Result; };
    }
}
```

m_imgにはInspectorにImageの参照を設定します。

<img src='https://qiita-image-store.s3.amazonaws.com/0/97014/8874ac27-d1fc-a8b7-ccc5-90cbb7092ed3.png' width="600">

これで実行すると、Spriteが差し替わる事が確認できます。

<img src='https://qiita-image-store.s3.amazonaws.com/0/97014/d54162a1-2be6-6b97-acee-39f9bb31a323.png' width="300">

### AssetReferenceを使ってロードする

AssetReferenceを使うと、アドレスをハードコーディングすることなく、インスペクタ上からAddressableなアセットの参照を設定することができます。
先程のスクリプトを以下のように変更しましょう。

- 変更点
  - メンバにAssetReferenceを追加
  - LoadAssetの引数をAssetReferenceに変更

```cs
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.UI;

public class TestMonoBehavior : MonoBehaviour {
    // AssetReferenceTypeRestriction: 設定できるアセットのタイプを制限できる
    // Spriteだと上手くいかなかったのでTexture2Dにした
    [AssetReferenceTypeRestriction(typeof(Texture2D))]
    public AssetReference m_ref;
    public Image m_img;

    void Start()
    {
        // AssetReferenceを引数にして読み込む
        Addressables.LoadAsset<Sprite>(m_ref)
                    .Completed += op => { m_img.sprite = op.Result; };
    }
}
```

次に、Inspector上からAssetReferenceを設定します。先ほどとは違うアセットを設定してみましょう。
プルダウンメニューで選択できる他、D&Dで設定することもできます。

<img src='https://qiita-image-store.s3.amazonaws.com/0/97014/10d17ceb-8cf2-fbb1-8fa6-0bf0fb5fbe0f.png' width="600">

これで実行すると、Spriteが設定されたアセットに変更されることが確認できます。

<img src='https://qiita-image-store.s3.amazonaws.com/0/97014/154c732a-3770-ae82-54da-24acf693b136.png' width="300">

AssetReferenceの特徴として、**アドレスを変更しても変更が追従できる**という点があります。AssetReferenceは内部的にアセットのGUIDを保持しているため、GUIDが変わらない限りは追従できます。

## プロファイラでロード状況を確認する

Window -> Asset Management -> Resource Profilerでプロファイラのウィンドウが開きます。
プロファイラを開いた状態で実行してみると、ロード状況が表示されます。

<img src='https://qiita-image-store.s3.amazonaws.com/0/97014/b8046d73-3eb4-5204-dd15-72335155649a.png' width="600">

プロファイラからは以下の情報が読み取れます。

- ロードイベントが起こったタイミング
- ロードされたアセットのアドレス
- アセットの参照カウント
- アセットのロード元、ロード方法

## プレイモードを変更する

Addressablesウィンドウの右上の設定アイコンからプレイモードを変更することができます。

<img src='https://qiita-image-store.s3.amazonaws.com/0/97014/5d3675bc-9ea6-c797-0340-63441ebb5957.png' width="600">

- Fast Mode: パッキングせずに直接ファイルをロードする、速いがProfilerで得られる情報が少ない
- Virtual Mode: パッキングせずにAssetBundleに近い動作をシミュレートする
- Packed Mode: 実際にパッキングし、AssetBundleからロードを行う

実際にプレイモードを変更してみて、プロファイラでの表示がどう変わるかを見てみましょう。

### Fast Mode

![image.png](https://qiita-image-store.s3.amazonaws.com/0/97014/de63c6d4-29b9-b50d-4684-6c85091ac105.png)


このモードでは、[AssetDatabase\.LoadAssetAtPath](https://docs.unity3d.com/ja/current/ScriptReference/AssetDatabase.LoadAssetAtPath.html)を使って直接ファイルをロードしています。AssetBundleのビルドが不要なためすぐに実行できる反面、アセットがどのAssetBundleに含まれているかはプロファイラ上では確認できません。

### Virtual Mode

![image.png](https://qiita-image-store.s3.amazonaws.com/0/97014/c689b55a-4869-71d8-c971-cf4896bce645.png)


FastModeと違い、アセットがどのAssetBundleに含まれているかが確認できる事が分かります。
このモードも、最終的には[AssetDatabase\.LoadAssetAtPath](https://docs.unity3d.com/ja/current/ScriptReference/AssetDatabase.LoadAssetAtPath.html)でロードを行っています。
AssetBundleのビルドは不要なものの、最初にAssetBundleとアセットの対応関係を構築するため、Fast Modeに比べて処理コストが嵩みます。
また、仮想の通信速度を設定してロードに掛かる時間をシミュレーションすることも可能です。

### Packed Mode

![image.png](https://qiita-image-store.s3.amazonaws.com/0/97014/e8410737-ac45-d26b-c995-ae2f7423daf8.png)

このモードでは、実際にAssetBundleをビルドしてロードします。エディタでの実行時には、未ビルドの際は**自動的にビルドを行います**[^3]。（判定方法などの詳細は未調査）
プロファイラで見れる情報としてはVirtualModeとほとんど変わりありません。
最終的には、[AssetBundle\.LoadFromFileAsync](https://docs.unity3d.com/jp/current/ScriptReference/AssetBundle.LoadFromFileAsync.html)や[UnityWebRequestAssetBundle\.GetAssetBundle(URI uri)](https://docs.unity3d.com/ja/current/ScriptReference/Networking.UnityWebRequestAssetBundle.GetAssetBundle.html)を使ってAssetBundleをロードしています。

[^3]: ちょっと使いにくい挙動なので、カスタマイズできるようになると嬉しい

## アセットの配置先（ロード元）を変更する

アセットの属するGroupTypeを変更することで、アセットの配置先（ロード元）を詳細に変更する事が可能です。
GroupTypeは、Addressablesウィンドウ上でグループを右クリックし、コンテキストメニューから変更する事が可能です。

GroupTypeには以下の3つがあります。

- Local Packed Content（デフォルト設定）
  - ローカルから読む専用の設定
  - **StreamingAssetsにビルドし、StreamingAssetsからロードする**
  - ロードにはLocalAssetBundleProviderが用いられる
    - 内部的には[AssetBundle\.LoadFromFileAsync](https://docs.unity3d.com/jp/current/ScriptReference/AssetBundle.LoadFromFileAsync.html)を使っている
  - 詳細設定は不可
- Remote Packed Content
  - Localと違い、**ビルド先とロード元を設定できる**
  - ロードにはRemoteAssetBundleProviderが用いられる
    - 内部的には[UnityWebRequestAssetBundle\.GetAssetBundle(URI uri)](https://docs.unity3d.com/ja/current/ScriptReference/Networking.UnityWebRequestAssetBundle.GetAssetBundle.html)を使っている
    - versionやhashは渡していないのでキャッシュは行われない
    - UnityWebRequestはローカルのファイルもロード可能なので、ロード元を `file://` にする事でローカルからロードする事も可能
- Advanced Packed Content
  - Remoteと同じく、ビルド先とロード元を設定できる
  - **ロードに用いるProviderを指定することができる**
    - ロード処理をカスタマイズしたい（暗号化、独自プロトコルなど）場合に利用

### アセットをサーバーからロードする

今回はローカルでサーバーを立ち上げてそこからロードしてみます。

#### グループの設定

Addressablesウィンドウから、グループを右クリックしてRemote Packed Contentに変更します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/97014/ecb5de12-daa4-fdb6-95ba-9ca04c1ff6a1.png)

この状態でグループを選択すると、ウィンドウ下部に設定項目が現れます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/97014/fa64d602-7c46-e293-e30f-0de543d7d47d.png)

- Packing Mode: AssetBundleのパッキング方法を選択する。
  - Pack Together: 1つのAssetBundleにまとめる
  - Pack Separately: 個別のAssetBundleに分ける
- Build Path: AssetBundleのビルド先
- Load Prefix: ロード時に使用するパス
  - 要はProvider（UnityWebRequest）に渡すstringのパターンで、 `{Load Prefix}/{AssetBundleのパス}` が最終的なURIになる

ここで、Packing ModeをSeparatelyに、Build Path、Load PrefixをRemoteに変更してみます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/97014/4e04ef0e-5bed-e9fc-e228-2b799664f3c2.png)

Load Prefixがlocalhostになりました。
この状態でプレイモードをPackedにして実行すると、当然ながらロードに失敗します。

#### サーバーへのAssetBundleの配置

ここで、ServerData以下にAssetBundleがビルドされている事を確認します。

<img src='https://qiita-image-store.s3.amazonaws.com/0/97014/67721b11-ccfc-42ee-ebf4-62976e0147fe.png' width="600">

もし正しくビルドされていない場合は、キャッシュの影響でビルドされていない可能性があります。以下のようなスクリプトを用意して実行しましょう[^4]。

[^4]: 現状では手動でビルドする方法が直接スクリプトからAPIを叩くしか無く、若干不便

```cs
using UnityEditor.AddressableAssets;

public static AddressableAssetsUtil
{
    [UnityEditor.MenuItem("Addressable/Build")]
    public static void Build()
    {
        // BuildTargetはよしなに設定すること
        BuildScript.PrepareRuntimeData(true, false, true, true, false, UnityEditor.BuildTargetGroup.Standalone, UnityEditor.BuildTarget.StandaloneOSX);
    }
}
```

AssetBundleが用意できたら、ローカルでAssetBundle配信サーバーを立てましょう。Apacheでもnginxでもなんでもいいですが、ドキュメントルートをビルド先 `{プロジェクトのパス}/ServerData` に設定します。
要は `http://localhost/{BuildTarget}/{AssetBundleのパス}` でAssetBundleのファイルにヒットする状態にします。
Macなら標準で入っているApacheでも使うのが楽でしょう。（サーバーの立て方については話が逸れるのでここでは詳しく触れません）

サーバーを立てた状態で実行し、正常にロードができると、プロファイラでlocalhostから読み込まれている事が確認できます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/97014/a5245c08-9e3d-c05e-5eb4-785fb8fd99b7.png)

### プロファイルを用いたロード元の切り替え

Build Path、Load Prefixの設定は、**プロファイル**を用いて変更することができます。
これによって、例えばロード元をfile://、localhost、ステージングサーバー、本番サーバーにそれぞれ切り替えるといったような事を、プロファイルを切り替えるだけで実現する事が可能です。

ウィンドウの左上からManage Profilesを選択するとプロファイル設定ウィンドウが開きます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/97014/24a50766-78fe-9289-f996-3d6d56e6a0e0.png)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/97014/d32f8441-03df-ee73-3433-507a0c96b189.png)

#### file://とlocalhostの切り替えを行う

試しに、file://とlocalhostの切り替えをプロファイルを使って行ってみます。
Manage Profile listボタンからプロファイルを追加することができます。「Remote」プロファイルを追加し、以下のような設定にしてみます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/97014/0d50a038-a96a-52b1-a4c7-63e491de803d.png)

MyLoadPrefixエントリを追加[^5]し、以下のように設定しました。

- Default[^6]: `file://[AssetPath.ProjectPath]/ServerData/[UnityEditor.EditorUserBuildSettings.activeBuildTarget]`
  - プロジェクトのパスを取得するAPIは標準では無いため、自身で用意する（下記参照）
- Remote: `http://localhost/[UnityEditor.EditorUserBuildSettings.activeBuildTarget]`

[^5]: 既存のエントリを編集すると何故か全てのプロファイルの値が書き換えられてしまう(?)為、新たなエントリを作成する
[^6]: Defaultプロファイルの名前は変更すると怪しい挙動をするので、変更しないほうが無難

プロファイルの値は`[]`で囲むことでstaticな変数を利用する事ができます。スクリプトからプロファイルの値を制御したい場合は、自身でstaticなプロパティを書くことで実現できます。
ここでは、ServerDataディレクトリのパスを取得するために、プロジェクトのパスを取得する必要があるため、以下のようなプロパティを実装します。

```cs
public static class AssetPath
{
    public static string ProjectPath
    {
        get
        {
            return System.IO.Directory.GetCurrentDirectory();
        }
    }
}
```

プロファイルが設定できたら、グループのLoadPrefixをMyLoadPrefixに変更しましょう。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/97014/c77c30dd-e58c-8f09-09d5-c0ad726f19d3.png)

この状態で左上のメニューからプロファイルを選択すると、Load Prefixが切り替わることが確認できます。実行してプロファイラで確認すると、ロード元が正常に切り替わっている事が確認できます。

## ラベルを付けて一括ロードする

アセットにはアドレスの他に**ラベル**を付ける事ができ、「特定のラベルが指定されているアセット全てを読み込み」といったようなことが可能です。
試しに以下のように「girls」ラベルを付けてみてロードしてみます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/97014/ca87c23f-bfba-17d7-b151-3e935eed6c8b.png)

以下のコードを適当な場所に挟みます。

```cs
Addressables.PreloadDependencies("girls", null);
                    .Completed += op => {
                        Addressables.LoadAsset<Sprite>("girl_01");
                    };
```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/97014/64ea5009-7393-c42b-894c-3b9e9a913ce3.png)

ラベルの付けられたアセットのAssetBundleがロードされ、すぐにアンロードされている事が分かります。
この状態で `girl_01` をロードすると、AssetBundleが既にダウンロードされているため、ダウンロード済みのAssetBundleから展開してロードします。
予めダウンロードしておきたいAssetBundleにラベルを付けておき、それを使ってロードするといった用途に使えそうです。

## コンテンツカタログの仕組み

ビルドされたAssetBundleと共に、**catalog_1.json**という意味深なファイルがあることに気付くと思います。

<img src='https://qiita-image-store.s3.amazonaws.com/0/97014/67721b11-ccfc-42ee-ebf4-62976e0147fe.png' width="600">

これが**コンテンツカタログ**の正体である事は察しが付きますね。ですが、**catalog_1.hash**という謎なファイルがあるのも気になります。
ということで、コンテンツカタログの仕組みを見ていきます。

### コンテンツカタログの中身

結論から言うと、それぞれのファイルの正体は以下です。

- catalog_1.json: コンテンツカタログ（ResourceLocationList）をJSONUtilityでシリアライズしたもの
- catalog_1.hash: 上記jsonのMD5ハッシュ値

中身はこんな感じです。（jsonはminifyされている物を見やすく整形しています）

```catalog_1.json
{  
   "locations":[  
      {  
         "m_address":"SampleScene",
         "m_guid":"2cda990e2423bbf4892e6590ba056729",
         "m_type":0,
         "m_typeName":"UnityEngine.SceneManagement.Scene",
         "m_internalId":"Scenes/SampleScene",
         "m_provider":"UnityEngine.ResourceManagement.SceneProvider",
         "m_labelMask":0,
         "m_dependencies":[  

         ],
         "m_isLoadable":true
      },
      {  
         "m_address":"0",
         "m_guid":"2cda990e2423bbf4892e6590ba056729",
         "m_type":1,
         "m_typeName":"UnityEngine.SceneManagement.Scene",
         "m_internalId":"Scenes/SampleScene",
         "m_provider":"UnityEngine.ResourceManagement.SceneProvider",
         "m_labelMask":0,
         "m_dependencies":[  

         ],
         "m_isLoadable":true
      },
      {  
         "m_address":"defaultlocalgroup_assets_boy_01.bundle",
         "m_guid":"",
         "m_type":0,
         "m_typeName":"UnityEngine.AssetBundle",
         "m_internalId":"file:///Users/user/Addressable Test/ServerData/StandaloneOSX/defaultlocalgroup_assets_boy_01.bundle",
         "m_provider":"UnityEngine.ResourceManagement.RemoteAssetBundleProvider",
         "m_labelMask":0,
         "m_dependencies":[  

         ],
         "m_isLoadable":false
      },
      {  
         "m_address":"boy_01",
         "m_guid":"7276a10699bf545ab88993a205239a28",
         "m_type":0,
         "m_typeName":"UnityEngine.Texture2D",
         "m_internalId":"Assets/AddressableAssets/boys/boy_01.png",
         "m_provider":"UnityEngine.ResourceManagement.BundledAssetProvider",
         "m_labelMask":0,
         "m_dependencies":[  
            "defaultlocalgroup_assets_boy_01.bundle"
         ],
         "m_isLoadable":true
      }
      //~中略~//
   ],
   "labels":[  
      "default",
      "girls"
   ]
}
```

```catalog_1.hash
ac03d2db95e6f339f366f0cc39218c87
```

### コンテンツカタログのロード処理

流れとしては以下のような感じです。

- 起動時に[RuntimeInitializeOnLoadMethod](https://docs.unity3d.com/ScriptReference/RuntimeInitializeOnLoadMethodAttribute.html)でロード処理が呼ばれる
- リモートにcatalog_1.hashを読みに行く
- ローカルのキャッシュのハッシュ値と比較し、catalog_1.jsonをロードする
  - ハッシュ値が同じならローカルのキャッシュからロード
  - ハッシュ値が違う場合はリモートからロードし、[Application.persistentDataPath](https://docs.unity3d.com/ja/current/ScriptReference/Application-persistentDataPath.html)以下にキャッシュする

**ハッシュ値によってキャッシュが行われている**所がポイントですね。　更新されない限りはハッシュ値分しか通信は発生しません。

### コンテンツカタログ自体の配置先の解決について

コンテンツカタログのロードの仕組みはわかりましたが、コンテンツカタログ自体の配置先はどのように解決しているのか？というのが疑問に思われるかと思います。

コンテンツカタログの配置先の情報は**ResourceManagerRuntimeData**に格納されています。ResourceManagerRuntimeDataは**ビルド時に生成され、jsonでシリアライズされ、StreamingAssets以下に配置されます**。

つまり、アプリをビルドする時はResourceManagerRuntimeDataの生成処理を事前に挟む必要があります。
`UnityEditor.AddressableAssets.BuildScript.BuildPlayer`を使うと、事前に生成を行ってからアプリをビルドしてくれます。
（エディタでの実行時には毎回自動で生成したものを読み込みます）

ResourceManagerRuntimeDataの配置先は決め打ちで `StreamingAssets/ResourceManagerRuntimeData_settings.json` になっており、コンテンツカタログのロード前にこれをロードしています。
実際の中身は以下のようになっており、コンテンツカタログの場所が記されていることが分かります。

```ResourceManagerRuntimeData_settings.json
{  
   "settingsHash":"13f9f4a40ed6bec5d9ad350892e96fdf4",
   "catalogLocations":{  
      "locations":[  
         {  
            "m_address":"0_RemoteCatalog_102d3183f9bcc41febf5fd015f404569",
            "m_guid":"",
            "m_type":0,
            "m_typeName":"",
            "m_internalId":"http://localhost/StandaloneOSX/catalog_{ContentVersion}.json",
            "m_provider":"UnityEngine.AddressableAssets.ContentCatalogProvider",
            "m_labelMask":1,
            "m_dependencies":[  
               "LocalCatalogHash102d3183f9bcc41febf5fd015f404569",
               "RemoteCatalogHash102d3183f9bcc41febf5fd015f404569"
            ],
            "m_isLoadable":true
         },
         {  
            "m_address":"LocalCatalogHash102d3183f9bcc41febf5fd015f404569",
            "m_guid":"",
            "m_type":0,
            "m_typeName":"",
            "m_internalId":"{UnityEngine.Application.persistentDataPath}/catalog_{ContentVersion}.hash",
            "m_provider":"UnityEngine.ResourceManagement.TextDataProvider",
            "m_labelMask":0,
            "m_dependencies":[  

            ],
            "m_isLoadable":false
         },
         {  
            "m_address":"RemoteCatalogHash102d3183f9bcc41febf5fd015f404569",
            "m_guid":"",
            "m_type":0,
            "m_typeName":"",
            "m_internalId":"http://localhost/StandaloneOSX/catalog_{ContentVersion}.hash",
            "m_provider":"UnityEngine.ResourceManagement.TextDataProvider",
            "m_labelMask":0,
            "m_dependencies":[  

            ],
            "m_isLoadable":false
         }
      ],
      "labels":[  

      ]
   },
   "usePooledInstanceProvider":false,
   "profileEvents":true,
   "contentVersion":"1",
   "assetCacheSize":25,
   "assetCacheAge":5.0,
   "bundleCacheSize":5,
   "bundleCacheAge":5.0
}
```

`http://localhost/StandaloneOSX/catalog_{ContentVersion}.json` となっていますが、ContentVersion自体もResourceManagerRuntimeDataに含まれているため、現状では**アプリをビルドしない限りコンテンツカタログのロード先は変更できない**ようになっています。
この辺りはそのうち変更できるようになりそうな気はします。

# 最後に

以上で現状使える機能はかなりの部分押さえられたと思います。**Addressable Assets System、完全に理解した。**
まだpreview版ということでバグがあったり使い勝手の良くない所はありますが、これまでのAssetBundle関係の悩みは大分解決されそうなので、正式リリース[^7]に期待したいですね！

[^7]: Unity 2018.2に向けて作られており、2018.2のリリースは夏予定

個人的には、以下の機能が欲しいなと思います。

- アドレスをAssetBundleGraphToolのようにノードベースで設定できる機能
- ビルドの実行を任意のタイミングで手軽にできる機能
  - 勝手にビルドされてコンテンツカタログが更新されたりするので困る
- ロード時のAssetBundleのパスのパターンを任意に変更できる機能（Load Suffix的な）
  - バージョン番号やハッシュを末尾に付けたい
- AssetBundleのキャッシュ対応
  - Providerを自分で書けば対応できそうだが、標準で対応して欲しい

AssetBundleGraphToolと統合してくれると本当に嬉しい…嬉しい…