Synamon’s Engineer blog

Synamonはリアルとデジタルの融合を加速させるため、メタバース領域で法人向けにサービス提供を行うテックカンパニーです。現在開発を進めている「メタバース総合プラットフォーム」をはじめ、メタバース市場の発展に向けた事業展開を行っています。このブログでは、メタバース技術とその周辺の技術、開発全般に関してエンジニアがお話しします。

Addressable Asset System入門

エンジニアの小松(@vtuber_watch)です。Synamon Advent Calendar 2021の21日目です。

UnityのAddressable Asset Systemの入門記事になります。Addressablesの紹介記事は多くありますが情報が古かったり必要な説明が抜けていたりするので最新のバージョンにおける情報をまとめています。

この記事では基本的な使い方を紹介します。アセットバンドルをビルドしてローカルからロードするまでをやります。これだけでもAddressablesの恩恵を受けられます。記事が長くなりすぎたのでアセットバンドルをサーバに配置する使い方は別記事に分割します。

Addressable Asset Systemとは

Addressablesを使うとUnityのアセットをアセットバンドルにまとめていい感じに読み込みできるようになります。

Addressablesによって得られるメリットはたくさんありますがここではメモリ使用量の削減とアセットバンドルの扱いの2点から紹介します。

メリット1. メモリ使用量の削減

Addressablesを使うとメモリ使用量を減らせます。この点については以下のUnity Blogの記事で紹介されています。

記事の中では3つのプレハブ(Sword, Boss Sword, Shiled)をランタイムに生成する例を使って説明されています。

public class SampleMonoBehaviour : MonoBehaviour
{
    // Addressablesを使わない場合
    [SerializeField] private GameObject prefab;

    // Addressablesを使う場合
    [SerializeField] private AssetReferenceT<GameObject> prefabReference;
}

Addressablesを使わない場合、MonoBehaviourのフィールドにプレハブをGameObjectとして持ち、必要なときにインスタンス化します。
この方法はメモリ効率が悪いです。プレハブを直接参照しているためこのMonoBehaviourが存在する限りプレハブ全体がメモリに載ってしまいます。

Addressablesを使う場合、MonoBehaviourのフィールドに持つのはGameObjectではなくAssetReferenceTになります。AssetReferenceTはGameObjectそのものではなくGameObjectへの参照です。必要に応じてプレハブを取得してインスタンス化します。
これによって必要なときだけプレハブがメモリに載るようになります。不要になったときにアンロードすることもできます。

メリット2. アセットバンドルを簡単に使える

Addressables以前は、アセットバンドルを実用するには自前で管理するためのシステムを用意する必要がありました。アセットバンドルの構成設定とビルド、ロードとアンロード、依存関係の解決などかなりの実装が必要です。

Addresssablesではこれらの機能がすでに用意されており、簡単にアセットバンドルを使い始められます。多くの機能が拡張できるように作られているので必要な部分だけカスタマイズして使えます。

パッケージのインポートと初期設定

Addressablesを使い始めるにはまずパッケージをインストールします。

1. メニューのWindow/Package ManagerからPackageManagerを開き、Addressablesを選んでInstallボタンを押します。
f:id:sakanox:20211221161925p:plain

2. インストールが完了したらメニューのWindows/Asset Management/Addressables/Groupsを押します。
f:id:sakanox:20211221162152p:plain

3. Create Addressables Settingsボタンを押します。
f:id:sakanox:20211221162534p:plain

以上でAddressablesがインストールされ、必要な設定ファイルが生成されました。

Addressablesの設定ファイルはAssets/AddressableAssetsDataフォルダに配置されます。

その中のAddressableAssetSettingsが全体の設定になります。
それぞれの設定の説明はマニュアルを見てください。今はデフォルトのままで大丈夫です。
f:id:sakanox:20211221163237p:plain

使ってみる

初期設定は出来たのでとりあえず使ってみます。

プレハブをアドレス可能(Addressable)にする

まずはサンプルとして使うプレハブを作ります。何でもいいですがここではデフォルトのCubeだけのプレハブを作ります。

作成したプレハブを選択すると、インスペクタの一番上に「Addressable」というチェックボックスがあります。ここにチェックを入れるとそのアセットはアドレス可能(Addressable)になります。アドレス可能なアセットがAddressablesシステムでロードできます。

チェックボックス隣のテキストはこのアセットのアドレスです。デフォルトではアセットのパスになっていますが自由に変更できます。このアドレスを指定してアセットをロードします。
f:id:sakanox:20211221164443p:plain

プレハブをインスタンス化する

作成したプレハブをロードしてみます。

ロードする方法はいくつかありますがここでは3つ紹介します。

  1. Addressables.LoadAssetAsyncはアドレスを指定してアセットをロードできます
  2. Addressables.InstantiateAsyncはアドレスを指定してプレハブをインスタンス化できます。
  3. AssetReferenceTはインスペクタでアセットを設定してアセットのロードやインスタンス化ができます。
1. Addressables.LoadAssetAsyncを使う

以下のスクリプトをシーンに配置して実行してください。プレハブがロードされてインスタンス化されます。

using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;

