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

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

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

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

實作方式

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

  • GameObjectPool
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameObjectPool {

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

    public GameObjectPool (PooledGameObject prefab, int initailSize) {
        m_prefab = prefab;
        for (int i = 0; i < initailSize; i++) {
            PooledGameObject go = GameObject.Instantiate<PooledGameObject> (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<PooledGameObject> (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<PooledGameObject> (m_prefab, parent);
                go.pool = this;
                m_usingObjects.Add (go);
                return go;
            }
        }
    }

    public void BackToPool (PooledGameObject go) {
        lock (m_availableObjects) {
            m_availableObjects.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
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,如果有需要可以參考:

 

** 2017/08/23 Update:

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

廣告

發表迴響

在下方填入你的資料或按右方圖示以社群網站登入:

WordPress.com Logo

您的留言將使用 WordPress.com 帳號。 登出 / 變更 )

Twitter picture

您的留言將使用 Twitter 帳號。 登出 / 變更 )

Facebook照片

您的留言將使用 Facebook 帳號。 登出 / 變更 )

Google+ photo

您的留言將使用 Google+ 帳號。 登出 / 變更 )

連結到 %s