Unity實現簡單的對象池

一、簡介

先說說為什麼要使用對象池
在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不斷生成,可以自行設置計時器來限制生成的速度

Tags: