最美情侣中文字幕电影,在线麻豆精品传媒,在线网站高清黄,久久黄色视频

歡迎光臨散文網(wǎng) 會員登陸 & 注冊

【Unity】工具類系列教程——對象池!

2017-11-01 17:29 作者:皮皮關(guān)做游戲  | 我要投稿

【為什么使用對象池】

游戲制作避免不了做游戲優(yōu)化,讓游戲達到60分容易,但從60分到90分就是一個漫長的優(yōu)化路程,因此提前接觸到優(yōu)化的知識在游戲開發(fā)設計的時候就能規(guī)避很多坑點(說是接觸而不是開展優(yōu)化是因為項目初期功能不明確,做太多優(yōu)化的功能其實是沒有意義的)。

例如游戲中會經(jīng)常用到很多個相同類型的物體,比如子彈特效,比如一個UI的背包。如果每次要用的時候都去創(chuàng)建用完就刪除掉,會造成頻繁的資源回收(GC),部分游戲玩著玩著就卡頓就源于此。

對象池是一種設計模式,是一種游戲經(jīng)常用到的腳本類型,了解對象池對游戲性能優(yōu)化非常有幫助。

【對象池初識】

對象池的核心思想是:預先初始化一組可重用的實體,而不是按需銷毀然后重建

就像做游戲一樣的,我們先做一個對象池原型的實現(xiàn)具體功能。

比如說一個界面:

,

界面的Item是隨著游戲進程增加而增加的。



我們利用對象池的核心思想就是“當我們第一次實例化好多個Item物體后,下一次打開如果界面更新就沒必要再創(chuàng)建第二次重復的資源物體了”,因此如果這個界面關(guān)閉的時候應該把資源臨時存放在一個“池”里面。

像這樣:



/*資源保存在“池”中,外部表現(xiàn)無非是Item物體修改了父物體,然后關(guān)閉激活,這是非常簡單的操作*/

當再打開界面的時候,因為池中有資源,所以就跳過了讀取資源實例化的步驟(資源加載和實例化是最消耗性能的)。

/*這里有必要強調(diào)的是,我們沒必要單獨去做第一次對象池的實例化,我們只需要知道,這個物體是重復的,所以這個物體我們都在對象池中去取。而對象池也只關(guān)注,當別人調(diào)用了我的創(chuàng)建實例函數(shù),我必須返回給它一個創(chuàng)建好的物體,而池子里面有沒有資源讓對象池自己判斷,如果沒有則創(chuàng)建,如果有則直接拿出來給它,這種設計方法叫做空池觸發(fā)*/

以下是我們做空池觸發(fā)的對象池腳本:

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

public class ObjectsPool : MonoBehaviour {

  [SerializeField]
   private GameObject _prefab;

   private Queue<GameObject> _pooledInstanceQueue = new Queue<GameObject>();

   public GameObject GetInstance()
   {
       if (_pooledInstanceQueue.Count>0)
       {
           GameObject instanceToReuse = _pooledInstanceQueue.Dequeue();
           instanceToReuse.SetActive(true);
           return instanceToReuse;
       }

       return Instantiate(_prefab);
   }

   public void ReturnInstance(GameObject gameObjectToPool)
   {
       _pooledInstanceQueue.Enqueue(gameObjectToPool);
       gameObjectToPool.SetActive(false);
       gameObjectToPool.transform.SetParent(gameObject.transform);
   }
}

腳本解析:

_prefab:我們要加載和實例化的資源對象


_pooledInstanceQueue :對象池存儲的實質(zhì),利用隊列思想來存取物體
GetInstance():得到對象函數(shù),內(nèi)部判斷當前隊列數(shù)量是否為0(是否空池),如果空池則創(chuàng)建資源,否則從池子中取得對象返回。取的對象后,對象池不會在對該對象處理,因此是移除了隊列。
ReturnInstance():返回對象函數(shù),對象池有進有出,當外部功能用完資源后,通過該函數(shù)重新讓資源入池。這里處理了讓對象重新進入隊列,同時關(guān)閉物體激活和設置父物體。


然后我們做一個測試腳本:

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

public class TestScript : MonoBehaviour {

   public GameObject Tran_Content;

   public ObjectsPool mPool;

   private List<GameObject> itemList = new List<GameObject>();

   public void OnEnable()
   {
       for (int i = 0; i < 8; i++)
       {
           var obj = mPool.GetInstance();
           obj.transform.SetParent(Tran_Content.transform);
           itemList.Add(obj);
       }
   }


   public void OnDisable()
   {
       for (int i = 0; i < itemList.Count; i++)
       {
           mPool.ReturnInstance(itemList[i]);
       }
   }
}


