🎶
【Unity】サウンドシステムにオブジェクトプール導入【SEManager】
背景
- 上記ライブラリ開発中、「SE機能にオブジェクトプール導入しよ~」との考えに至った
オブジェクトプールは何故必要か
-
AudioSource
管理に導入したい (Unity でサウンド再生に必要なクラス) - SE の場合、瞬時に大量の
AudioSource
が必要になる状況が頻発する - そこで、
AudioSource
生成を抑えて使いまわせる設計が有効だと考えた
SE 機能の設計
プログラム関係図(全体)
-
SEManager
- SE再生, 停止, 中断, 再開を担当する
-
IAudioSourcePool
- プール操作を抽象化
-
ISoundLoader
-
AudioClip
ロードを抽象化 - 現状は Addressable のみ対応だが、他のロード方法を指定可能にする予定
-
AudioSourcePool群 詳細
-
_Base
- プールの初期化, 再初期化を実装
-
_FIFO
-
_Strict
- プールの扱い方が異なる派生クラス達
-
AudioSourcePoolFactory (表には記載ナシ)
- 指定の振る舞いのオブジェクトプール(
_FIFO
,_Strict
)を返す
- 指定の振る舞いのオブジェクトプール(
オブジェクトプールの挙動
AudioSourcePool_Base
(基本機能)
AudioSourcePool_Base の全容
internal abstract class AudioSourcePool_Base : IAudioSourcePool
{
protected readonly GameObject sourceRoot;
protected readonly AudioMixerGroup mixerGroup;
protected Queue<AudioSource> pool;
protected readonly int maxSize;
protected readonly int initSize;
public IEnumerable<AudioSource> GetAllResources() => pool;
public AudioSourcePool_Base(AudioMixerGroup mixerG, int initSize, int maxSize)
{
pool = new();
sourceRoot = new("SE_AudioSources");
this.maxSize = maxSize;
this.initSize = initSize;
this.mixerGroup = mixerG;
//プール初期化
for (int i = 0; i < initSize; i++)
{
var source = CreateSourceWithOwnerGameObject();
pool.Enqueue(source);
}
}
public void Reinitialize()
{
Log.Safe("Reinitialize実行");
//プール内の要素を全て未使用にする
foreach (var source in pool)
{
source.Stop();
source.clip = null;
}
//プールサイズを初期化時の値に戻す
while (pool.Count > initSize) //超過時
{
var source = pool.Dequeue();
Object.Destroy(source.gameObject);
}
while (pool.Count < initSize) //不足時
{
pool.Enqueue(CreateSourceWithOwnerGameObject());
}
}
public abstract AudioSource Retrieve();
protected AudioSource CreateSourceWithOwnerGameObject()
{
var obj = new GameObject("SESource");
obj.transform.parent = sourceRoot.transform;
var source = obj.AddComponent<AudioSource>();
source.playOnAwake = false;
source.outputAudioMixerGroup = mixerGroup;
return source;
}
}
-
AudioSource
プールをQueue
で確保 - 派生クラスでは
Retrieve()
の挙動が異なる
_FIFO
の Retrieve()
AudioSourcePool_FIFO の全容
internal sealed class AudioSourcePool_FIFO : AudioSourcePool_Base
{
public AudioSourcePool_FIFO(AudioMixerGroup mixerG, int initSize,
int maxSize)
: base(mixerG, initSize, maxSize)
{
}
public override AudioSource Retrieve()
{
Log.Safe("Retrieve実行");
//未使用のAudioSourceがあれば、それを返す
for (int i = 0; i < pool.Count; i++)
{
var source = pool.Dequeue();
if (source.isPlaying == false)
{
pool.Enqueue(source);
return source;
}
pool.Enqueue(source);
}
//プールが最大サイズの場合、最古のものを強制的に中断して利用
if (pool.Count >= maxSize)
{
var oldest = pool.Dequeue();
oldest.Stop();
pool.Enqueue(oldest);
return oldest;
}
else //最大サイズ未満なら新規作成
{
var created = CreateSourceWithOwnerGameObject();
pool.Enqueue(created);
return created;
}
}
}
- プールを回し、非再生状態のものがあればそれを返す
- 全て再生中で、プールが最大サイズ未満の場合
-
AudioSource
を新規作成して返す
-
- 全て再生中で、プールが最大サイズの場合
- 最古
AudioSource
を返す (それで再生されていた SE は中断)
- 最古
_Strict
の Retrieve()
- 同時再生 SE 数を制限したい場合に有効。超過時は諦める設計
AudioSourcePool_Strict の全容
internal sealed class AudioSourcePool_Strict : AudioSourcePool_Base
{
public AudioSourcePool_Strict(AudioMixerGroup mixerG, int initSize,
int maxSize)
: base(mixerG, initSize, maxSize)
{
}
public override AudioSource Retrieve()
{
Log.Safe("Retrieve実行");
//未使用のAudioSourceがあれば、それを返す
for (int i = 0; i < pool.Count; i++)
{
var source = pool.Dequeue();
if (source.isPlaying == false)
{
pool.Enqueue(source);
return source;
}
pool.Enqueue(source);
}
//プールが最大サイズ未満なら新規作成したものを返す
if (pool.Count < maxSize)
{
var created = CreateSourceWithOwnerGameObject();
pool.Enqueue(created);
return created;
}
//最大サイズで全て使用中ならnull
return null;
}
}
- プールを回し、未使用のものがあればそれを返す
- 全て再生中で、プールが最大サイズ未満の場合
-
AudioSource
を新規作成して返す
-
- 全て再生中で、プールが最大サイズの場合
-
null
を返す
-
SEManager
がどのようにオブジェクトプールを利用するか
public async UniTask Play(string resourceAddress,
float volume, float pitch, float spatialBlend, Vector3 position)
{
//AudioClip ロード
var (success, clip) = await loader.TryLoadClip(resourceAddress);
if (!success) return;
//プールから AudioSource 取得
var source = sourcePool.Retrieve();
if (source == null)
{
Log.Warn("AudioSource 取得に失敗したため再生中断");
return;
}
//SE再生設定, 再生実行
source.pitch = pitch;
source.volume = volume;
source.spatialBlend = spatialBlend;
source.transform.position = position;
source.PlayOneShot(clip);
}
工夫点
Queue
で表現した理由
プールを - 「先頭を借りて末尾に戻す」だけで最古判定が実装できる
- また、
SEManager
でAudioSource
を返却する処理が必要ない
- また、
UnityEngine.Pool.ObjectPool<T> を導入していない理由
- デフォルトで無制限プール → メモリ爆増リスク
- 最大数制限や再利用ポリシーを細かく制御したかった
- 自作プールにしておくと後からログなどを挟みやすい
Discussion