【Unity】Addressable用ライフタイム管理ライブラリAddlerをリリースしました

Addressableのライフタイム管理ライブラリAddlerをリリースしました。

github.com

注意: 1.0.0をリリースしました。この記事は古いかも

本記事はAddlerの0.1.0をリリースした時に書いたものですが、2023/1/24に1.0.0を正式リリースしました。

日本語ドキュメントも書いたので、本記事ではなくこちらのドキュメント↓を参照してもらうのがいいと思います。

github.com

Addlerとは?

Addressableアセットシステムでは、ロードしたリソースが不要になったら明示的にリリースする必要があります。

// ロード
var handle = Addressables.LoadAssetAsync<GameObject>("FooPrefab");
await handle.Task;

// リリース
Addressables.Release(handle);

これを忘れるとメモリリークの原因となり、いずれ深刻な問題を引き起こす負債を生みます。

AddlerではリソースのライフタイムをGameObjectなどに紐づけることによりメモリ管理をシンプルにします。

var fooObj = new GameObject();

// fooObjにリソースのライフタイムを結びつける
// fooObjがDestroyされると同時にリソースもリリースされる
Addressables.LoadAssetAsync<GameObject>("BarPrefab").BindTo(fooObj);

上記のコードでは、fooObjが破棄されると同時にリソースがリリースされます。
こうしておけばリリースを忘れる心配はありません。

ライフタイムをバインディング

またリソースを事前にロードしておき同期的にそれを取得するプリロード機能や、
Prefabのインスタンスをプーリングして使いまわす機能も実装されています。

オブジェクトプーリング

さらにこれらのライフタイムもGameObjectなどにバインドすることができます。

AddlerはこのようにしてAddressableにおけるリソースのライフタイムを適切に管理するためのライブラリです。

インストール

  1. Window > Package ManagerからPackage Managerを開く
  2. 「+」ボタン > Add package from git URL
  3. 以下を入力

Package Manager

あるいはPackages/manifest.jsonを開き、dependenciesブロックに以下を追記します。

{
    "dependencies": {
        "com.harumak.addler": "https://github.com/Haruma-K/Addler.git?path=/Packages/com.harumak.addler"
    }
}

バージョンを指定したい場合には以下のように記述します。

ライフタイムをバインドする

Addressableで読み込んだリソースのライフタイムをバインドするにはAddressables.LoadAsssetAsync()の後ろにBindTo()と記述します。

// リソースをロードしてハンドルのライフタイムをgameObjectにバインドする
var handle = Addressables.LoadAssetAsync<GameObject>("FooPrefab").BindTo(gameObject);
await handle.Task;
var prefab = handle.Result;

// gameObjectを破棄してハンドルをリリースする
Destroy(gameObject);

これで、gameObjectが破棄されると同時にリソースがリリースされます。
なおライフタイムはGameObject以外にもバイディング可能です(後述)。

プリロード

Addressablesはリソースを非同期にロードするAPIのみを提供しています。

// 非同期ロード
var handle = Addressables.LoadAssetAsync<GameObject>("FooPrefab");
await handle.Task;

しかし実際には、「ロード画面」で事前にリソースをロードして、ゲーム中は同期的にリソースをロードしたいケースがほとんどでしょう。
プリローダはこの処理を行うための機能です。

プリローダの使い方

プリローダはAddressablesPreloaderクラスをインスタンス化して使用します。

// プリローダを作成
var preloader = new AddressablesPreloader();

// プリロード
await preloader.PreloadAsync("FooPrefab", "BarPrefab");

// プリロードしたリソースを同期的に取得
var fooPrefab = preloader.Get<GameObject>("FooPrefab");
var barPrefab = preloader.Get<GameObject>("BarPrefab");

// すべてのリソースをリリース
preloader.Dispose();

AddressablesPreloader.PreloadAsync()を呼ぶと引数に渡したアドレスが指すリソースを全てロードします。
AddressablesPreloader.Get()メソッドを使うとプリロードしたリソースを同期的に取得できます。

プリローダを使用し終わったらAddressablesPool.Dispose()を呼ぶことですべてのリソースがリリースされます。