這個測試腳本有一個問題,以下的內(nèi)容會看到。


當前效果:



【一些基礎規(guī)范】

做完基礎原型后,先普及一些概念。很多知識點都來自infoq.com/cn/news/2015/,這里我提取一些方便大家理解。

兩種基本的對象池回收模式

“借用(borrowing)”和引用計數(shù)。前者更清晰,而后者則意味著要實現(xiàn)自動回收。
借用模式:和我們剛才做的UI功能相似,將對象從對象池中借用出來,對象不在和對象池有任何關(guān)系,之后由消費者返回對象池。借用和返回都由消費者來實現(xiàn)。
引用計數(shù)模式:引用計數(shù)用于同時有多個消費者訪問已分配對象的情況,只有當所有的消費者都釋放了對象引用時,對象才可以被回收。這個模式可以用Unity的內(nèi)存池舉例,Unity內(nèi)存存放了游戲資源,但是有部分資源如果沒有被當前游戲的功能塊引用,則會在某段時間自動清理掉該部分內(nèi)存。而判斷是否被引用的方法是通過給每一個內(nèi)存資源加一個引用計數(shù),當沒有對象用到該資源時(計數(shù)為0)即開始釋放資源, Unity中的 Resources.UnloadUnusedAssets()接口可以主動調(diào)用釋放無用的資源。

分配觸發(fā)方式:

空池觸發(fā):任何時候,只要池空了,就分配對象。這是一種最簡單的方式。
水位線:空池觸發(fā)的缺點是,某次對象請求會因為執(zhí)行對象分配而中斷。為了避免這種情況,可以使用水位線觸發(fā)。當從池中請求新對象時,檢查池中可用對象的數(shù)量。如果可用對象小于某個閾值,就觸發(fā)分配過程。
Lease/Return速度:大多數(shù)時候,水位線觸發(fā)已經(jīng)足夠,但有時候可能會需要更高的精度。在這種情況下,可以使用lease和return速度。例如,如果池中有100個對象,每秒有20個對象被取走,但只有10個對象返回,那么9秒后池就空了。開發(fā)者可以使用這種信息,提前做好對象分配計劃。

常常有游戲在加載進度條時,給對象池注入水位線,比如提前存入10個模型的資源,在加載進入戰(zhàn)斗的時候就可以流暢的全部加載出來。這種觸發(fā)方式就像緩存作用,可以把游戲中用到的資源提前緩存準備好,避免游戲運行中動態(tài)加載。

避免問題的規(guī)范:

引用混亂:對象在系統(tǒng)中某個地方注冊了,但沒有返回到池中。
過早回收:消費者已經(jīng)決定將對象返還給對象池,但仍然持有它的引用,并試圖執(zhí)行寫或讀操作,這時會出現(xiàn)這種情況。
隱式回收:當使用引用計數(shù)時可能會出現(xiàn)這種情況。
大小錯誤:這種情況在使用字節(jié)緩沖區(qū)和數(shù)組時非常常見:對象應該有不同的大小,而且是以定制的方式構(gòu)造,但返回對象池后卻作為通用對象重用。
重復下單:這是引用泄露的一個變種,存在多路復用時特別容易發(fā)生:一個對象被分配到多個地方,但其中一個地方釋放了該對象。
就地修改:對象不可變是最好的,但如果不具備那樣做的條件,就可能在讀取對象內(nèi)容時遇到內(nèi)容被修改的問題。
縮小對象池:當池中有大量的未使用對象時,要縮小對象池。
對象重新初始化:確保每次從池中取得的對象不含有上次使用時留下的臟字段。

我們剛才的UI測試腳本就犯了回收的錯誤。 我們將對象池的對象臨時存儲在了itemList 中,方便界面關(guān)閉的時候?qū)ο蠡厥铡5俏覀兓厥胀戤厡ο蠛蟛]有將itemList 清理。這樣就會造成對象池中的對象在外部仍然能夠獲取到,這樣沒法安全的清理對象池回收內(nèi)存,同時如果我們加入界面數(shù)據(jù)后,對象的不正確存儲會造成功能問題。


【進階功能】

剛才我們所做的對象池只能存儲一種對象,現(xiàn)在我們要擴展功能,讓對象池能存儲多種對象。

思路:

將Queue<GameObject>轉(zhuǎn)換成Dictionary<string, Queue<GameObject>>處理做為存儲對象功能,同時我們需要讓對象池能識別不同的對象,因此加入Dictionary<GameObject, string>類型的變量存儲物體的Tag。


代碼如下:



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

