Unity物件池技術(原理+實戰)

LemonXQ發表於2017-08-13

寫在前面

  很早就聽說過物件池技術……然而一直到這幾天才真正去了解= =。還得感謝Jasper Flick的部落格,這裡推薦他的Unity C# Tutorials系列,目前我只看了前幾篇,收穫還是挺大的~本篇部落格也是基於這個系列中的一篇——Object Pools,加上個人的一些理解,對Unity的物件池技術進行簡單介紹。

物件池簡介

  顧名思義,物件池是存放物件的緩衝區。使用者可以從緩衝區中放入/取出物件。一類物件池存放一類特定的物件。那麼物件池有什麼用呢?在遊戲中,經常會有產生/銷燬大量同類遊戲物件的需求,比如遊戲中源源不斷的敵人、頻繁重新整理的寶箱、乃至一些遊戲特效(風、雨等)。如果沒有一種比較好的機制來管理這些物件的產生和銷燬,而是一昧的Instantiate和Destroy,將使你的遊戲效能大大下降,甚至出現卡死、崩潰……

物件池實現

  簡而言之,就是當需要使用一個物件的時候,直接從該類物件的物件池中取出(SetActive(true)),如果物件池中無可用物件,再進行Instantitate。而當不再需要該物件時,不直接進行Destroy,而是SetActive(false)並將其回收到物件池中。下面直接貼下程式碼:

PooledObject.cs

using UnityEngine;
/// <summary>
/// 所有需要使用物件池機制的物件的基類
/// </summary>
public class PooledObject : MonoBehaviour
{
    // 歸屬的池
    public ObjectPool Pool { get; set; }

    // 場景中某個具體的池(不可序列化)
    [System.NonSerialized]
    private ObjectPool poolInstanceForPrefab;

    /// <summary>
    /// 回收物件到物件池中
    /// </summary>
    public void ReturnToPool()
    {
        if (Pool)
        {
            Pool.AddObject(this);
        }
        else
        {
            Destroy(gameObject);
        }
    }

    /// <summary>
    /// 返回物件池中可用物件的例項
    /// </summary>
    public T GetPooledInstance<T>() where T : PooledObject
    {
        if (!poolInstanceForPrefab)
        {
            poolInstanceForPrefab = ObjectPool.GetPool(this);
        }
        return (T)poolInstanceForPrefab.GetObject();
    }
}

ObjectPool.cs

using UnityEngine;
using System.Collections.Generic;

public class ObjectPool : MonoBehaviour
{
    // 池中物件prefab
    private PooledObject prefab;

    // 儲存可用物件的緩衝區
    private List<PooledObject> availableObjects = new List<PooledObject>();

    /// <summary>
    /// 從池中取出物件,返回該物件
    /// </summary>
    public PooledObject GetObject()
    {
        PooledObject obj;
        int lastAvailableIndex = availableObjects.Count - 1;
        if (lastAvailableIndex >= 0)
        {
            obj = availableObjects[lastAvailableIndex];
            availableObjects.RemoveAt(lastAvailableIndex);
            obj.gameObject.SetActive(true);
        }
        else // 池中無可用obj
        {
            obj = Instantiate<PooledObject>(prefab);
            obj.transform.SetParent(transform, false);
            obj.Pool = this;
        }
        return obj;
    }

    /// <summary>
    /// 向池中放入obj
    /// </summary>
    public void AddObject(PooledObject obj)
    {
        obj.gameObject.SetActive(false);
        availableObjects.Add(obj);
    }

    /// <summary>
    /// 【靜態方法】建立並返回物件所屬的物件池
    /// </summary>
    public static ObjectPool GetPool(PooledObject prefab)
    {
        GameObject obj;
        ObjectPool pool;
        // 編輯器模式下檢查是否有同名pool存在,防止重複建立pool
        if (Application.isEditor)
        {
            obj = GameObject.Find(prefab.name + " Pool");
            if (obj)
            {
                pool = obj.GetComponent<ObjectPool>();
                if (pool)
                {
                    return pool;
                }
            }
        }
        obj = new GameObject(prefab.name + " Pool");
        DontDestroyOnLoad(obj);
        pool = obj.AddComponent<ObjectPool>();
        pool.prefab = prefab;
        return pool;
    }
}

實戰:七彩噴泉

【注:以下譯至前面提到的Object Pools一文,有部分刪減】

1.實現效果:

2.生成大量物體

  • 首先新建指令碼Stuff.cs,程式碼如下:
using UnityEngine;

[RequireComponent(typeof(Rigidbody))]
public class Stuff : MonoBehaviour {

    Rigidbody body;

    void Awake () {
        body = GetComponent<Rigidbody>();
    }
}
  • 建立Cube和Sphere,掛上Stuff指令碼。並將它們做成Prefab
  • 接下來需要建立StuffSpawner(孵化器),並掛上StuffSpawner指令碼,程式碼如下:
using UnityEngine;

public class StuffSpawner : MonoBehaviour {

    public float timeBetweenSpawns;

    public Stuff[] stuffPrefabs;

    float timeSinceLastSpawn;

    void FixedUpdate () {
        timeSinceLastSpawn += Time.deltaTime;
        if (timeSinceLastSpawn >= timeBetweenSpawns) {
            timeSinceLastSpawn -= timeBetweenSpawns;
            SpawnStuff();
        }
    }

