Unity優化篇:物件池的建立與使用。(簡單且實用)
阿新 • • 發佈:2018-12-13
1.物件池是什麼?
物件池是一種Unity經常用到的記憶體管理服務,它的作用在於可以減少建立每個物件的系統開銷。
2.為什麼要使用物件池?
在Unity遊戲開發的過程中經常會建立一些新的物件,如果數量較少還可以接受,如果建立的新物件數量龐大,那麼對記憶體而言是一個極大的隱患。例如射擊遊戲當中,每發射一顆子彈,都要建立一個新的子彈物件,那麼子彈是數量龐大,可想而知一場遊戲當中會建立多少這樣的新物件,那麼如果這些子彈建立之後都對遊戲起著關鍵且持續性的作用也無可厚非,問題是子彈發射完成之後,幾秒之後就不再擁有任何的意義,一般會將它自動的隱藏,也就是我們所說的SetActive(false),因此大量的非活躍物件出現在遊戲場景當中。
3.怎麼建立並使用物件池?
物件池背後的理念其實是非常簡單的。我們將物件儲存在一個池子中,當需要時在再次使用,而不是每次都例項化一個新的物件。池的最重要的特性,也就是物件池設計模式的本質是允許我們獲取一個“新的”物件而不管它真的是一個新的物件還是迴圈使用的物件。
我們需要兩個字典,一個動態儲存和去除當前所需要的遊戲物體,另一個當記事本,記錄那些遊戲物體被記錄過,那些沒有,並且據此來判定是否需要建立新的物件池。
建立物件池:
我修改了一下GetObj函式,讓它能在指定位置生成遊戲物體。
using System.Collections; using System.Collections.Generic; using UnityEngine; public class ObjectPool { public const string stoneExplosion = "done_explosion_asteroid"; public const string enemyExplosion = "done_explosion_enemy"; public const string playerExplosion= "done_explosion_player"; public const string redStone = "Asteroid_Lrg_B_01"; public const string boss1 = "Boss1"; public const string shuijing = "Crystal_Lrg_A_01"; public const string blackStone = "Done_Asteroid 02"; public const string playerAttack = "Done_Bolt"; public const string enemyAttack = "Done_Bolt-Enemy"; public const string player = "Done_Player"; public const string purpleEnemy = "Done_PurpleEnemy"; public const string redEnemy = "Done_RedEnemy"; /// <summary> /// 物件池 /// </summary> private Dictionary<string, List<GameObject>> pool; /// <summary> /// 預設體 /// </summary> private Dictionary<string, GameObject> prefabs; #region 單例 private static ObjectPool instance; private ObjectPool() { pool = new Dictionary<string, List<GameObject>>(); prefabs = new Dictionary<string, GameObject>(); } public static ObjectPool GetInstance() { if (instance == null) { instance = new ObjectPool(); } return instance; } #endregion /// <summary> /// 從物件池中獲取物件 /// </summary> /// <param name="objName"></param> /// <returns></returns> public GameObject GetObj(string objName,Vector3 position,Quaternion quaternion) { //結果物件 GameObject result = null; //判斷是否有該名字的物件池 if (pool.ContainsKey(objName)) { //物件池裡有物件 if (pool[objName].Count > 0) { //獲取結果 result = pool[objName][0]; //啟用物件 result.transform.position = position; result.transform.rotation = quaternion; result.SetActive(true); //從池中移除該物件 pool[objName].Remove(result); //返回結果 return result; } } //如果沒有該名字的物件池或者該名字物件池沒有物件 GameObject prefab = null; //如果已經載入過該預設體 if (prefabs.ContainsKey(objName)) { prefab = prefabs[objName]; } else //如果沒有載入過該預設體 { //載入預設體 prefab = Resources.Load<GameObject>("Prefabs/" + objName); //更新字典 prefabs.Add(objName, prefab); } //生成 result = Object.Instantiate(prefab); result.transform.position = position; result.transform.rotation = quaternion; //改名(去除 Clone) result.name = objName; //返回 return result; } /// <summary> /// 回收物件到物件池 /// </summary> /// <param name="objName"></param> public void RecycleObj(GameObject obj) { //設定為非啟用 obj.SetActive(false); //判斷是否有該物件的物件池 if (pool.ContainsKey(obj.name)) { //放置到該物件池 pool[obj.name].Add(obj); } else { //建立該型別的池子,並將物件放入 pool.Add(obj.name, new List<GameObject>() { obj }); } } }
使用物件池:
把用到Instantiate的地方替換為GetObj,
把用到Destroy的地方替換為RecycleObj,
注意點:
1.由於物件池是採用將遊戲物體的狀態設定為true和false來實現目的,所以有些遊戲物體掛載的指令碼的Start()函式和Awake()函式需要根據情況來更改一下。(可以考慮一下OnEnable,OnDisable)
- 很多型別的物件被重新使用前,在某些情況下,需要被reset。至少,所有的成員變數都要設定成初始值。這可以在池中實現而不需要使用者處理。何時和如何重置需要考慮以下兩個方面:
- 重置是立即的(例如,在儲存物件時即重置)還是延遲的(例如,在物件被重新使用後重置)。
- 重置是被池管理(例如,對於被放入池中的物件來說是透明的)還是宣告池物件的類。
- 建立管理所有型別池的ObjectPool。
- 某些型別的資源是很珍貴的(如資料庫連線),池需要顯示上限並提供一個針對分配物件失敗的安全措施;
- 當池中物件很多卻很少使用時,或許需要收縮的功能(不管是自動的還是強制的)。
- 最後,池可以被多個執行緒共享,因此需要實現為執行緒安全的。
-
那麼其中那些是必需的呢?你的答案或許和我的不一樣,但請允許我闡述我的觀點:
- 重置是必需的。但是正如你將在下面看的那樣,我並沒有強制到底是在池中還是被管理類中處理重置邏輯。你可能兩種都需要,之後的程式碼中我將向你展示各自兩個版本。
- Unity強制限制多執行緒。你可以在主執行緒中定義工作者執行緒,但只有主執行緒可以呼叫Unity API。以我的經驗看來,我們並不需要將池實現為支援多執行緒。
- 僅個人而言,我並不介意每次為一個型別申明一個新的池。可選的方案是採用單例模式:建立一個新的物件池並放置於儲存池的字典中,該字典放置在一個靜態變數中。為了安全使用,你需要將將你的物件池實現為支援多執行緒。但就我看到的物件池而言沒有一個是100%安全的。
- 在本篇文章中我重點處理記憶體。其它型別資源池也是很重要的,但超出本篇文章的範圍。這很大程度上減少了以下的需求:
- 不需要一個作限制用的最大值。如果你的遊戲使用太多的資源,你已經陷入麻煩了,物件池也救不了你。
- 我們也可以假設沒有其它程序等待你儘快釋放記憶體。這就意味著重置可以是延遲的,也不需要提供收縮功能。