public class NewObjectPool : MonoBehaviour {

   private GameObject CachePanel;

   private Dictionary<string, Queue<GameObject>> m_Pool = new Dictionary<string, Queue<GameObject>>();

   private Dictionary<GameObject, string> m_GoTag = new Dictionary<GameObject, string>();


   /// <summary>
   /// 清空緩存池,釋放所有引用
   /// </summary>
   public void ClearCachePool()
   {
       m_Pool.Clear();
       m_GoTag.Clear();
   }

   /// <summary>
   /// 回收GameObject
   /// </summary>
   public void ReturnCacheGameObejct(GameObject go)
   {
       if (CachePanel == null)
       {
           CachePanel = new GameObject();
           CachePanel.name = "CachePanel";
           GameObject.DontDestroyOnLoad(CachePanel);
       }

       if (go == null)
       {
           return;
       }

       go.transform.parent = CachePanel.transform;
       go.SetActive(false);

       if (m_GoTag.ContainsKey(go))
       {
           string tag = m_GoTag[go];
           RemoveOutMark(go);

           if (!m_Pool.ContainsKey(tag))
           {
               m_Pool[tag] = new Queue<GameObject>();
           }

           m_Pool[tag].Enqueue(go);
       }
   }

   /// <summary>
   /// 請求GameObject
   /// </summary>
   public GameObject RequestCacheGameObejct(GameObject prefab)
   {
       string tag = prefab.GetInstanceID().ToString();
       GameObject go = GetFromPool(tag);
       if (go == null)
       {
           go = GameObject.Instantiate<GameObject>(prefab);
           go.name = prefab.name + Time.time;
       }


       MarkAsOut(go, tag);
       return go;
   }


   private GameObject GetFromPool(string tag)
   {
       if (m_Pool.ContainsKey(tag) && m_Pool[tag].Count > 0)
       {
           GameObject obj = m_Pool[tag].Dequeue();
           obj.SetActive(true);
           return obj;
       }
       else
       {
           return null;
       }
   }


   private void MarkAsOut(GameObject go, string tag)
   {
       m_GoTag.Add(go, tag);
   }

   private void RemoveOutMark(GameObject go)
   {
       if (m_GoTag.ContainsKey(go))
       {
           m_GoTag.Remove(go);
       }
       else
       {
           Debug.LogError("remove out mark error, gameObject has not been marked");
       }
   }

}

腳本解析:

m_GoTag :相比第一版對象池我們增加了這個變量,這里利用對象的InstanceID是唯一的,讓InstanceID作為標記。
RequestCacheGameObejct(GameObject prefab):這里增加傳入prefab,因為對象池需要能存儲多個對象,對象池通過外部傳入的資源對象來判斷。函數(shù)內(nèi)部增加對取出的物體標記的功能。
MarkAsOut(GameObject go, string tag)和RemoveOutMark(GameObject go):將取出的資源添加標記,避免返回的資源不是從對象池創(chuàng)建的資源,返回對象池后資源去除標記。
ReturnCacheGameObejct(GameObject go):增加判定返回對象的功能。


【總結(jié)】


對象池算是游戲優(yōu)化必定會用到的設計模式,網(wǎng)上對對象池有很多的資源,但是針對游戲行業(yè)的比較少,要么太過復雜,要么太過偏門。其實總的來說對象池并不難,是一個花一點時間就能掌握的技巧。當你使用對象池來做功能優(yōu)化的時候,你會開始逐漸脫離引擎寫代碼,這是程序進階的必經(jīng)之路。


對游戲開發(fā)感興趣的同學,歡迎圍觀我們:【皮皮關(guān)游戲開發(fā)教育】 ,會定期更新各種教程干貨,當然,更有別具一格的線下小班教育。

我們的官網(wǎng)地址:levelpp.com/

我們的游戲開發(fā)技術(shù)交流群:610475807

我們的微信公眾號:皮皮關(guān)


【Unity】工具類系列教程——對象池!的評論 (共 條)

分享到微博請遵守國家法律
沈丘县| 镇雄县| 宝清县| 聂荣县| 天津市| 张家川| 贵南县| 颍上县| 石渠县| 寿阳县| 沅江市| 洪雅县| 留坝县| 大名县| 汪清县| 依兰县| 富阳市| 义马市| 清流县| 固阳县| 苏尼特右旗| 嘉义市| 台江县| 黔江区| 花莲市| 宁津县| 普洱| 富阳市| 汝州市| 环江| 舟山市| 丽江市| 七台河市| 天长市| 岚皋县| 穆棱市| 孝义市| 永新县| 茶陵县| 车致| 五河县|