前天寫了篇文章[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); 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