【Unity】僕はそろそろResourcesフォルダを卒業しようと思う

Addressableがいい感じになってきたしそろそろResourcesフォルダを使った開発を卒業しようという話です。

Unity2019.3.5
Addressables 1.7.5

Resourcesフォルダは非推奨

Resourcesフォルダはお手軽にアセットを読み込む際には便利な方法です。
しかし実はResourcesフォルダによるアセットの読み込みはUnityが公式としてもはや推奨していない方法になります。
実際に下の記事には、Resourcesフォルダのベストプラクティスの項に「Don't use it.」と書かれています。

learn.unity.com

なぜ非推奨なのか?

使うべきでない理由として、上の記事では以下の3点が挙げられています。

  1. 細かなメモリ管理が難しくなる
  2. Resourcesに入っているアセットが多いとアプリの立ち上げ時間やビルド時間が長くなる
  3. プラットフォームに応じたリソースの配信ができない

1.に関して補足すると、ResourcesにはResources.UnloadAssets()というAPIが用意されていますが、
これは単純にそのアセットのアンロードを行うだけであり、参照カウンタによる厳密なメモリ管理や依存関係を含めたアンロードを行う機能は備えていません。
またResources.UnloadUnusedAssets()というAPIもありますが、こちらは使っていないリソースを一気にアンロードするというかなりざっくりした機能です。

Resourcesを使ってもいいケース

ただし以下のようなケースに限って言えば便利な機能であるとされています。

  1. プロトタイプ作成時(※これは今は事情が変わっていそう(下記参照))
  2. アプリが立ち上がっている間常駐するリソース
  3. メモリをそんなに食わないものに使用する場合
  4. プラットフォーム間でリソースを切り替える必要がない場合
  5. アプリ立ち上げ処理用に使う場合

個人的にはこれに加えてエディタ用のツールを作る際には使うべきケースも多いと思います。

ただしこのケースが挙げられた記事はAddressableアセットシステムの登場以前に書かれたものであり、
Addressableが登場した今となっては「1. プロトタイプ作成時」にもResourcesは使わないほうが良いかと思います。
このあたりについては次節以降で詳しく説明します。

Addressableアセットシステムを使う

じゃあResourcesを使わずにどうするのかというと、Addressableアセットシステムを使います。
AddressableアセットシステムはUnity2019.2か3あたりで本リリースされた、リソース管理の仕組みです。

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

Addressableアセットシステムの大きな特徴として「アドレス」があります。
アドレスとはアセット毎に付けることができる任意の名前であり、それを指定することでリソースを読み込みます。

アドレスでロード

ソースコードとしては以下のような感じになります。

// アドレスを使ってロードする
Addressables.LoadAssetAsync<GameObject>("ExamplePrefab");
設定一つで組み込み/DLCを切り替え

またAddressableは開発のワークフローを強く意識した作りになっており、
設定項目を切り替えるだけでそのアセットをアプリに組み込むアセットとするかダウンロードコンテンツとするかを変更できます。

組み込み/DLC

そしてどちらの場合でも読み込みの仕方は変わりません。設定を切り替えるだけです。

Addressables.LoadAssetAsync<GameObject>("ExamplePrefab");
プロトタイプからAssetBundleに移行するためのコストがゼロに

さて、アプリのプロトタイプや本開発の初期フェーズではとりあえずResourcesフォルダを使うことも多いと思います。
この場合には、Resourcesフォルダからの相対パスを指定してアセットを読み込むことになります。
例えばAssets/Resources/Prefabs/ExamplePrefab.prefabを読み込むには以下のように記述します。

Resources.LoadAsync("Prefabs/ExamplePrefab.prefab");

しかしプロジェクトが進むにつれて、このように読んでいたリソースをResourcesフォルダ外に移動してAssetBundleとして切り出す必要が出てきます。
AssetBundleの読み込みはAssetBundle名とアセット名を使って行います。

var assetBundle = AssetBundle.LoadFromFile("SomeFolder/some_bundle.bundle");
assetBundle.LoadAssetAsync<GameObject>("Assets/Prefabs/ExamplePrefab.prefab");    

すると当然、Resourcesを使って書いていた読み込み処理を大きく修正する必要が出てきます。

これに対してAddressableでは上記の通り、設定を切り替えるだけで組み込みかダウンロードコンテンツかを切り替えられるため、このようにパスを書き換えるようなコストは発生しません。

厳密なメモリ管理

またAddressableは内部で参照カウンタを持っており、厳密なメモリ管理が可能になっています。
ロードを行うたびに参照カウンタがインクリメントされます。

// 参照カウンタがインクリメント
var handle = Addressables.LoadAssetAsync<GameObject>("ExamplePrefab");

アンロードを行うと参照カウンタがデクリメントされ、ゼロになったらそのリソースはアンロードされます。

// 参照カウンタがデクリメント
// ゼロになったらアンロード
Addressables.Release(handle);

実際にAddressableをResourcesの代わりとして使ってみる

それでは実際にAddressableアセットシステムをResourcesフォルダの代わりに使ってみます。
Addressableの細かい使い方は別の記事(最下部にまとめています)で説明するとして、ここではざっくり概要がわかるように説明します。

まずPackage ManagerからAddressablesをインストールし、Window > Asset Management > Addressables > Groupsからウィンドウを開きます。

ウィンドウ

アドレスを登録する

初期設定ではDefault Local Groupというのが組み込みリソース用のグループになります。
なおBuilt in Dataはビルド設定に含まれるシーンとResourcesフォルダに入ったものです。
Addressableの仕組みに乗らない(アドレスを振れない)のでグレーアウトされています。

