Unity的C#編程教程_64_對象池 Object Pooling 詳解及應用練習

目錄:

Object Pooling Design Pattern

  • object polling 是設計範式的一種
    • 是一種優化的範式,尤其對於移動化來說非常重要
    • 不是生成和銷毀遊戲對象,而是循環利用遊戲對象

假設我們創建一個射擊遊戲,我們射出的子彈都是遊戲對象,傳統的做法是不斷從槍口生成,然後擊中目標後不斷銷毀,這個利用的是垃圾回收機制,只有當前幀的垃圾回收完成後,才能開始處理下一幀,所以如果有很多的遊戲對象要生成和銷毀,那麼遊戲可能就會卡住。

替代的優化解決方案是 object polling,我們可以循環利用這些子彈,只生成一次,然後在整個程式運行過程中不斷循環使用。

創建一個遊戲對象 Player,並掛載同名腳本:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Player : MonoBehaviour
{
    [SerializeField]
    private GameObject _bulletPrefab;

    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            Instantiate(_bulletPrefab);
        }
    }
}

設計一個 Bullet 的 Prefab,掛載同名腳本,然後拖拽賦值:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Bullet : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        Destroy(this.gameObject, 2f); // 2 秒鐘後自動銷毀
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

這樣我們進入遊戲後,按下空格鍵就會創建 Bullet,過了 2 秒鐘自動銷毀。這個是大多數初學者的做法。

這樣做的話,就是在利用垃圾回收機制。

下節我們將探討如何轉化為 object pooling

Challenge: Pool Manager

  • 任務說明:
    • 創建一個 Pool Manager class
    • 存儲所有需要用的 Bullet
    • 這樣我們就可以有個預設的 Bullet 列表供使用,比如 10 個,可以用來循環使用

創建遊戲對象 Pool Manager,掛載同名腳本:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PoolManager : MonoBehaviour
{
    [SerializeField] // 在 inspector 中可見,用於拖拽賦值
    private GameObject _bulletPrefab; // 子彈的 prefab 需要賦值

    // 以下為 PoolManager 的 singleton 定義部分,用於簡易調用
    private static PoolManager _instance;
    public static PoolManager Instance
    {
        get
        {
            if (_instance == null)
            {
                Debug.LogError("no instance");
            }

            return _instance;
        }
    }

    private void Awake()
    {
        _instance = this;     
    }

    // 以下用於定義 Bullet 的對象池
    public List<GameObject> bulletList;
    int i;
    bool fullBullet;

    public void CreatBullet()
    {
        if (fullBullet == false) // 如果池子沒滿
        {
            GameObject obj = Instantiate(_bulletPrefab); // 生成遊戲對象實例
            bulletList.Add(obj); // 添加到列表中(池子中)

            if (bulletList.Count > 9) // 池子滿了
            {
                fullBullet = true; // 修改 flag
            }
        }
        else if(i < 10) // 池子滿了以後就可以直接激活,而不用生成了
        {
            bulletList[i].SetActive(true); // 挨個激活
            i++;
        }
        else
        {
            i = 0; // 激活一輪以後再從頭開始
        }
        
    }


    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

這裡將其設計為 singleton 便於訪問。

玩家 Player 腳本,控制開槍:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Player : MonoBehaviour
{

    // Start is called before the first frame update
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space)) // 按下空格鍵表示開槍
        {
            PoolManager.Instance.CreatBullet(); // 調用 PoolManager 生成子彈
        }
    }
}

子彈 Bullet 的腳本,控制子彈的滅活方式:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Bullet : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }

    private void OnEnable() // 遊戲對象被激活的時候調用
    {
        StartCoroutine(bulletDis()); // 啟動協程
    }


    IEnumerator bulletDis() // 定義協程
    {
        yield return new WaitForSeconds(2); // 等待2秒

        this.gameObject.SetActive(false); // Bullet滅活
    }
}

