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

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

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が破棄されると同時にリソースがリリースされます。
こうしておけばリリースを忘れる心配はありません。

f:id:halya_11:20210208174032p:plain:w500
ライフタイムをバインディング

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

f:id:halya_11:20210208173955p:plain:w500
オブジェクトプーリング

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

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

インストール

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

f:id:halya_11:20210208173955p:plain:w500
オブジェクトプーリング

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;
    }
}

リポジトリ

github.com