というわけでこのDefault Local Groupに適当なPrefabをドラッグ&ドロップします。
アドレスには適当な文字列を入力しておきます。
今回は以下のようにAssets/Prefabs/Example.prefabExampleというアドレスで登録しました。

Prefabを登録

読み込みスクリプトを書く

次にこれを読み込むスクリプトを書きます。

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

public class Example : MonoBehaviour
{
    private AsyncOperationHandle<GameObject> _handle;
    
    private async void Start()
    {
        // ロード&インスタンス化
        _handle = Addressables.InstantiateAsync("Example", Vector3.zero, Quaternion.identity);
        await _handle.Task;
    }

    private void OnDestroy()
    {
        // 解放
        Addressables.ReleaseInstance(_handle);
    }
}

エディタで再生してみると正常にGameObjectが生成されることを確認できます。

生成された

実機ビルドする

次に実機ビルドを行います。
Addressableを実機で使うためにはAddressableのビルドを行う必要があります。
ビルドを行うためにはAddressableのウィンドウからBuild > New Build > Default Build Scriptを選択します。

Addressableビルド

これによりAddressableの内部で使われているAssetBundleのビルドと、アドレスや依存関係の情報を構築されます。
組み込み用のAssetBundleはAddressableアセットシステムがアプリのビルド時にアプリ(StreamingAssets)に含めてくれます。

そのためあとは普通にアプリをビルドするだけです。

実機ビルド

実機ビルドでも正常にリソースが読み込めることを確認できました。

Addressableのビルドを自動化する

※ 2021年12月15日追記※
こちら情報が更新され、Unity2021.2以上、Addressables 1.19.4であれば、
Addressable Asset SettingsBuild Addressables content on Player Buildにチェックを入れれば
プレイヤービルド時にAddressblesもビルドされるようになりました。

Addressable Asset Settings

この節のうちの以下の部分についてはそれ以前に書いた内容です

さてビルド前にいちいちAddressableをビルドするのは面倒だし忘れるので自動化します。
ビルドウィンドウからBuildボタンが押されたときにAddressableのビルドも走らせるようにします。

using UnityEditor;
using UnityEditor.AddressableAssets;
using UnityEditor.AddressableAssets.Settings;

public class AddressableBuilder
{
    [InitializeOnLoadMethod]
    private static void Initialize()
    {
        // ビルドボタンの押下をフック
        BuildPlayerWindow.RegisterBuildPlayerHandler(BuildPlayerHandler);
    }
 
    private static void BuildPlayerHandler(BuildPlayerOptions options)
    {
        // 必要に応じてClean
        AddressableAssetSettings.CleanPlayerContent(AddressableAssetSettingsDefaultObject.Settings.ActivePlayerDataBuilder);
        
        // Addressableをビルド
        AddressableAssetSettings.BuildPlayerContent();
        
        // Playerをビルド
        BuildPlayerWindow.DefaultBuildMethods.BuildPlayer(options);
    }
}

上記はビルドボタン押下をフックしているので、スクリプトからビルドする際には別途処理を書く必要があります。
本当はIPreprocessBuildWithReportで出来たらいいのですが、これでAddressableのビルドを行うと例外がスローされるようです。

またこの辺りはAddressable側で勝手にやってくれという感じですが、パフォーマンスの観点から敢えてこうしているようです。
このあたりは以下のフォーラムで議論が行われていて、現状は今後何かしら変更入るかも?入らないかも?な感じです。

How to trigger "build player content" when build unity project. - Unity Forum

同期ロードもサポートされた(2021.06.08追記)

Addressablesでは当初、同期ロードがサポートされていませんでした。
そのため、すでにResources.Load()を使って同期的に読み込んでいる部分をAddressablesに置き換えるにはまず非同期化する必要がありました。

しかしバージョン1.17.4で、公式に同期ロードがサポートされました。
いくつか使用上の注意点(以下の記事を参照)はあるものの、これでResources.Load()を簡単に置き換えられるようになりました。

light11.hatenadiary.com

便利ツールの紹介

最後にAddressableアセットシステムに関連する便利なツールを紹介して終わります。

アドレス設定を自動化するEZAddresser

Addressableにおけるアドレスの設定はいちいちドラッグ&ドロップしなければいけない面倒さがあります。
この面倒さを解消するためのツールがEZAddresserです。

light11.hatenadiary.com

EZAddresserを使うと以下の2ステップで簡単にアドレスを設定し、読み込むことができるようになります。

  1. ロードしたいアセットを「Addressables」フォルダに入れる
  2. Addressables.LoadAssetAsync("[ここにアセット名]") を呼ぶ

自動的にアドレスが設定される様子

Unity Addressable Importer

もう少し厳密にアドレスを管理したい場合にはUnity Addressable Importerがおススメです。

light11.hatenadiary.com

これを使用すると対象のアセットを正規表現で指定して、アドレスやグループなどを設定することができます。

Unity Addressable Importer

Addressableアセットシステム関連記事

本記事ではResourcesフォルダとの対比という観点でAddressableアセットシステムの触りの部分を紹介しました。
本ブログではAddressableに関する記事をいくつか書いているため、必要に応じて以下も参照してください。

大体下のほうが応用的な内容です。

light11.hatenadiary.com

light11.hatenadiary.com

light11.hatenadiary.com

light11.hatenadiary.com

light11.hatenadiary.com

light11.hatenadiary.com

light11.hatenadiary.com

light11.hatenadiary.com

light11.hatenadiary.com

light11.hatenadiary.com

light11.hatenadiary.com

light11.hatenadiary.com

light11.hatenadiary.com

light11.hatenadiary.com

light11.hatenadiary.com

light11.hatenadiary.com

light11.hatenadiary.com

light11.hatenadiary.com

light11.hatenadiary.com

light11.hatenadiary.com

light11.hatenadiary.com