配列やリストを使わない MonoBehaviour プール

👇 コチラの Unity `MonoBehaviour` 対応版。

https://qiita.com/sator_imaging/items/2a387a54a01e91e5d71d


- [つかいかた](#つかいかた)
    - [メニューコマンド](#メニューコマンド)
- [TODO／各種仕様](#todo各種仕様)
    - [プレハブをプーリングしたい](#プレハブをプーリングしたい)
    - [プレハブに `PoolableBehaviour` が含まれる場合](#プレハブに-poolablebehaviour-が含まれる場合)
    - [プール返却時の階層構造](#プール返却時の階層構造)
    - [貸出時の挙動をカスタマイズしたい](#貸出時の挙動をカスタマイズしたい)
    - [返却時／削除時の挙動をカスタマイズしたい](#返却時削除時の挙動をカスタマイズしたい)
    - [`Rent` を使わずに Poolable を GameObject にアタッチした場合](#rent-を使わずに-poolable-を-gameobject-にアタッチした場合)
    - [複数の `PoolableBehaviour` をひとつの GameObject にアタッチした場合](#複数の-poolablebehaviour-をひとつの-gameobject-にアタッチした場合)
- [ソースコード](#ソースコード)


# つかいかた

Unity の事情に合わせたので少し特殊。メインスレッドからしか呼べません。

※ 必要な `GameObject` は `Rent` すると勝手に作られます。

```cs
public class MyBehaviour : PoolableBehaviour<MyBehaviour>  // 👈
{
    // オーバーライドの必要ナシ。継承すれば動く
}

// 貸出時に GameObject を有効化するか選ぶ。false なら Awake, OnEnable, Start は実行されない
var component = MyBehaviour.Rent(activateGameObject: false, parent: null);
component.InitializeBeforeActivate();
component.gameObject.SetActive(true);

// 任意のタイミングで返却。または CancellationToken への紐づけ
component.ReturnToPool();
component.ReturnToPoolIfCanceled(otherComponent.destroyCancellationToken);

// いわゆる Warmup メソッド。インスタンス化だけでなく Awake 等も事前に実行しておきたいならパラメーターで指定する
MyBehaviour.Populate(count: 10, true);

// プールに残す未使用インスタンス数を指定して溢れたものは削除する（OnDestroy が実行される）
// ※ 貸し出し中のインスタンスは数に含まれない
MyBehaviour.TrimExcess(3);
```


## メニューコマンド

`Unity エディター > DEBUG > Poolable Behaviour` から各種テストが可能。





# TODO／各種仕様

## プレハブをプーリングしたい

`PoolableBehaviour` を継承し「`Awake` メソッドでプレハブをインスタンス化して抱える」コンポーネントとして実装すれば実現できると思います。

※ プールに残っている未使用インスタンスを探して存在しなかったらプレハブから持ってくる、という実装は必要ありません。どのようなリソースもプーリング可能な MonoBehaviour に管理を委ねれば間接的にプールの管理下に入ることになります。


## プレハブに `PoolableBehaviour` が含まれる場合

貸し出された Poolable インスタンスは完全にプールの管理下から外れます。これは `AddComponent` メソッドで追加したり、Unity エディターのインスペクターでマウスを使ってアタッチしたときと同じ状態です。

プレハブに含まれる Poolable は「貸し出されてプール管理下から外れた」状態のインスタンスです。なので、何もしなければ普通のコンポーネントと同じ振る舞いをします。

> Poolable のインスタンス再利用に必要なのは「`ReturnToPool` を実行すること」です。実行しなかった場合は普通のコンポーネントと同じ挙動を示します。なので、親 Transform やシーンが削除されれば巻き込まれて消えます。
> 
> ※ インスタンスをプールに返却し損ねても特にペナルティーはありません。


## プール返却時の階層構造

- PoolableA
    - PoolableB
        - PoolableC
    - NonPoolableComponent

このような階層構造で `PoolableA.ReturnToPool()` を実行すると、`B` `C` もプールに返却されます。つまり、階層構造が維持されずフラットになります。ただし Poolable ではない `NonPoolableComponent` は `A` の階層に残ります。


## 貸出時の挙動をカスタマイズしたい

`Rent` メソッドにオーバーロードを追加します。シグネチャが同じなら `new` します。

```cs
// 引数無しで常に良い感じにするメソッドに出来ないならカスタマイズしなくて良い
public static MyBehaviour Rent()
{
    var result = Rent(false, parent: appropriateTransform);
    result.AwesomeSetup();
    result.gameObject.SetActive(true);
    return result;
}

// new 宣言を Obsolete して戻り値の型を変えておけば元のオーバーロードが間違って使われるのを防げる
// Rent() 内では継承元のメソッドを直接呼んで迂回する → PoolableBehaviour<MyBehaviour>.Rent(true);
[Obsolete("Use Rent() instead", true)]
new public static void Rent(bool x, Transform? y = null, bool z = true)
    => throw new NotImplementedException();
```


## 返却時／削除時の挙動をカスタマイズしたい

`OnDisable` `OnDestroy` を実装します。別の方法でカスタマイズした場合、親 Transform やシーンに巻き込まれて削除された時の動作を保証できません。


## `Rent` を使わずに Poolable を GameObject にアタッチした場合

多分動きます。


## 複数の `PoolableBehaviour` をひとつの GameObject にアタッチした場合

動く気がします。





# ソースコード

https://gist.github.com/sator-imaging/953566d60e79fa7899ddef31a3852278




--

サクッと作れるかと思ったら意外と大変でした。

以上です。お疲れ様でした。