Unity的C#編程教程_64_對象池 Object Pooling 詳解及應用練習
目錄:
Challenge: Pool Manager
Challenge: Request from Pool Manager
Recycle the Pool
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);
}
}