public class SpawnerByLoadAssetAsync : MonoBehaviour
{
    private AsyncOperationHandle<GameObject> prefabHandle;
    private GameObject spawnedGameObject;

    private async void Start()
    {
        // Addressables.LoadAssetAsyncで読み込む
        prefabHandle = Addressables.LoadAssetAsync<GameObject>("Assets/Prefabs/Cube.prefab");

        // .Taskで読み込み完了までawaitできる
        GameObject prefab = await prefabHandle.Task;

        // 読み込んだプレハブをインスタンス化する
        spawnedGameObject = Instantiate(prefab);
        spawnedGameObject.name = "Spawned Game Object";
    }

    private void OnDestroy()
    {
        // インスタンス化したGameObjectを破棄する
        Destroy(spawnedGameObject);

        // 使い終わったらhandleをリリースする
        Addressables.Release(prefabHandle);
    }
}

アドレス可能なアセットはAddressables.LoadAssetAsyncでロードします。ここにロードしたいアセットのアドレスを指定します。

LoadAssetAsyncの戻り値はAsyncOperationHandleになっています。Addressablesの操作はほとんど非同期になっており、AsyncOperationHandleで操作の状態を表します。.Taskを付けてawaitすれば簡単に操作の完了を待てます。

また、Addressablesでロードしたアセットは使い終わったら必ずリリースする必要があります。Addressables.Releaseを使います。これを忘れるとプレハブが読み込まれたままになってしまうので注意してください。リリース漏れは後で紹介するEvent Viewerで確認できます。

内部では参照カウンタ方式でそのアセットが何ヵ所から使われているかをカウントしています。リリースするたびにカウントが減り、0になったらアセットがアンロードされます。

2. Addressables.InstantiateAsyncを使う

次のスクリプトも先のSpawnerByLoadAssetAsyncと同じような動作をします。

using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;

public class SpawnerByInstantiateAsync : MonoBehaviour
{
    private GameObject spawnedGameObject;

    private async void Start()
    {
        // Addressables.InstantiateAsyncでプレハブをインスタンス化する
        AsyncOperationHandle<GameObject> handle = Addressables.InstantiateAsync("Assets/Prefabs/Cube.prefab");

        // .Taskでインスタンス化完了までawaitできる
        spawnedGameObject = await handle.Task;

        spawnedGameObject.name = "Spawned Game Object";
    }

    private void OnDestroy()
    {
        // 使い終わったらインスタンスをリリースする
        Addressables.ReleaseInstance(spawnedGameObject);
    }
}

Addressables.LoadAssetAsyncの代わりにAddressables.InstantiateAsyncを使っています。名前の通り、ロードする代わりに直接インスタンス化できます。

リリースでもAddressables.ReleaseではなくAddressables.ReleaseInstanceを使います。AsyncOperationHandleではなくインスタンス化されたGameObjectを渡してリリースできます。

Addressables.LoadAssetAsyncはGameObject以外のアセットもロードできますがAddressables.InstantiateAsyncはプレハブ専用です。プレハブをインスタンス化したい場合はAddressables.InstantiateAsyncの方が簡単に書けます。

3. AssetReferenceTを使う

先ほどの2つの方法はアドレスを指定してロードする方法でしたが、AssetReferenceTを使うとインスペクタでアセットを設定してロードできます。*1

次のコードをシーンに配置して、prefabReferenceにCubeプレハブを設定します。

using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;

public class SpawnerByAssetReferenceT : MonoBehaviour
{
    [SerializeField] private AssetReferenceT<GameObject> prefabReference;

    private GameObject spawnedGameObject;

    private async void Start()
    {
        // InstantiateAsyncでプレハブをインスタンス化する
        AsyncOperationHandle<GameObject> handle = prefabReference.InstantiateAsync();

        // .Taskでインスタンス化完了までawaitできる
        spawnedGameObject = await handle.Task;

        spawnedGameObject.name = "Spawned Game Object";
    }

    private void OnDestroy()
    {
        // 使い終わったらインスタンスをリリースする
        prefabReference.ReleaseInstance(spawnedGameObject);
    }
}

ここではInstantiateAsyncを使ってプレハブをインスタンス化していますが、LoadAsyncもあるのでアセットのロードにも使えます。

AssetReferenceTはインスペクタで参照を設定できるのが強みです。特に理由がなければAssetReferenceTを使うのをお勧めします。

f:id:sakanox:20211221174854p:plain

Addressables Groups

全てのアドレス可能なアセットはグループにわけて管理されます。

メニューのWindows/Asset Management/Addressables/Groupsでグループを管理できます。

f:id:sakanox:20211221180242p:plain

画像のPrefabsやMaterialsがグループで、その下のAssets/Prefabs/Cube.prefabなどがグループに入っているアセットです。

グループは右クリックで新しく作成できます。グループにアセットをドラッグ&ドロップしてアセットを振り分けられます。

同じグループのアセットは1つのアセットバンドルとしてビルドされます。*2つまり、グループ単位でロードやアンロードが行われるということです。同じタイミングで使われるアセットを同じグループにしておくと効率が良くなります。

Play Mode Script

