[Unity] 另一個物件池的實現 – Another Practicing of Object Pool

前天寫了篇文章[Unity] 物件池的實現 - Practicing of Object Pool的使用目的,以及一個簡單的實作例子。

本文是要介紹另外一個新出爐的實作方式,考慮跟 Unity 機制做配合,以改善下面兩點:

  • 因為需要在場景中有一個物件來掛載物件池,如果要切換場景但保留物件池,需要特別建立為 DontDestory 物件。
  • 如果不是本來就設置在場景中,而是要跟 Resources 或 AssetBundle 中的 Prefab 做動態的配合,會不方便建立新的物件池。

實作方式

這個實作物件池 (Object Pool) 的方式共用上了兩個類別,分別是沒有繼承MonoBehaviour 的GameObjectPool,以及繼承了 MonoBehaviour 的PooledGameObject

GameObjectPool

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameObjectPool {

private PooledGameObject m_prefab;
private List m_availableObjects = new List ();
private List m_usingObjects = new List ();

public GameObjectPool (PooledGameObject prefab, int initailSize) {
m_prefab = prefab;
for (int i = 0; i < initailSize; i++) {
PooledGameObject go = GameObject.Instantiate (m_prefab);
go.pool = this;
m_availableObjects.Add (go);
go.gameObject.SetActive (false);
}
}

public GameObjectPool (PooledGameObject prefab, Transform anchor, int initailSize) {
m_prefab = prefab;
for (int i = 0; i < initailSize; i++) {
PooledGameObject go = GameObject.Instantiate (m_prefab, anchor);
go.pool = this;
m_availableObjects.Add (go);
go.gameObject.SetActive (false);
}
}

public PooledGameObject GetPooledInstance (Transform parent) {
lock (m_availableObjects) {
int lastIndex = m_availableObjects.Count - 1;
if (lastIndex >= 0) {
PooledGameObject go = m_availableObjects[lastIndex];
m_availableObjects.RemoveAt (lastIndex);
m_usingObjects.Add (go);
go.gameObject.SetActive (true);
if (go.transform.parent != parent) {
go.transform.SetParent (parent);
}
return go;
} else {
PooledGameObject go = GameObject.Instantiate (m_prefab, parent);
go.pool = this;
m_usingObjects.Add (go);
return go;
}
}
}

public void BackToPool (PooledGameObject go) {
lock (m_availableObjects) {
m_availableObjects.Add (go);
m_usingObjects.Add (go); // Update 2018/4/19: 補漏
go.gameObject.SetActive (false);
}
}

public void Clear (bool includeUsingObject = true) {
lock (m_availableObjects) {
for (int i = m_availableObjects.Count - 1; i >= 0; i--) {
PooledGameObject go = m_availableObjects[i];
m_availableObjects.RemoveAt (i);
GameObject.Destroy (go.gameObject);
}
}
if (includeUsingObject) {
lock (m_usingObjects) {
for (int i = m_usingObjects.Count - 1; i >= 0; i--) {
PooledGameObject go = m_usingObjects[i];
m_usingObjects.RemoveAt (i);
GameObject.Destroy (go.gameObject);
}
}
}
}
}

PooledGameObject

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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PooledGameObject : MonoBehaviour {

[SerializeField]
private int m_initailSize = 5;

private GameObjectPool m_pool;
public GameObjectPool pool {
get {
if (m_pool == null) {
m_pool = new GameObjectPool (this, m_initailSize);
}
return m_pool;
}
set {
m_pool = value;
}
}

public void InitailizePool (Transform anchor) {
if (m_pool == null) {
m_pool = new GameObjectPool (this, anchor, m_initailSize);
}
}

public PooledGameObject GetPooledInstance (Transform parent) {
return this.pool.GetPooledInstance (parent);
}

public void BackToPool () {
this.pool.BackToPool (this);
}

public void Clear (bool includeUsingObject = true) {
this.pool.Clear (includeUsingObject);
}
}

使用上,必須將 PooledGameObject 掛載到要使用物件池的物件上,通常會是一個 Prefab。

PooledGameObject 的 Inspector 視窗上可以調整 m_initailSize 參數的值,來決定物件池初始化時,要預先準備多少數量的物件進行待命。

呼叫物件的方式

只要 Prefab 被 Load 到任何腳本之中,或者預先 Invoke Reference 在場景的某腳本上,都可以輕易 GetComponent () 來取得物件池腳本,並用 GetPooledInstance () 取得第一個 clone 物件。

而 PooledGameObject 準備了三個主要的操作手段:

  • GetPooledInstance (Transform parent)可以由物件池中取得一個新個體,相當於不使用物件池時的 Instantiate () 動作。
  • BackToPool ()可以將物件本身退回物件池中,用於取代 Destroy () 的動作。
  • Clear (bool includeUsingObject)可以清空物件池,Destroy 所有物件池管理的對象物件。includeUsingObject 如果為 false,則尚在使用中的物件不會連帶清除。

與上一篇文章的實作方法不同,不是以物件池的管理器做為操作對象,而是用被管理的物件作為操作對象。這樣子的變化,可以免除場景中需要一個物件來掛載腳本的需求,進而改善文章開頭提出的兩點問題。

而作為管理器的 GameObjectPool,則是使用了 Lazy Initialization 的 C# property 應用,只有在第一次被呼叫的時候,才會開始建立物件池本體。

而一般設想的使用方式下,這個物件池的管理器 (GameObjectPool)本體將被 Prefab 本體首次呼叫與建立,後續被實體化的 clone 物件,則直接注入 (Injection) 同一個 GameObjectPool 到 PooledGameObject 之中,不再需要新建,保證所有的實體都在一個管理器的管轄之下。

** 換句話說,PooledGameObject 的 Line 13 ~ 15 只會在 Prefab 執行一次;其餘 clone 物件則會在實體化當下執行 Line 19。

雖然 Lazy Initialization 的好處是執行時機不須主動決定,會在需求發生時才執行。但是如果 initailSize 的值設定的較大,將會導致第一次使用物件池時有巨大的效能消耗,所以也提供了InitailizePool ()這個方法來讓使用者主動去初始化物件池。

後話

到目前為止我還蠻喜歡這個實作方式,不過因為使用上不會有一個管理器物件在場景上可以觀察,其實會有點黑盒子的感覺。

這篇文章的描寫如果不能夠清楚的表達這個實作方式的機制,歡迎提出來讓我知道。

另外雖然我盡可能去設想情境了,但如果有發現這個實作方式會在特定情況遺失 GameObjectPool 的參照,造成 Memory leak 的發生,也請告訴我,讓我進行改善。

本篇實作有分享專案在 Github,如果有需要可以參考:

https://github.com/douduck08/Unity-ObjectPool

** 2017/08/23 Update:

PooledGameObject 針對 pool 初始化做了一點修改,減少 transform.parent 的切換次數,修改後的 PooledGameObject.cs 可以參考:

https://github.com/douduck08/Unity-ObjectPool/blob/master/Assets/Scripts/ObjectPool/PooledGameObject.cs