同樣的任務我們可以有不同的實現思路,以下是另一種稍有區別的思路,在遊戲啟動的時候就一下子形成整個 Bullet 列表,之前是每次按下空格生成一個直至添加滿列表:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PoolManager : MonoBehaviour
{
    [SerializeField] // 在 inspector 中可見,用於拖拽賦值
    private GameObject _bulletPrefab; // 子彈的 prefab 需要賦值

    // 以下為 PoolManager 的 singleton 定義部分,用於簡易調用
    private static PoolManager _instance;
    public static PoolManager Instance
    {
        get
        {
            if (_instance == null)
            {
                Debug.LogError("no instance");
            }

            return _instance;
        }
    }

    private void Awake()
    {
        _instance = this;     
    }

    // 以下用於定義 Bullet 的對象池
    [SerializeField]
    private List<GameObject> _bulletList;
    [SerializeField]
    private GameObject _bulletContainer; // 生成的子彈遊戲對象匯總到一個母文件下面

    private int i;

    public void CreatBullet()
    {
        if(i < 10) // 池子滿了以後就可以直接激活,而不用生成了
        {
            if (_bulletList[i].activeSelf == false) // 這個修正可以確保按下發射鍵過快時不出現問題
            {
                _bulletList[i].SetActive(true); // 挨個激活
                i++;
            }
        }
        else
        {
            i = 0; // 激活一輪以後再從頭開始
        }
        
    }

    private void CreatBulletList(int bulletCount)
    {
        for(int j = 0; j < bulletCount; j++)
        {
            GameObject bullet = Instantiate(_bulletPrefab); // 生成遊戲對象實例
            bullet.transform.parent = _bulletContainer.transform; // 設置母文件夾
            bullet.SetActive(false); // 生成後即刻滅活,不顯示出來

            _bulletList.Add(bullet); // 添加到列表中(池子中)
        }
    }


    // Start is called before the first frame update
    void Start()
    {
        CreatBulletList(10);
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

創建遊戲對象 BulletContainer,拖拽賦值到 PoolManager 腳本下面的 Bullet Container 框中。

運行後可實現預期效果。

Challenge: Request from Pool Manager

  • 任務說明:
    • 創建子彈的對象池
    • Player 每次按下空格鍵,從對象池獲取一個未激活的子彈,並進行激活
    • 如果對象池中沒有未激活的子彈,則生成一個子彈, 並額外添加到對象池中

PoolManager 腳本,掛載到 PoolManager 遊戲對象上:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PoolManager : MonoBehaviour
{
    [SerializeField] // 在 inspector 中可見,用於拖拽賦值
    private GameObject _bulletPrefab; // 子彈的 prefab 需要賦值

    // 以下為 PoolManager 的 singleton 定義部分,用於簡易調用
    private static PoolManager _instance;
    public static PoolManager Instance
    {
        get
        {
            if (_instance == null)
            {
                Debug.LogError("no instance");
            }

            return _instance;
        }
    }

    private void Awake()
    {
        _instance = this;     
    }

    // 以下用於定義 Bullet 的對象池
    [SerializeField]
    private List<GameObject> _bulletList;
    [SerializeField]
    private GameObject _bulletContainer; // 生成的子彈遊戲對象匯總到一個母文件下面

    public GameObject CreatBullet()
    {
        foreach(var bullet in _bulletList) // 遍歷子彈池子
        {
            if (bullet.activeSelf == false) // 如果還有沒被激活的子彈
            {
                bullet.SetActive(true); // 激活
                return bullet; // 跳出該方法
            }
        }

        // 如果所有子彈都被激活了還不夠用,則生成新的子彈,並添加到子彈池子
        GameObject newBullet = Instantiate(_bulletPrefab); // 生成遊戲對象實例,並激活
        newBullet.transform.parent = _bulletContainer.transform; // 設置母文件夾

        _bulletList.Add(newBullet); // 添加到列表中(池子中)
        return newBullet; // 跳出該方法
    }

    private void CreatBulletList(int bulletCount)
    {
        for(int j = 0; j < bulletCount; j++)
        {
            GameObject bullet = Instantiate(_bulletPrefab); // 生成遊戲對象實例
            bullet.transform.parent = _bulletContainer.transform; // 設置母文件夾
            bullet.SetActive(false); // 生成後即刻滅活,不顯示出來

            _bulletList.Add(bullet); // 添加到列表中(池子中)
        }
    }

    // Start is called before the first frame update
    void Start()
    {
        CreatBulletList(10);
     }

    // Update is called once per frame
    void Update()
    {
        
    }
}

Player 腳本,掛載到 Player 遊戲對象上:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Player : MonoBehaviour
{

    // Start is called before the first frame update
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space)) // 按下空格鍵表示開槍
        {
            _ = PoolManager.Instance.CreatBullet();
            // 調用 PoolManager 生成子彈
            // 遊戲中如果需要對這個子彈產生位移等操作,程式碼也可以放在這裡
        }
    }
}

Bullet 腳本,掛載到 Bullet 的 prefab 上:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Bullet : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }

    private void OnEnable() // 遊戲對象被激活的時候調用
    {
        StartCoroutine(bulletDis()); // 啟動協程
    }


    IEnumerator bulletDis() // 定義協程
    {
        yield return new WaitForSeconds(2); // 等待2秒

        this.gameObject.SetActive(false); // Bullet滅活
    }
}

創建遊戲對象 BulletContainer,拖拽賦值到 PoolManager 腳本下面的 Bullet Container 框中。

Recycle the Pool

  • 循環使用遊戲對象池的另一種方法

前面我們提到了使用協程可以達到循環使用對象池,其實我們還有別的選擇:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Bullet : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }

    private void OnEnable() // 遊戲對象被激活的時候調用
    {
        Invoke("Inactive",2); // 2秒以後運行 Inactive 方法
    }


    private void Inactive() // 反激活
    {
        this.gameObject.SetActive(false);
    }
}

Tags: