学习目标:
大家都知道在一些游戏中常常要创建大量的游戏对象,如果这些对象长期占用一些内存而没有触发垃圾回收机制(以下简称GC)或者过于频繁的触发GC就会导致游戏的帧数暴跌,在移动设备直接造成卡死的现象,那引用对象池的概念,能让这些游戏对象在刚开始的时候就被初始实例化而不会在游戏中频繁生成也不用触发垃圾回收机制,相当于对性能极大的提升,这些都是Unity非常经典的模式,那么在Unity2021.2以后的版本Unity终于自己创了一个新的命名空间UnityEngine.Pool不用玩家再自己造轮子了,下面跟着B站一位大佬Up学习了如何引用该命名空间,
学习内容:
进入官网的API可以看到这个命名空间包含了多个数据结构的对象池,
这里就用最常用的ObjectPool<T>吧,
然后选择一个2021.2之后的版本,我用的是长期支持的LTS3.8
创建一个项目进入后然后把素材导进来,如果你想自己动手做的话Scripts里面的就别放进来了,然后把无法识别的脚本的组件都删了,因为我们要动手跟着大佬做一遍,
我们先创建一个名字叫Gem的脚本,然后挂载到Gem Base的父预制体,这样其它的宝石预制体都会挂载他。
创建一个委托以及一个计时器要来记录什么时候执行委托,当碰到标签为Floor的地板后就触发开始计时器的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); // 执行这个委托
}
}
private void OnTriggerEnter(Collider other)
{
if (other.gameObject.CompareTag("Floor"))
{
hasLanded = true;
}
}
public void SetDeactiveAction(System.Action<Gem> deactiveAcion)
{
this.deactiveAcion = deactiveAcion;
}
}
再创建一个叫GemSpawnNormalVersion的脚本
这个用来控制生成宝石的。别忘了在生成宝石的函数最后订阅这个委托并给它要执行的事件
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]; //随机生成某种类型的宝石
var gem = Instantiate(prefab, transform); //把这个宝石生成器对象作为生成宝石的脚本
gem.transform.position = transform.position + Random.insideUnitSphere * 2;
gem.SetDeactiveAction(delegate { Destroy(gameObject);});
}
}
}
再窗口配置好后,
这时候运行游戏,没问题12钟宝石随机生成,在地面过了两秒后触发垃圾回收机制GC,但你的电脑有没有红温呢,反正我笔记本电脑温度高的一批,我就不截图了怕我电脑烧了。
这时候就到了使用对象池的时间了
我们创建一个GemPool的脚本挂载上去。
请结合代码看我的解释,我们引用新命名空间,using UnityEngine.Pool;
然后创建一个ObjectPool<T>泛型T里面是Gem类,然后再Awake函数初始化,这里需要几个委托函数,第一个用于先创建也就是该类型的游戏对象这里我们是Gem类的要先实例化!,然后返回这个游戏对象,第二个是用于当我们调用对象池的Get函数索要执行的功能也就是启用这个对象,第三个则是调用Release()也就是返回这个对象池要执行的功能,第四个则是当对象池尺寸不足以容纳这么多游戏对象的时候就会销毁无法返回对象池的游戏对象,第五个则是一个bool的,用来自动检测对象池是否超尺寸,由于这个对象池本质是一个栈的数据结构,所以当尺寸小于实际产生的游戏对象,就会生成更多的游戏对象来扩大尺寸(但会产生GC),第六个则是对象池的默认尺寸,第七个是对象池能容忍的最大尺寸。
using UnityEngine;
using UnityEngine.Pool;
public class GemPool : MonoBehaviour
{
[SerializeField] private Gem prefab;
[SerializeField] private int minCapcitySize =10;//这两个变量用来定义对象池也就是栈的存储空间
[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;
//对象池仅仅发生激活和非激活状态之间的切换,只有调用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); }); //在实例化宝石后再调用release函数回收这个宝石
return gem;
}
}
别忘了调用委托SetDeactiveAction先让实例化的对象返回对象池中。
这里随机选择一个预制体,运行游戏可以看到有一部分就在禁用状态。
那么我们怎么推广所有宝石预制体呢?很简单,只需要用数组给Gem类加个数组用foreach依次生成即可,那对于游戏中所有要用到的预制体呢,他们可没有用Gem这个类。
这样我们要造新轮子写个基类让所有要用到对象池的游戏对象继承使用即可,
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的函数Lamada表达式后半部分的功能不太了解,其实官方API都有标明他们的功能
这样回到GemPool的脚本只需要继承这个类再重写两个函数即可
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); }); //在实例化宝石后再调用release函数回收这个宝石
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;
}
}
最后的扩展:
其实这还不算是最好的性能,当游戏对象还是太多的时候,游戏帧数就会慢慢降到个位数直到卡死,接下来要介绍更好的运用ObjectPool性能
创建一个新脚本叫GemSpawnNormalVersion
首先是Gem【】数组用来管理每一种宝石的生成,在Start函数中为他们每个创造一个父对象poolHolder,这样方便管理各个种类的宝石,然后直到poolHolder挂载GemPool脚本并且为这个脚本上设置好它专属的Gem类(不然为空会报错)就可以激活它了,别忘了赋在我们的链表List<GemPool>上,最后在Spawn函数中,我们随机选择某种类型的宝石并在链表中取出来,并调用它GemPool脚本的Get()函数。
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();
}
}
}
挂载后运行,无论你的SpawnAmounts调的多离谱都会帧数稳定很多了。(30帧左右吧)
这样性能就能妥善解决了大性能小号稳定
学习产出:
学习了怎么使用新命名空间UnityEngine.Pool,并且了探讨了更加优化版本的正确对象池使用。