Object Pool pattern

物件池(Object Pool):是一種減輕大量建立並銷毀物件時 CPU 負擔的設計模式。使用物件池時,物件會先被建立並放入池中等待,需要時應用程式不會新建物件,而是從物件池中取得並啟用它。當使用完畢後,物件不會被銷毀,而是被放回物件池中。

改進

  • 可以在程式載入時建立物件池,這樣使用者就不會感到卡頓。
  • 考慮將物件池設為static或是singleton的:這樣可以在所有情況下方便呼叫使用。
  • 使用Dictionary來管理多個物件池:如果有多個物件池,可以使用 Key-Value 的資料結構(如 Dictionary)來管理,只需根據對應的 Key 就能方便地取得所需的物件池。
  • 注意釋放物件池中的物件:確保物件在物件池中時不會被釋放,避免執行期間發生錯誤。
  • 設定物件池上限:物件過多會消耗大量記憶體,因此需要為物件池設定一個上限,避免物件池中物件過多。

以下是一個簡單的物件池,

  • 這個物件池使用Stack
  • SetupPool()用來產生一些物件放到物件池中
  • GetPooledObject()用來啟用物件並取得,當物件不足(stack.Count == 0)時,會再產生一個新物件
  • ReturnToPool()用來將物件停用,並返回物件到物件池
  • 注意不要直接使用,因為這個例子沒有設定Stack上限,可能會產生過多的物件
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    public class ObjectPool : MonoBehaviour
    {
    [SerializeField] private uint initPoolSize;
    [SerializeField] private PooledObject objectToPool;
    // 存儲物件池中的物件
    private Stack<PooledObject> stack;
    private void Start()
    {
    SetupPool();
    }
    // 建立物件池(在卡頓不明顯時調用)
    private void SetupPool()
    {
    stack = new Stack<PooledObject>();
    PooledObject instance = null;
    for (int i = 0; i < initPoolSize; i++)
    {
    instance = Instantiate(objectToPool);
    instance.Pool = this;
    instance.gameObject.SetActive(false);
    stack.Push(instance);
    }
    }
    // 從物件池中返回第一個可用的物件
    public PooledObject GetPooledObject()
    {
    // 如果物件不夠,則實例化新的物件
    if (stack.Count == 0)
    {
    PooledObject newInstance = Instantiate(objectToPool);
    newInstance.Pool = this;
    return newInstance;
    }
    // 否則,從物件池中取得下一個物件
    PooledObject nextInstance = stack.Pop();
    nextInstance.gameObject.SetActive(true);
    return nextInstance;
    }
    public void ReturnToPool(PooledObject pooledObject)
    {
    stack.Push(pooledObject);
    pooledObject.gameObject.SetActive(false);
    }
    }

建立一個 PooledObject 類別,讓它依賴 ObjectPool,這樣可以在使用完畢後返回物件池:

1
2
3
4
5
6
7
8
9
public class PooledObject : MonoBehaviour
{
private ObjectPool pool;
public ObjectPool Pool { get => pool; set => pool = value; }
public void Release()
{
pool.ReturnToPool(this);
}
}

在Unity 2021後,有內建 UnityEngine.Pool 物件池,它提供了多種容器的Pool,像是

  • ObjectPool:是一個Stack
  • DictionaryPool<T0,T1>
  • GenericPool
  • HashSetPool
  • LinkedPool
  • ListPool
    使用內建 UnityEngine.Pool 物件池有許多好處,包含
  • 可以快速的使用物件池,不需要重複造輪子
  • 有多種容器供你選用
  • 當到達最大物件數量後會摧毀該物件,不建立過多的物件

以下是UnityEngine.Pool的範例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
using UnityEngine.Pool;
public class RevisedGun : MonoBehaviour
{
// 使用基於 Stack 的 ObjectPool
private IObjectPool<RevisedProjectile> objectPool;
// 如果嘗試返回已經存在於池中的物件,則拋出異常
[SerializeField] private bool collectionCheck = true;
// 控制池的預設容量和最大大小
[SerializeField] private int defaultCapacity = 20;
[SerializeField] private int maxSize = 100;

private void Awake()
{
objectPool = new ObjectPool<RevisedProjectile>(CreateProjectile,
OnGetFromPool, OnReleaseToPool, OnDestroyPooledObject,
collectionCheck, defaultCapacity, maxSize);
}

// 在new ObjectPool建構子中使用,主要是先建立物件以填充物件池
private RevisedProjectile CreateProjectile()
{
RevisedProjectile projectileInstance = Instantiate(projectilePrefab);
projectileInstance.ObjectPool = objectPool;
return projectileInstance;
}

// 在new ObjectPool建構子中使用,告訴物件池在將物件返回時要做什麼動作
private void OnReleaseToPool(RevisedProjectile pooledObject)
{
pooledObject.gameObject.SetActive(false);
}

// 在new ObjectPool建構子中使用,告訴物件池在物件池中取出物件時要做什麼初始化動作
private void OnGetFromPool(RevisedProjectile pooledObject)
{
pooledObject.gameObject.SetActive(true);
}

// 在new ObjectPool建構子中使用,告訴物件池在超過最大物件數量時如何銷毀物件
private void OnDestroyPooledObject(RevisedProjectile pooledObject)
{
Destroy(pooledObject.gameObject);
}

private void FixedUpdate()
{
// 省略其他代碼
}
}

與上面簡單的ObjectPool相同,讓RevisedProjectile依賴 IObjectPool<RevisedProjectile>,在使用完畢後呼叫objectPool.Release(this)返回物件池:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class RevisedProjectile : MonoBehaviour
{
// 延遲後停用
[SerializeField] private float timeoutDelay = 3f;

private IObjectPool<RevisedProjectile> objectPool;

// 公共屬性,用於給 projectile 一個 ObjectPool 的引用
public IObjectPool<RevisedProjectile> ObjectPool { set => objectPool = value; }

public void Deactivate()
{
StartCoroutine(DeactivateRoutine(timeoutDelay));
}

private IEnumerator DeactivateRoutine(float delay)
{
yield return new WaitForSeconds(delay);

// 重置移動中的 Rigidbody
Rigidbody rBody = GetComponent<Rigidbody>();
rBody.velocity = Vector3.zero;
rBody.angularVelocity = Vector3.zero;

// 將 projectile 返回到物件池
objectPool.Release(this);
}
}

Reference: https://github.com/Unity-Technologies/game-programming-patterns-demo/tree/main/Assets/7%20Object%20Pool

評論