學(xué)習(xí)目標(biāo):
? 大家都知道在一些游戲中常常要創(chuàng)建大量的游戲?qū)ο?,如果這些對象長期占用一些內(nèi)存而沒有觸發(fā)垃圾回收機(jī)制(以下簡稱GC)或者過于頻繁的觸發(fā)GC就會導(dǎo)致游戲的幀數(shù)暴跌,在移動設(shè)備直接造成卡死的現(xiàn)象,那引用對象池的概念,能讓這些游戲?qū)ο笤趧傞_始的時(shí)候就被初始實(shí)例化而不會在游戲中頻繁生成也不用觸發(fā)垃圾回收機(jī)制,相當(dāng)于對性能極大的提升,這些都是Unity非常經(jīng)典的模式,那么在Unity2021.2以后的版本Unity終于自己創(chuàng)了一個(gè)新的命名空間UnityEngine.Pool不用玩家再自己造輪子了,下面跟著B站一位大佬Up學(xué)習(xí)了如何引用該命名空間,
這里貼個(gè)連接:
【Unity 2021】對象池API | 對象池模式 | Unity提高教程 | Object Pool_嗶哩嗶哩_bilibili
Github:GitHub - https://github.com/AtCloudStudio/UnityObjectPoolTutorial
官方的API:Unity - Scripting API: ObjectPool<T0>?
學(xué)習(xí)內(nèi)容:
?進(jìn)入官網(wǎng)的API可以看到這個(gè)命名空間包含了多個(gè)數(shù)據(jù)結(jié)構(gòu)的對象池,
這里就用最常用的ObjectPool<T>吧,
然后選擇一個(gè)2021.2之后的版本,我用的是長期支持的LTS3.8
創(chuàng)建一個(gè)項(xiàng)目進(jìn)入后然后把素材導(dǎo)進(jìn)來,如果你想自己動手做的話Scripts里面的就別放進(jìn)來了,然后把無法識別的腳本的組件都刪了,因?yàn)槲覀円獎邮指罄凶鲆槐椋?/p>
?我們先創(chuàng)建一個(gè)名字叫Gem的腳本,然后掛載到Gem Base的父預(yù)制體,這樣其它的寶石預(yù)制體都會掛載他。
創(chuàng)建一個(gè)委托以及一個(gè)計(jì)時(shí)器要來記錄什么時(shí)候執(zhí)行委托,當(dāng)碰到標(biāo)簽為Floor的地板后就觸發(fā)開始計(jì)時(shí)器的bool
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Gem : MonoBehaviour
{
[SerializeField] private float lifeAfterLanding = 2f; //著陸后過了兩秒就自動消除
private float deactiveTimer;
private bool hasLanded;
System.Action<Gem> deactiveAcion;
void Update()
{
if (!hasLanded)
return;
deactiveTimer += Time.deltaTime;
if(deactiveTimer >= lifeAfterLanding)
{
deactiveTimer = 0;
deactiveAcion.Invoke(this); // 執(zhí)行這個(gè)委托
}
}
private void OnTriggerEnter(Collider other)
{
if (other.gameObject.CompareTag("Floor"))
{
hasLanded = true;
}
}
public void SetDeactiveAction(System.Action<Gem> deactiveAcion)
{
this.deactiveAcion = deactiveAcion;
}
}
再創(chuàng)建一個(gè)叫GemSpawnNormalVersion的腳本
這個(gè)用來控制生成寶石的。別忘了在生成寶石的函數(shù)最后訂閱這個(gè)委托并給它要執(zhí)行的事件
using UnityEngine;
public class GemSpawnNormalVersion : MonoBehaviour
{
[SerializeField] private Gem[] gemPrefabs;
[SerializeField] private int spawnAmounts = 50;
[SerializeField] private float gemSpawnInterval;
private float gemSpawnTimer;
private void Update()
{
gemSpawnTimer += Time.deltaTime;
if(gemSpawnTimer >= gemSpawnInterval)
{
gemSpawnTimer = 0;
Spawn();
}
}
private void Spawn()
{
for (int i = 0; i < spawnAmounts; i++)
{
var randomIndex = Random.Range(0, gemPrefabs.Length);
var prefab = gemPrefabs[randomIndex]; //隨機(jī)生成某種類型的寶石
var gem = Instantiate(prefab, transform); //把這個(gè)寶石生成器對象作為生成寶石的腳本
gem.transform.position = transform.position + Random.insideUnitSphere * 2;
gem.SetDeactiveAction(delegate { Destroy(gameObject);});
}
}
}
再窗口配置好后,
?
?這時(shí)候運(yùn)行游戲,沒問題12鐘寶石隨機(jī)生成,在地面過了兩秒后觸發(fā)垃圾回收機(jī)制GC,但你的電腦有沒有紅溫呢,反正我筆記本電腦溫度高的一批,我就不截圖了怕我電腦燒了。
?這時(shí)候就到了使用對象池的時(shí)間了
?我們創(chuàng)建一個(gè)GemPool的腳本掛載上去。
請結(jié)合代碼看我的解釋,我們引用新命名空間,using UnityEngine.Pool;
然后創(chuàng)建一個(gè)ObjectPool<T>泛型T里面是Gem類,然后再Awake函數(shù)初始化,這里需要幾個(gè)委托函數(shù),第一個(gè)用于先創(chuàng)建也就是該類型的游戲?qū)ο筮@里我們是Gem類的要先實(shí)例化!,然后返回這個(gè)游戲?qū)ο?,第二個(gè)是用于當(dāng)我們調(diào)用對象池的Get函數(shù)索要執(zhí)行的功能也就是啟用這個(gè)對象,第三個(gè)則是調(diào)用Release()也就是返回這個(gè)對象池要執(zhí)行的功能,第四個(gè)則是當(dāng)對象池尺寸不足以容納這么多游戲?qū)ο蟮臅r(shí)候就會銷毀無法返回對象池的游戲?qū)ο螅谖鍌€(gè)則是一個(gè)bool的,用來自動檢測對象池是否超尺寸,由于這個(gè)對象池本質(zhì)是一個(gè)棧的數(shù)據(jù)結(jié)構(gòu),所以當(dāng)尺寸小于實(shí)際產(chǎn)生的游戲?qū)ο螅蜁筛嗟挠螒驅(qū)ο髞頂U(kuò)大尺寸(但會產(chǎn)生GC),第六個(gè)則是對象池的默認(rèn)尺寸,第七個(gè)是對象池能容忍的最大尺寸。
?
using UnityEngine;
using UnityEngine.Pool;
public class GemPool : MonoBehaviour
{
[SerializeField] private Gem prefab;
[SerializeField] private int minCapcitySize =10;//這兩個(gè)變量用來定義對象池也就是棧的存儲空間
[SerializeField] private int maxCapcitySize = 100;
[SerializeField] private int activeCount => pool.CountActive;
[SerializeField] private int inacitveCount => pool.CountInactive;
[SerializeField] private int totalCount => pool.CountAll;
ObjectPool<Gem> pool;
//對象池僅僅發(fā)生激活和非激活狀態(tài)之間的切換,只有調(diào)用ObjectPool.Clear()或者Dispose()才會清除對象池中的元素
private void Awake()
{
pool = new ObjectPool<Gem>(OnCreatePoolItem, OnGetPoolItem, OnReleasePoolItem, OnDestoryPoolItem, true, minCapcitySize, maxCapcitySize);
}
private void Update()
{
var gem = pool.Get();
gem.transform.position = transform.position + Random.insideUnitSphere * 2;
}
private void OnDestoryPoolItem(Gem obj)
{
Destroy(obj.gameObject);
}
private void OnReleasePoolItem(Gem obj)
{
obj.gameObject.SetActive(false);
}
private void OnGetPoolItem(Gem obj)
{
obj.gameObject.SetActive(true);
}
private Gem OnCreatePoolItem()
{
var gem = Instantiate(prefab, transform);
gem.SetDeactiveAction(delegate { pool.Release(gem); }); //在實(shí)例化寶石后再調(diào)用release函數(shù)回收這個(gè)寶石
return gem;
}
}
別忘了調(diào)用委托SetDeactiveAction先讓實(shí)例化的對象返回對象池中。
?
這里隨機(jī)選擇一個(gè)預(yù)制體,運(yùn)行游戲可以看到有一部分就在禁用狀態(tài)。
那么我們怎么推廣所有寶石預(yù)制體呢?很簡單,只需要用數(shù)組給Gem類加個(gè)數(shù)組用foreach依次生成即可,那對于游戲中所有要用到的預(yù)制體呢,他們可沒有用Gem這個(gè)類。
這樣我們要造新輪子寫個(gè)基類讓所有要用到對象池的游戲?qū)ο罄^承使用即可,
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Pool;
public class BasePool<T> : MonoBehaviour where T : Component
{
[SerializeField] protected T prefab;
[SerializeField] int minSize =100;
[SerializeField] int maxSize = 500;
public int activeCount => pool.CountActive;
public int inacitveCount => pool.CountInactive;
public int totalCount => pool.CountAll;
ObjectPool<T> pool;
public void Initialize(bool checkPoolSize = true)
{
pool = new ObjectPool<T>(OnCreatePoolItem, OnGetPoolItem, OnReleasePoolItem, OnDestoryPoolItem, checkPoolSize, minSize, maxSize);
}
protected virtual void OnDestoryPoolItem(T obj)
{
Destroy(obj.gameObject);
}
protected virtual void OnReleasePoolItem(T obj)
{
obj.gameObject.SetActive(false);
}
protected virtual void OnGetPoolItem(T obj)
{
obj.gameObject.SetActive(true);
}
protected virtual T OnCreatePoolItem() => Instantiate(prefab, transform);
public void Get() => pool.Get();
public void Release(T obj) => pool.Release(obj);
public void Clear() => pool.Clear();
}
可能你對這些Public的函數(shù)Lamada表達(dá)式后半部分的功能不太了解,其實(shí)官方API都有標(biāo)明他們的功能
這樣回到GemPool的腳本只需要繼承這個(gè)類再重寫兩個(gè)函數(shù)即可?
using UnityEngine;
using UnityEngine.Pool;
public class GemPool : BasePool<Gem>
{
private void Awake()
{
Initialize();
}
private void Update()
{
Get();
}
protected override Gem OnCreatePoolItem()
{
var gem = base.OnCreatePoolItem();
gem.SetDeactiveAction(delegate { Release(gem); }); //在實(shí)例化寶石后再調(diào)用release函數(shù)回收這個(gè)寶石
return gem;
}
protected override void OnGetPoolItem(Gem gem)
{
base.OnGetPoolItem(gem);
gem.transform.position = transform.position + Random.insideUnitSphere * 2;
}
public void SetGemPrefab(Gem prefab)
{
this.prefab = prefab;
}
}
最后的擴(kuò)展:
其實(shí)這還不算是最好的性能,當(dāng)游戲?qū)ο筮€是太多的時(shí)候,游戲幀數(shù)就會慢慢降到個(gè)位數(shù)直到卡死,接下來要介紹更好的運(yùn)用ObjectPool性能
創(chuàng)建一個(gè)新腳本叫GemSpawnNormalVersion
首先是Gem【】數(shù)組用來管理每一種寶石的生成,在Start函數(shù)中為他們每個(gè)創(chuàng)造一個(gè)父對象poolHolder,這樣方便管理各個(gè)種類的寶石,然后直到poolHolder掛載GemPool腳本并且為這個(gè)腳本上設(shè)置好它專屬的Gem類(不然為空會報(bào)錯)就可以激活它了,別忘了賦在我們的鏈表List<GemPool>上,最后在Spawn函數(shù)中,我們隨機(jī)選擇某種類型的寶石并在鏈表中取出來,并調(diào)用它GemPool腳本的Get()函數(shù)。
?
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GemSpawnPoolVersion : MonoBehaviour
{
[SerializeField] private Gem[] gemPrefabs;
[SerializeField] private int spawnAmounts =50;
[SerializeField] private float gemSpawnInterval;
private float gemSpawnTimer;
List<GemPool> gemPools = new List<GemPool>();
private void Start()
{
foreach (var gemPrefab in gemPrefabs)
{
var poolHolder = new GameObject($"Pool:{gemPrefab.name}");
poolHolder.transform.parent = transform;
poolHolder.transform.position = transform.position;
poolHolder.SetActive(false);
var pool = poolHolder.AddComponent<GemPool>();
pool.SetGemPrefab(gemPrefab);
poolHolder.SetActive(true);
gemPools.Add(pool);
}
}
private void Update()
{
gemSpawnTimer += Time.deltaTime;
if (gemSpawnTimer >= gemSpawnInterval)
{
gemSpawnTimer = 0;
Spawn();
}
}
private void Spawn()
{
for (int i = 0; i < spawnAmounts; i++)
{
var randomIndex = Random.Range(0, gemPrefabs.Length);
var pool = gemPools[randomIndex];
pool.Get();
}
}
}
?掛載后運(yùn)行,無論你的SpawnAmounts調(diào)的多離譜都會幀數(shù)穩(wěn)定很多了。(30幀左右吧)
這樣性能就能妥善解決了大性能小號穩(wěn)定文章來源:http://www.zghlxwxcb.cn/news/detail-410198.html
學(xué)習(xí)產(chǎn)出:
學(xué)習(xí)了怎么使用新命名空間UnityEngine.Pool,并且了探討了更加優(yōu)化版本的正確對象池使用。文章來源地址http://www.zghlxwxcb.cn/news/detail-410198.html
到了這里,關(guān)于[Unity命名空間教程]介紹Unity新自帶的命名空間UnityEngine.Pool的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!