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

f:id:halya_11:20200724183257p:plain
アドレスでロード

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

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

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

f:id:halya_11:20200724184500p:plain
組み込み/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からウィンドウを開きます。

f:id:halya_11:20200724191914p:plain
ウィンドウ

アドレスを登録する

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

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

f:id:halya_11:20200724192213p:plain
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が生成されることを確認できます。

f:id:halya_11:20200724192855p:plain
生成された

実機ビルドする

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

f:id:halya_11:20200724193908p:plain
Addressableビルド

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

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

f:id:halya_11:20200724194023p:plain
実機ビルド

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

Addressableのビルドを自動化する

さてビルド前にいちいち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);
    }
}

上記はビルドボタン押下をフックしているので、スクリプトからビルドする際には別途処理を書く必要があります。
こんなこと自動で勝手にやってくれという感じですが、パフォーマンスの観点から敢えてこうしているようです。
以下のフォーラムで議論が行われていて、現状は今後何かしら変更入るかも?入らないかも?な感じです。

https://forum.unity.com/threads/how-to-trigger-build-player-content-when-build-unity-project.689602/

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