Unity實現簡單的物件池

AlphaIcarus發表於2022-05-05

一、簡介

先說說為什麼要使用物件池
在Unity遊戲執行時,經常需要生成一些物體,例如子彈、敵人等。雖然Unity中有Instantiate()方法可以使用,但是在某些情況下並不高效。特別是對於那些需要大量生成又需要大量銷燬的物體來說,多次重複呼叫Instantiate()方法和Destory()方法會造成大量的效能消耗。
這時使用物件池是一個更好的選擇。
那麼什麼是物件池呢?
簡單來說,就是在一開始建立一些物體(或物件),將它們隱藏(休眠)起來,物件池就是這些物體的集合,當需要使用的時候,就將需要的物件啟用然後使用,而不是例項化生成。如果物件池中的物件消耗完了可以擴大物件池或者重新再次使用物件池中的物件。
一般情況下,一個物件池中存放的都是一類物體,我們一般希望建立多個物件池來儲存不同型別的物體。
例如我們需要兩個物件池來分別儲存球體和立方體。
那麼可以選擇使用Dictionary來建立物件池,這樣不僅可以建立物件池,還能指定每個物件池儲存物件的型別。這樣就能通過Tag來訪問物件池。
至於物件池中可以使用Queue(佇列)來儲存具體的物件,佇列不僅可以快速獲取到第一個物件,能夠按順序獲取物件。如果出隊的物件在使用完成之後再次入隊,那麼這樣就可以一直迴圈來重用物件。

二、Unity中的具體實現

新建一個Unity專案,在場景中新增一個空物體,命名為ObjectPool
同時製作一個黑色的地面便於顯示和觀察

新建指令碼ObjectPooler新增到ObjectPool上

public class ObjectPooler : MonoBehaviour
{
    [System.Serializable]   
    public class Pool    //物件池類
    {
        public string tag;          //物件池的Tag(名稱)
        public GameObject prefab;   //物件池所儲存的物體型別
        public int size;            //物件池的大小
    }
    public List<Pool> pools;        
    
    Dictionary<string, Queue<GameObject>> poolDictionary;  //宣告字典

    void Start()
    {
        //例項化字典                  物件池的Tag   物件池儲存的物體
        poolDictionary = new Dictionary<string, Queue<GameObject>>();
    }
}

在Inspector中新增對應的資料,這裡簡單建立了立方體和球體並設為了預製體

然後繼續修改ObjectPooler

public class ObjectPooler : MonoBehaviour
{
    [System.Serializable]   
    public class Pool
    {
        public string tag;
        public GameObject prefab;
        public int size;
    }
    public List<Pool> pools;
    Dictionary<string, Queue<GameObject>> poolDictionary;

    public static ObjectPooler Instance;    //單例模式,便於訪問物件池
    private void Awake()
    {
        Instance = this;
    }
    void Start()
    {
        poolDictionary = new Dictionary<string, Queue<GameObject>>();
        foreach (Pool pool in pools)
        {
            Queue<GameObject> objectPool = new Queue<GameObject>();     //為每個物件池建立佇列
            for (int i = 0; i < pool.size; i++)
            {
                GameObject obj = Instantiate(pool.prefab);
                obj.SetActive(false);   //隱藏物件池中的物件
                objectPool.Enqueue(obj);//將物件入隊
            }
            poolDictionary.Add(pool.tag, objectPool);   //新增到字典後可以通過tag來快速訪問物件池
        }
    }

    public GameObject SpawnFromPool(string tag, Vector3 positon, Quaternion rotation)     //從物件池中獲取物件的方法
    {
        if (!poolDictionary.ContainsKey(tag))  //如果物件池字典中不包含所需的物件池
        {
            Debug.Log("Pool: " + tag + " does not exist");
            return null;
        }

        GameObject objectToSpawn = poolDictionary[tag].Dequeue();  //出隊,從物件池中獲取所需的物件
        objectToSpawn.transform.position = positon;  //設定獲取到的物件的位置
        objectToSpawn.transform.rotation = rotation; //設定物件的旋轉
        objectToSpawn.SetActive(true);                //將物件從隱藏設為啟用

        poolDictionary[tag].Enqueue(objectToSpawn);     //再次入隊,可以重複使用,如果需要的物件數量超過物件池內物件的數量,在考慮擴大物件池
        //這樣重複使用就不必一直生成和消耗物件,節約了大量效能
        return objectToSpawn;  //返回物件
    }
}

新建指令碼CubeSpanwer,來使用物件池生成物體

public class CubeSpanwer : MonoBehaviour
{
    ObjectPooler objectPooler;
    private void Start()
    {
        objectPooler = ObjectPooler.Instance;
    }
    private void FixedUpdate()
    {
        //這樣會高效一點,比ObjectPooler.Instance
        objectPooler.SpawnFromPool("Cube", transform.position, Quaternion.identity);
    }
}

新建指令碼Cube,新增到Cube預製體上,讓其在生成時新增一個力便於觀察
注意:為了方便觀察這裡移除了Cube上的BoxCollider

public class Cube : MonoBehaviour
{
    void Start()
    {
        GetComponent<Rigidbody>().AddForce(new Vector3(Random.Range(0f, 0.2f), 1f, Random.Range(0f, 0.2f)));
    }
}

我們發現Cube並沒有向上飛起而是堆疊在一起

這時因為Cube只在生成時在Start中新增了力,只呼叫了一次,但馬上就被隱藏放入物件池了,等到再次取出時,並沒有任何方法的呼叫,只是單純設定位置

我們需要讓cube物件知道自己被重用了,再次呼叫新增力的方法
新建介面 IPooledObject

public interface IPooledObject
{
    void OnObjectSpawn();
}

然後讓Cube繼承該介面

public class Cube : MonoBehaviour, IPooledObject
{
    private Rigidbody rig;
    public void OnObjectSpawn()
    {
        rig = gameObject.GetComponent<Rigidbody>();
        rig.velocity = Vector3.zero;	//將速度重置為0,物體在被隱藏時仍然具有速度,不然重用時仍然具有向下的速度
        rig.AddForce(new Vector3(Random.Range(0, 0.2f), 10, Random.Range(0, 0.2f)), ForceMode.Impulse);
    }
}

然後修改ObjectPooler,讓Cube在被重用時呼叫重用的方法

public GameObject SpawnFromPool(string tag, Vector3 positon, Quaternion rotation)     //從物件池中獲取物件的方法
    {
        ......
        IPooledObject pooledObj = objectToSpawn.GetComponent<IPooledObject>();
        if (pooledObj != null)  //判斷,並不是所有物件都繼承了該介面,例如Cube我想讓它向上飛,Sphere則讓它直接生成,Sphere就不必繼承IPoolObject介面
        {
            pooledObj.OnObjectSpawn();  //呼叫重用時的方法
        }
        poolDictionary[tag].Enqueue(objectToSpawn);
        return objectToSpawn;
    }

執行結果:

Cube從CubeSpawner不斷生成,可以自行設定計時器來限制生成的速度

相關文章