    void SpawnStuff () {
        Stuff prefab = stuffPrefabs[Random.Range(0, stuffPrefabs.Length)];
        Stuff spawn = Instantiate<Stuff>(prefab);
        spawn.transform.localPosition = transform.position;
    }
}

  • 現在我們有了孵化器,可以在一個點產生Cube和Sphere,但這還不夠。我們可以給這些stuff一個初始速度及方向。
    public float velocity;

    void SpawnStuff () {
        Stuff prefab = stuffPrefabs[Random.Range(0, stuffPrefabs.Length)];
        Stuff spawn = Instantiate<Stuff>(prefab);
        spawn.transform.localPosition = transform.position;
        spawn.Body.velocity = transform.up * velocity;
    }
  • 執行一下可以發現一個個物體上升又下降,周而復始。如果你傾斜一下孵化器,會讓它看上去更像流動的物體。事實上,如果我們把多個孵化器分佈在一個環上,將得到類似噴泉的效果。因此,新建一個空物體StuffSpawnerRing,掛上如下指令碼:
using UnityEngine;

public class StuffSpawnerRing : MonoBehaviour {

    public int numberOfSpawners;

    public float radius, tiltAngle;

    public StuffSpawner spawnerPrefab;

    void Awake () {
        for (int i = 0; i < numberOfSpawners; i++) {
            CreateSpawner(i);
        }
    }
}
    void CreateSpawner (int index) {
        Transform rotater = new GameObject("Rotater").transform;
        rotater.SetParent(transform, false);
        rotater.localRotation =
            Quaternion.Euler(0f, index * 360f / numberOfSpawners, 0f);

        StuffSpawner spawner = Instantiate<StuffSpawner>(spawnerPrefab);
        spawner.transform.SetParent(rotater, false);
        spawner.transform.localPosition = new Vector3(0f, 0f, radius);
        spawner.transform.localRotation = Quaternion.Euler(tiltAngle, 0f, 0f);
    }
  • 現在將場景中的Spawner做成prefab並刪除,調整SpawnerRing的引數

3.新增銷燬區(KillZone)

  • 我們現在得到了無止盡生成的下落的物體。為了防止程式卡頓,我們需要引入銷燬區。所有進入銷燬區的物體都要被銷燬。
  • 建立一個帶有Box Collider的物體,設定為觸發器,為Collider設定一個非常大的size(如1000),並將其放置在噴泉下方某個位置。最後給該物體新增一個Tag以便能被正確識別

  • 重新編輯Stuff.cs,新增觸發器事件處理

    void OnTriggerEnter (Collider enteredCollider) {
        if (enteredCollider.CompareTag("Kill Zone")) {
            Destroy(gameObject);
        }
    }
  • 看看現在的效果:

4.加入可變因素

  • 目前我們的噴泉缺少隨機性,我們可以用隨機值代替固定值。因為我們要處理多個資料,所以讓我們建立一個結構體來更好地實現隨機化。
using UnityEngine;

[System.Serializable]
public struct FloatRange {

    public float min, max;

    public float RandomInRange {
        get {
            return Random.Range(min, max);
        }
    }
}
  • 隨機化生成時間
    public FloatRange timeBetweenSpawns;

    float currentSpawnDelay;

    void FixedUpdate () {
        timeSinceLastSpawn += Time.deltaTime;
        if (timeSinceLastSpawn >= currentSpawnDelay) {
            timeSinceLastSpawn -= currentSpawnDelay;
            currentSpawnDelay = timeBetweenSpawns.RandomInRange;
            SpawnStuff();
        }
    }

  • 隨機化物體scale和rotation
    public FloatRange timeBetweenSpawns, scale;

    void SpawnStuff () {
        Stuff prefab = stuffPrefabs[Random.Range(0, stuffPrefabs.Length)];
        Stuff spawn = Instantiate<Stuff>(prefab);

        spawn.transform.localPosition = transform.position;
        spawn.transform.localScale = Vector3.one * scale.RandomInRange;
        spawn.transform.localRotation = Random.rotation;

        spawn.Body.velocity = transform.up * velocity;
    }

  • 隨機化物體速度大小
    public FloatRange timeBetweenSpawns, scale, randomVelocity;

    void SpawnStuff () {
        …

        spawn.Body.velocity = transform.up * velocity +
            Random.onUnitSphere * randomVelocity.RandomInRange;
    }

  • 隨機化物體角速度
    void SpawnStuff () {
        …

        spawn.Body.velocity = transform.up * velocity +
            Random.onUnitSphere * randomVelocity.RandomInRange;
        spawn.Body.angularVelocity =
            Random.onUnitSphere * angularVelocity.RandomInRange;
    }

  • 隨機化材質(實現七彩)
    public Material[] stuffMaterials;

    void CreateSpawner (int index) {
        …

        spawner.stuffMaterial = stuffMaterials[index % stuffMaterials.Length];
    }

5.應用物件池進行管理

  • 讓Stuff繼承PooledObject(PooledObject程式碼見前),修改觸發器事件,進入銷燬區時不Destroy,而是呼叫ReturnToPool方法。
  • 接下來,我們需要改變StuffSpawner來讓它使用物件池來建立物件,而不是直接Instanstiate。如何做到呢?某種程度上我們需要擁有每個prefab的池,但我們不想要重複的池,也就是說所有孵化器都共享他們。當然,如果我們能直接從一個prefab得到一個池化的例項而不用考慮那些池本身將更加方便。
    void SpawnStuff () {
        Stuff prefab = stuffPrefabs[Random.Range(0, stuffPrefabs.Length)];
        Stuff spawn = prefab.GetPooledInstance<Stuff>();

        …
    }

其他

  1. 並非所有的物件都適合使用物件池來管理。需要在“物件生成的開銷”以及“維護物件池的開銷”之間進行權衡。
  2. 為避免在場景切換時重新生成pool,從而帶來效能損耗,可在程式碼中加入DontDestroyOnLoad(pool)
  3. 同樣,在場景切換時,應該將原場景中的物件回收進相應物件池中。即在OnLevelWasLoaded方法中呼叫ReturnToPool方法

相關文章