前言
这篇其实也是对于上一篇的对象池的修改强化,顺便解决可能需要做这个优化的朋友们的困扰。
先说说我的优化思路:
结合飘字尤其是伤害飘字,玩过泰拉瑞亚都应该知道一些高攻速的物体经常会出一大堆数字(当然tr的这个飘字其实是优化的很好的),这种就满足对象池的要求频繁创建和销毁对象。然后假设有一把逆天武器攻速9999,打不到几秒估计你除了数字什么都看不见而且帧数也是肉眼可见的FPS--,所以我们需要限制将飘字的最大数量,当达到最大上限又需要新的飘字的时候我们就需要把旧的飘字改为新的飘字来使用。
代码段
一共分为4个类,其实只需要1个就够了,我这样写是为了让原有的对象池可以管理正常对象无上限的对象,也有扩展的自动回收和回收的扩展。代码如下:
对象池基类ObjectPools
public class ObjectPools : MonoBehaviour
{
[Header("对象池所管理对象")] public PoolGameObject PoolOject;
[Header("池组单位数量")] public int PoolCount;
[Header("清理时间间隙")] public float ClearTime;
[Header("对象池容器")] protected ConcurrentQueue<PoolGameObject> Pools = new();
private void Awake()
{
InvokeRepeating(nameof(DestroyPools), ClearTime, ClearTime);
}
/// <summary>
/// 从对象池新建新的对象
/// </summary>
public virtual void CreatePools()
{
for (int i = 0; i < PoolCount; i++)
BackPools(Instantiate(PoolOject, transform));
}
/// <summary>
/// 从对象池中取出对象
/// </summary>
/// <returns>取出的对象</returns>
public virtual PoolGameObject OutPools()
{
if(Pools.IsEmpty)
CreatePools();
Pools.TryDequeue(out PoolGameObject Pool);
Pool.gameObject.SetActive(true);
return Pool;
}
/// <summary>
/// 将对象放回对象池中
/// </summary>
/// <param name="Pool">回收的对象</param>
public virtual void BackPools(PoolGameObject Pool)
{
Pool.Current_DestroyTime = Pool.DestroyTime;
Pool.transform.SetParent(transform);
Pool.gameObject.SetActive(false);
Pools.Enqueue(Pool);
}
/// <summary>
/// 对闲置的资源进行销毁,释放资源
/// </summary>
public virtual void DestroyPools()
{
ConcurrentQueue<PoolGameObject> queue = new ();
while (!Pools.IsEmpty)
{
Pools.TryDequeue(out PoolGameObject pool);
pool.Current_DestroyTime -= ClearTime;
if (pool.Current_DestroyTime > 0)
queue.Enqueue(pool);
else
Destroy(pool.gameObject);
}
Pools = queue;
}
/// <summary>
/// 自动归池的特殊类型返回,如果不属于Aup_PoolGameObject子类只会返回null
/// 不在OutPools合并也是为了预留不需要自动返回的情况,同时这一样也能少写一点(
/// </summary>
/// <returns></returns>
public Aup_PoolGameObject OutAupPools()
{
Aup_PoolGameObject Auptmp = OutPools().GetComponent<Aup_PoolGameObject>();
if (!Auptmp)
return null;
Auptmp.ObjectPool = this;
Auptmp.Set_Auto();
return Auptmp;
}
}
限制数量型对象池类Limit_ObjectPools
public class Limit_ObjectPools : ObjectPools
{
[Header("被取出的元素")] protected LinkedList<PoolGameObject> UsePools = new();
[Header("最大数量")] public int Max_PoolObjectNum;
/// <summary>
/// 在原有的生成基础上会额外判断是否超出了数量上限,保证对象的总体数量在一定范围内
/// </summary>
public override void CreatePools()
{
for (int i = 0; i < PoolCount && Pools.Count + UsePools.Count < Max_PoolObjectNum; i++)
BackPools(Instantiate(PoolOject, transform));
}
/// <summary>
/// 检查是否有可用的对象,如果没有则看看数量是否达到上限,没有则新建新的对象
/// 如果达到了上限会把最早分出去的对象返回
/// </summary>
/// <returns></returns>
public override PoolGameObject OutPools()
{
PoolGameObject outpool;
if (!Pools.IsEmpty)
{
Pools.TryDequeue(out outpool);
}
else if (UsePools.Count < Max_PoolObjectNum)
{
outpool = base.OutPools();
}
else
{
outpool = UsePools.First.Value;
UsePools.RemoveFirst();
}
outpool.gameObject.SetActive(true);
UsePools.AddLast(outpool);
return outpool;
}
/// <summary>
/// 当对象返回对象池时,要从UsePools中删除,避免重复
/// </summary>
/// <param name="Pool">回归对象池的对象</param>
public override void BackPools(PoolGameObject Pool)
{
UsePools.Remove(Pool);
base.BackPools(Pool);
}
}
对象类型基类PoolGameObject
public class PoolGameObject : MonoBehaviour
{
[Header("闲置销毁时间")] public float DestroyTime;
[Header("剩余时间")] public float Current_DestroyTime;
}
自动回收对象类型类Aup_PoolGameObject
public class Aup_PoolGameObject : PoolGameObject
{
[Header("回归的对象池")] public ObjectPools ObjectPool;
[Header("自动回归时间")] public float Auto_Time;
[Header("协程状态")] public Coroutine CoutDown;
/// <summary>
/// 在设置时调用这个方法会自动设置这个对象,然后自动进行等待一定时间后归池
/// 我的想法是对象池会在出池的时候就去调用这个方法,让对象自动归池
/// 不再Start方法一开始就调用是考虑可能需要有的时候不需要自动归池的情况
/// 或者是它的父类很多要自动归池但是它是个例外不需要的情况
/// </summary>
public void Set_Auto()
{
if(CoutDown != null)
StopAllCoroutines();
CoutDown = StartCoroutine(Auto_BackPools());
}
/// <summary>
/// 自动回归对象池,在需要时调用
/// </summary>
/// <returns></returns>
public IEnumerator Auto_BackPools()
{
yield return new WaitForSeconds(Auto_Time);
ObjectPool.BackPools(this);
}
}
看不懂的朋友没关系,如有有需要你可以直接先拷贝来用,在你需要进行对应方式管理的对象继承对应的对象类型类,然后需要用的挂载对应管理这个预制体的对象池,等到以后学会了就能够容易的理解了,并且这个这个池其实是还能继续优化加强的。
思考过程
对象类型
写这这个功能系统的时候思考了很多,最开始的时候我是尝试在单例模式的设计下来管理所有不同类型的对象,但是因为种种问题和限制最终决定放弃了,因为单例模式的设计特点不适合进行多种类型的管理,还会因为同类型不同预制体的类型产生获取冲突,上篇我也提到了单例模式的设计问题,这下算是亲自实验了。
使用普通的每个对象池对象管理对应的类型,本质上可以使用GameObject类型就可以,但是因为考虑到对象池中对象在某些时刻不怎么使用的情况下,无意义的保留这些可能很长时间不用的对象会造成巨大的内存占用,所以就需要有个方法定时检查一直没怎么用的对象然后即使销毁清理,普通的对象中我们没有它的时间参数就需要人为的添加时间参数变量也就是PoolGameObject类型,然后需要进行销毁的进一步编写子类Aup_PoolGameObject,然后内部实现其自动归池的方法,当我们需要某些对象用对象池管理时,只需要它挂载的脚本直接间接继承自PoolGameObject类型即可实现。
对象池类型
接下来才是难点,由于需要进行定时清理的方法,我们就需要一个方法可以不断的每隔一段时间就调用自身一次,协程递归并不是个好主意,因为这可能会导致递归太深出错,而Untiy其实有一个自带办法实现这种功能,InvokeRepeating(),它传入三个参数:方法名(string),第一次开始调用的时间,每次重复的时间间隙。接下来就是基于对象池重复部分我就不详细解说了,需要注意的一点就是每次对象加入/返回到池中一定要更新状态时间。在原有的ObjectPools中我增加了两个方法,一个是DestroyPools(),一个是OutAupPools()方法,OutAupPools()放后面解释,DestroyPools()也就是被InvokeRepeating()调用定时去清理闲置的资源,当经过很多个时间段后如果这些资源都没有被使用,即Current_DestroyTime > 0就销毁这些闲置的对象,拿出所有在容器中的对象,满足条件的放入容器中遍历销毁,剩下的则放置在临时容器中最后放回主容器,在放回方法中添加了一个Pool.Current_DestroyTime = Pool.DestroyTime;用来更新状态时间。
而限制数量的对象池,我们需要在每次新增对象时,计算对象池所有的对象的数量,由于ConcurrentQueue(你使用Queue容器也可以,Concurrent容器取出元素的特点在于如果没有取出那么变量为设置为null并且返回false,否则为true,这个容器主要在于线程安全和并且存取更快,并且可以通过IsEmpty快速判断是否为空)容器在将元素取出后会导致容器内元素数量减少,所以我们需要在额外一个容器保存被拿出的元素,然后最困难的点在于取出元素需要的判断,如果对象池容器(Pools)中还有元素,在取出容器中(UsePools)记录后就把这个元素给它(为什么不判断超出数量上限,看之后的代码),如果为空,我们就需要判断总体的数量是否超出了上限,因为主容器为空了就不需要计算了,如果没有超出就创建新的元素并且循环过程中也要计算放置超出上限,如果上述情况都不符合,就说明我们需要把最先取出的元素给作为新的元素返回了,这也就是为什么我选择LinkedList容器的原因,LinkedList容器在和Queue容器一样按照顺序排列,但是它提供了简单的方法Frist.Value获得第一个元素,RemoveFirst()移除第一个元素,AddLast()新增元素在最后的位置,而且最重要的在于它可以使用Remove()移除指定元素而Queue需要遍历并且用临时容器。放回元素则很简单,不过多赘述。
定时清理方法,这个的实现非常简单,我们只需要用循环的方式取出容器元素,如果容器元素不符合销毁条件就放到新容器,如果满足就放入列表,然后遍历列表销毁处理,临时容器赋值给就容器。
额外
代码中给出的是协程的方式判断归池操作,当这个对象被再次调用出来就会终止当前对象中所有运行的协程操作然后重新开始新的协程操作,实现对象的自动归池处理除了使用协程以外还可以使用异步线程的方式,这种方式不会注射主线程而且对于优化很重要,但是UnityAPI是不能进行多线程操作的,那怕你只是移动某个物体的位置,在多线程中你通常进行那些可能会消耗很多时间的计算操作,当然Unity也有属于自己的线程安全类来多线程控制API,虽然我还不会就是了......
这是使用异步编程的方式进行定时回池,(相比之下就优化了很多):
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
/// <summary>
/// 自动回收的类型
/// </summary>
public class Aup_PoolGameObject : PoolGameObject
{
[Header("回归的对象池")] public ObjectPools ObjectPool;
[Header("自动回归时间")] public float Auto_Time;
[Header("记录Task对象")] protected Task CurrentTask;
/// <summary>
/// 异步线程的方式进行延迟等待并实现再次调用的重置回池时间
/// <summary>
public async void Set_Auto()
{
var RuningTask = CurrentTask = Task.Delay((int)(Auto_Time * 1000));
await CurrentTask;
if (CurrentTask == RuningTask)
ObjectPool.BackPools(this);
}
}
实现效果图
我直接放出限制池存放自动归池对象的效果,因为只有这两个的基类满足了才能实现,主要体现在定时回收和自动销毁。