English Documents Available(英語ドキュメント)
Unity の Addressable アセットシステムのためのメモリ管理システムです。
ロードしたリソースのライフタイム管理、オブジェクトプーリング、プリローディング機能を提供します。
詳細
Addressable アセットシステムでは、ロードしたリソースが不要になったら明示的に解放をする必要があります。
// ロードする
var handle = Addressables.LoadAssetAsync<GameObject>("FooPrefab");
await handle.Task;
// ロードしたアセットを使用する
var prefab = handle.Result;
// 不要になったらリリースする
Addressables.Release(handle);
これを忘れるとメモリリークの原因となり、アプリケーションのクラッシュなど深刻な問題に繋がります。
しかしながら、上記のような実装では、解放を忘れやすく、また忘れた時に気づきづらいという問題があります。
Addler ではこの問題に対応するため、以下のようにリソースのライフタイムを GameObject などに紐づけます。 紐づけた GameObject が破棄されると、リソースが自動的に解放されます。
var fooObj = new GameObject();
// fooObjにリソースのライフタイムを結びつける
// fooObjがDestroyされると同時にリソースもリリースされる
Addressables.LoadAssetAsync<GameObject>("BarPrefab").BindTo(fooObj);
上記のコードでは、fooObj
が破棄されると同時にリソースが解放されます。
こうしておけば解放を忘れる心配はありません。
またリソースを事前にロードしておき同期的にそれを取得するプリロード機能や、Prefabのインスタンスをプーリングして使いまわすオブジェクトプーリング機能も実装しています。
さらにこれらのライフタイムも GameObject などにバインドし、解放漏れを防ぐことができます。
Addler はこのようにして Addressable におけるリソースのメモリを適切に管理するためのライブラリです。
- Unity 2020.3 以上
- Addressables がインストールされていること
- Window > Package ManagerからPackage Managerを開く
- 「+」ボタン > Add package from git URL
- 以下を入力してインストール
あるいはPackages/manifest.jsonを開き、dependenciesブロックに以下を追記します。
{
"dependencies": {
"com.harumak.addler": "https://github.com/Haruma-K/Addler.git?path=/Assets/Addler"
}
}
バージョンを指定したい場合には以下のように末尾にバージョンを指定します。
Addler の基本的な機能として、リソースのライフタイムを GameObject などと紐づけて、確実かつ自動的に解放処理を行うライフタイムバインディングがあります。
Addressable で読み込んだリソースのライフタイムを GameObject と紐づけるには、以下のようにAddressables.LoadAsssetAsync()
の後ろにBindTo()
と記述します。
// リソースをロードしてハンドルのライフタイムをgameObjectにバインドする
var handle = Addressables
.LoadAssetAsync<GameObject>("FooPrefab")
.BindTo(gameObject);
await handle.Task;
var prefab = handle.Result;
// gameObjectを破棄してハンドルをリリースする
Destroy(gameObject);
これで、gameObjectが破棄されると同時にリソースが解放されます。
ライフタイムは GameObject 以外にバインドすることもできます。GameObject 以外にバインドするためには IReleaseEvent
を実装したクラスを作成し、BindTo()
にそれを渡します。
Addler には ParticleSystem の終了タイミングにライフタイムをバインドするためのクラスを用意しています。IReleaseEvent
の実装例として以下にこのクラスの実装を示します。
using System;
using Addler.Runtime.Core.LifetimeBinding;
using UnityEngine;
[RequireComponent(typeof(ParticleSystem))]
public sealed class ParticleSystemBasedReleaseEvent : MonoBehaviour, IReleaseEvent // Implement IReleaseEvent
{
[SerializeField] private ParticleSystem particle;
private bool _isAliveAtLastFrame;
private void Awake()
{
if (particle == null)
particle = GetComponent<ParticleSystem>();
}
private void Reset()
{
particle = GetComponent<ParticleSystem>();
}
private void LateUpdate()
{
var isAlive = particle.IsAlive(true);
if (_isAliveAtLastFrame && !isAlive)
ReleasedInternal?.Invoke();
_isAliveAtLastFrame = isAlive;
}
event Action IReleaseEvent.Dispatched
{
add => ReleasedInternal += value;
remove => ReleasedInternal -= value;
}
private event Action ReleasedInternal;
}
Addressables は基本的にリソースを非同期的にロードします。
// Asynchronous loading
var handle = Addressables.LoadAssetAsync<GameObject>("fooPrefab");
await handle.Task;
しかし実際には、いわゆるロード画面で事前にリソースをロードして、ゲーム中は同期的にリソースをロードしたいというケースがあります。
プリローダはこのような処理を実現するための機能です。
プリロードは AddressablePreloader
クラスにより行います。
使い方は以下のコードの通りです。
using System;
using System.Collections;
using Addler.Runtime.Core.Preloading;
using UnityEngine;
public sealed class Example : MonoBehaviour
{
private IEnumerator PreloadExample()
{
var preloader = new AddressablePreloader();
// Preload
{
var progress = new Progress<float>(x => Debug.Log($"Progress: {x}"));
// Preload by address.
yield return preloader.PreloadKey<GameObject>("fooAddress", progress);
// You can also preload by label.
yield return preloader.PreloadKey<GameObject>("fooLabel", progress);
// You can also preload multiple keys at once.
yield return preloader.PreloadKeys<GameObject>(new[] { "barAddress", "bazAddress" }, progress);
}
// Get the preloaded object.
{
// Get by address.
preloader.GetAsset<GameObject>("fooAddress");
// Get multiple assets by label.
preloader.GetAssets<GameObject>("fooLabel");
}
// Dispose the preloader and release all the assets.
preloader.Dispose();
}
}
AddressablePreloader.PreloadKey()/PreloadKeys()
を呼ぶと引数に渡したキーが指すリソースを全てロードします。
AddressablesPreloader.GetAsset()
メソッドを使うとプリロードしたリソースを同期的に取得できます。
プリローダを使用し終わったらAddressablePool.Dispose()
を呼ぶことですべてのリソースがリリースされます。
プリローダのライフタイムをバインドすることもできます。
// Bind the lifetime of the preloader to the GameObject.
// When gameObject is destroyed, the preloader will be disposed and release all the assets.
var preloader = new AddressablePreloader().BindTo(gameObject);
プリローダのライフタイムが終了するとすべてのリソースがリリースされます。
プリローディングの制約として、「プリロード時に指定したキーの種類」と「プリロードされたリソースを取得する際に指定したキーの種類」が一致している必要があります。
例えば、アドレスAのアセットを含むラベルAをプリロード時に指定した場合、取得時にもラベルAを指定する必要があります。
アドレスAを指定して取得することはできません。
これは、Addressable アセットシステムの仕様上、アドレスやラベル、AssetReference が指すリソースのキー (PrimaryKey) を同期的に取得する手段がないためです。
もしこのようなケースに対応したい場合は Addressables 1.17.1 からサポートされた Synchronous Workflow を使用することができます。 ただしこれには実行中の全ての AsyncOperation が終わるまで同期的に待つという仕様上の制約があるため、使用する際には注意が必要です。
Unity のゲームでは Prefab をインスタンス化した GameObject が多数使われます。
しかし Prefab のインスタンス生成や破棄にはコストがかかり、頻繁に行いすぎるとパフォーマンスの低下を招きます。
例えば弾丸のように同じ Prefab のインスタンスを多数生成するようなケースでは、一定数のインスタンスをあらかじめ生成しておいてそれらを使いまわすことによりパフォーマンスの低下を防ぐことができます。
これをオブジェクトプーリングと呼びます。
Addler には Addressable アセットシステムでオブジェクトプーリングを扱うための機能が実装されています。
オブジェクトプールはAddressablePool
クラスにより行います。
使い方は以下のコードの通りです。
using System;
using System.Collections;
using Addler.Runtime.Core.Pooling;
using UnityEngine;
public sealed class Example : MonoBehaviour
{
private IEnumerator PoolExample()
{
// Create a new pool with key of the GameObject.
var pool = new AddressablePool("fooPrefab");
// Create instances in the pool.
var progress = new Progress<float>(x => Debug.Log($"Progress: {x}"));
yield return pool.Warmup(5, progress);
// Get an instance from the pool.
var pooledObject = pool.Use();
var instance = pooledObject.Instance;
// Return the instance to the pool.
pool.Return(pooledObject);
//pooledObject.Dispose(); // You can also return the instance by disposing the pooled object.
// Destroy the pool and release all instances.
pool.Dispose();
}
}
AddressablePool.Warmup()
を呼ぶと引数に渡した数だけ Prefab のインスタンスが生成されます。
プールからインスタンスを取得するにはAddressablePool.Use()
メソッドでPooledObject
を取得します。
これのInstance
プロパティからインスタンスを取得できます。
AddressablePool.Return
あるいは PooledObject.Dispose()
メソッドを呼ぶとインスタンスがプールに戻ります。
プールを使用し終わったらAddressablePool.Dispose()
でプールを破棄してください。
全てのインスタンスが破棄。解放されます。
オブジェクトプールや、プールから取得したオブジェクトのライフタイムをバインドすることもできます。
using System.Collections;
using Addler.Runtime.Core.Pooling;
using UnityEngine;
public sealed class Example : MonoBehaviour
{
private IEnumerator PoolExample()
{
// Bind the lifetime of the pool to GameObject.
// If gameObject1 is destroyed, the pool will be disposed.
var pool = new AddressablePool("FooPrefab")
.BindTo(gameObject1);
yield return pool.Warmup(5);
// Bind the lifetime of the instance to GameObject.
// If gameObject2 is destroyed, the instance will be returned to the pool.
var instance = pool
.Use()
.BindTo(gameObject2)
.Instance;
}
}
プールから取得したオブジェクトのライフタイムが終了するとプールに返却され、オブジェクトプールのライフタイムが終了するとすべてのインスタンスが破棄・リリースされます。
Addler の機能のうち、プリローディングやオブジェクトプーリングを使用しない場合には、それらを無効化し、コンパイル対象から外すことができます。 無効化は Player Settings から以下の Scripting Define Symbols を設定することで行います。
- ADDLER_DISABLE_PRELOADING : プリローディングを無効化する
- ADDLER_DISABLE_POOLING : オブジェクトプーリングを無効化する
プリローディングやオブジェクトプーリングでは、コルーチンを使って非同期処理を待機します。 コルーチンの代わりにUniTaskを使いたい場合には以下の設定を行います。
- UniTask をインストールする(複数のインストール方法があります)
- (Package Manager を経由しない方法で1.をインストールした場合のみ)Scripting Define Symbols に
ADDLER_UNITASK_SUPPORT
を追加して Unity を再起動する AddressablePool.WarmupAsync
などコルーチンを使っていたメソッドの UniTask 版が Async という接尾辞と共に使用可能になります
本ソフトウェアはMITライセンスで公開しています。
ライセンスの範囲内で自由に使っていただけますが、使用の際は以下の著作権表示とライセンス表示が必須となります。
また、本ドキュメントの目次は以下のソフトウェアを使用して作成されています。
toc-generatorのライセンスの詳細は Third Party Notices.md を参照してください。