Play Mode Scriptメニューでエディタ上で実行したときの動作を設定できます。

f:id:sakanox:20211221185214p:plain

「Use Asset Database」ではアセットバンドルを使わずにアセットを直接読み込みます。動作が速いので開発中にはこれを使います。

「Simulate Groups」ではアセットバンドルは使われませんが、グループの設定に従ってアセットバンドルの動きをシミュレートします。これによってEventViewerでアセットバンドルのロードやアンロードを監視できるようになります。
エディタ上でグループの設定を試すのに使えます。

「Use Existing Build」では実際にビルドされたアセットバンドルが使われます。あらかじめアセットバンドルをビルドしておく必要があります。

アセットバンドルのビルド

Build/New Build/Default Build Scriptでグループをアセットバンドルにビルドできます。ビルドはLibrary/com.unity.addressables/aaフォルダに出力されます。
f:id:sakanox:20211221190757p:plain

ビルドされたアセットバンドルは、プロジェクトのビルド時には自動的にStreamingAssetsにコピーされます。これによって実機でも正しくアセットバンドルをロードできます。

グループの設定

Addressables Groupsでグループを選択するとグループごとの設定ができます。

f:id:sakanox:20211221180740p:plain

ここではBuild & Load Pathsについて説明します。他の項目についてはマニュアルを見てください。

Build & Load Paths

一番上の「Build & Load Paths」でアセットバンドルをどこにビルドしてどこからロードするかを設定できます。

f:id:sakanox:20211221182117p:plain
「Local」ではLibraryフォルダ内にビルドされ、プロジェクトのビルド時には自動的にStreamingAssetsにコピーされます。
ロードもEditor実行時はLibraryフォルダ、ビルドではStreamingAssetsから自動的に行われます。

f:id:sakanox:20211221182131p:plain
「Remote」ではServerDataフォルダ内にビルドされます。ビルドしたアセットバンドルは手動でサーバなどにアップロードする必要があります。
ロード時にサーバからダウンロードされます。

f:id:sakanox:20211221182458p:plain
「custom」ではパスを自由に設定できます。

Remoteに設定したときの詳しい使い方はまた別の記事で説明します。

Addressables Analyze

アセットバンドルには、そのグループに振り分けたアセットだけでなく、それらのアセットが依存するアセットも含めてビルドされます。

依存先のアセットがどこかのグループに入っていればそこへの参照が張られます。どこのグループにも入っていない場合は同じアセットバンドルに直接含まれます。実際にどのアセットがどのアセットバンドルに含まれるかをAddressables Analyzeで確認できます。

Addressables AnalyzeはAddressable GroupsのTools/Window/Analyzeメニューで表示できます。
f:id:sakanox:20211221193405p:plain

「Analyze Select Rules」を押すとグループ設定を検証できます。

使ってみる

実際に以下のグループ設定で検証してみます。Cube.prefabとCube2.prefabはどちらもGreen.matというマテリアルに依存しています。
f:id:sakanox:20211221193604p:plain

この状態で「Analyze Select Rules」を押すと以下のような結果になります。
f:id:sakanox:20211221193806p:plain

アセットバンドルに含まれるアセット一覧はBundle Layout Previewに表示されています。

ここでは2つのアセットバンドルにGreen.matが暗黙的(Implicit)に含まれてしまっています。同じアセットが別々のアセットバンドルに含まれると、同じアセットが2重にロードされて効率が悪くなります。Analyze結果でも黄色い三角で警告として表示されています。

これを解決するにはGreen.matをどこかのグループに入れてしまえばOKです。「Fix Selected Rules」を押すと自動的に重複するアセットを新しいグループに隔離してくれます。実行するとGreen.matがDuplicate Asset Isolationというグループに入りました。
f:id:sakanox:20211221194402p:plain

Addressables Event Viewer

Event Viewerを使うとアセットバンドルのロードやアンロードを監視できます。アセットのリリース漏れなどがないかを確認できます。

Event Viewerを使うには設定を変更する必要があります。

  1. AddressableAssetsData/AddressableAssetSettingsを選び、Send Profiler Eventsにチェックを入れます。
  2. Addressables GroupsでPlay Mode ScriptをSimulate GroupsまたはUse Existing Buildにします。

EventViewerはAddressable GroupsのTools/Window/Event Viewerで表示できます。エディタ上で実行開始すると以下のように現在の状態を見られます。
f:id:sakanox:20211221195520p:plain
ここではCube.prefabが1つロードされていることがわかります。

まとめ

  • グループに割り振られたアセットがアドレス可能となり、Addressablesシステムでロードできます。
  • グループに割り振られたアセットがアセットバンドルとしてひとまとめにビルドされます。
  • Addressables Analyzeを使ってアセットの重複を検査・修正できます。
  • Addressables Event Veiwerを使ってアセットのリリース漏れを検査できます。

*1:Unity2019以前ではインスペクタでGenericsを使えないためAssetReferenceT<GameObject>ではなくAssetReferenceGameObjectになります

*2:グループ設定で別のビルド方法もできますが、ビルド時間が延びたり管理しづらくなったりするのでお勧めしません