プリローダのライフタイムをバインドする

プリローダのライフタイムをバインドすることもできます。

// プリローダのライフタイムをバインドする
var preloader = new AddressablesPreloader().BindTo(gameObject);

プリローダのライフタイムが終了するとすべてのリソースがリリースされます。

オブジェクトプール

UnityのゲームではPrefabをインスタンス化したGameObjectが多数使われます。
しかしPrefabのインスタンス生成や破棄にはコストがかかり、頻繁に行いすぎるとパフォーマンスの低下を招きます。

例えば弾丸のように同じPrefabのインスタンスを多数生成するようなケースでは、
一定数のインスタンスをあらかじめ生成しておいてそれらを使いまわすことによりパフォーマンスの低下を防ぎます。
これをオブジェクトプーリングと呼びます。

オブジェクトプーリング

AddlerにはAddressableアセットシステムでオブジェクトプーリングを扱うための機能が実装されています。

オブジェクトプールの使い方

オブジェクトプールはAddressablesPoolクラスをインスタンス化して使用します。

// FooPrefabのプールを作る
var pool = new AddressablesPool("FooPrefab");

// インスタンスを生成する
await pool.WarmupAsync(5);

// プールからインスタンスを取得する
var operation= pool.Use();
var instance = operation.Object;

// プールにインスタンスを返却する
operation.Dispose();

// プールを破棄してすべてのインスタンスをリリースする
pool.Dispose();

AddressablesPool.WarmupAsync()を呼ぶと引数に渡した数だけPrefabのインスタンスが生成されます。
プールからインスタンスを取得するにはAddressablesPool.Use()メソッドでPooledObjectOperationを取得します。これのObjectプロパティからインスタンスを取得できます。
PooledObjectOperation.Dispose()メソッドを呼ぶとインスタンスがプールに戻ります。

プールを使用し終わったらAddressablesPool.Dispose()でプールを破棄してください。
全てのインスタンスが破棄され、またリソースがリリースされます。

オブジェクトプールのライフタイムをバインドする

オブジェクトプールや、プールから取得したオブジェクトのライフタイムをバインドすることもできます。

// プールのライフタイムをバインドする
var pool = new AddressablesPool("FooPrefab").BindTo(gameObject1);

await pool.WarmupAsync(5);

// インスタンスのライフタイムをバインドする
// gameObject2が破棄されたらインスタンスはプールに返却される
var instance = pool.Use().BindTo(gameObject2).Object;

プールから取得したオブジェクトのライフタイムが終了するとプールに返却され、
オブジェクトプールのライフタイムが終了するとすべてのインスタンスが破棄・リリースされます。

GameObject以外にバインドする

ライフタイムはGameObject以外にバインドすることもできます。
GameObject以外にバインドするためにはIEventDispatcherを実装したクラスを用意しBindTo()にそれを渡します。

AddlerにはParticleSystemの終了タイミングにライフタイムをバインドするためのクラスを用意しています。
IEventDispatcherの実装例として以下にこのクラスを示します。

using System;
using UnityEngine;

namespace Addler.Runtime.Foundation.EventDispatcher
{
    [RequireComponent(typeof(ParticleSystem))]
    public class ParticleSystemFinishedEventDispatcher : MonoBehaviour, IEventDispatcher // Implement IEventDispatcher
    {
        private bool _isAliveAtLastFrame;
        private ParticleSystem _particleSystem;

        private void Awake()
        {
            _particleSystem = GetComponent<ParticleSystem>();
        }

        private void LateUpdate()
        {
            var isAlive = _particleSystem.IsAlive(true);
            if (_isAliveAtLastFrame && !isAlive)
            {
                // Call OnDispatch when the ParticleSystem is finished.
                OnDispatch?.Invoke();
            }

            _isAliveAtLastFrame = isAlive;
        }

        public event Action OnDispatch;
    }
}

ライセンス

本ソフトウェアはMITライセンスで公開しています。
ライセンスの範囲内で自由に使っていただいてかまいませんが、
使用の際は以下の著作権表示とライセンス表示が必須となります。

https://github.com/Haruma-K/Addler/blob/master/LICENSE.md

リポジトリ

github.com