Unity写个多用对象池
1.介绍
游戏开发中我们会频繁使用到预制体来优化内存,优化性能,增强游戏表现。
当要使用的预制体次数很多,创建销毁很频繁时,为了方便管理、提升性能,我们需要一个对象池。
一般使用单例+一个预制体+一个存储类型就能做出一个简单的对象池。
但当我们需要对很多种物体进行对象池管理时、当我们需要对很多类型的物体进行对象池管理时,当我们需要对对象的使用周期进行监听时、当我们需要一些简便的管理以及为协作者提供接口时…这时我们就需要仔细思考一下如何升级我们的对象池了。
2.需要实现的功能
- 实现多物体的对象池,提供简单方便的接口
- 实现多类型的对象池,提供简单方便的接口
- 为每种每类对象的生命周期提供监听方法的接口
3.拆解分析与实现
从实现的简单程度与基础程度,先实现基础对象池的生命周期监听,在此基础上实现多物体对象池和多类型对象池的管理
3.1 对象生命周期的监听
对象的生命周期包含对象创建时、从对象池中获取时、回收进对象池时。正常写需要将创建、获取、回收方法封装一层传入一个委托方法,Unity有提供ObjectPool<>、LinkedPool<>、DictionaryPool<>、HashPool<>等对象池并提供监听接口,我们使用ObjectPool<>作为基础的元池,逐步构建一个更大的对象池。
ObjectPool类:
点击查看代码
//unity的内置类,属性方法比较简单,可看unityAPI手册学习
public class ObjectPool<T> : IDisposable, IObjectPool<T> where T : class
{
public ObjectPool(Func<T> createFunc, Action<T> actionOnGet = null, Action<T> actionOnRelease = null, Action<T> actionOnDestroy = null, bool collectionCheck = true, int defaultCapacity = 10, int maxSize = 10000);
public int CountAll { get; }
public int CountActive { get; }
public int CountInactive { get; }
public void Clear();
public void Dispose();
public T Get();
public PooledObject<T> Get(out T v);
public void Release(T element);
}
3.2 多物体的对象池
当预制体很多时,我们可以使用Dictonary等带标志的数据结构存储、使用一个个预制体的对应对象池,我们只需要将对象与字典联系起来,以后就能使用一个字典来管理所有的预制体,并且我们可以规定使用预制体的名字作为键,这样在使用与理解上都十分友好,唯一需要注意的是,必须要有良好的预制体命名规范,不能有相同名字的不同预制体。
点击查看代码
//用于挂载所有预制体
public List<GameObject> poolPrefabs;
//管理预制体
private Dictionary<string, GameObject> sortedPrefabs = new();
void SortedPoolInit()
{
foreach (GameObject prefab in poolPrefabs)
{
if (!sortedPrefabs.ContainsKey(prefab.name))
sortedPrefabs.Add(prefab.name, prefab);
else
Debug.LogError("----------有重复的prefab名称");
}
}
//对象池Get对应对象的接口
public GameObject Get(string name)
{
}
对象池Release回收对象的接口
public void Release(string name, GameObject obj)
{
}
3.3 多类型的对象池
除开值类型,GameObject一类的,其他类型比如一些类WaitForSeconds、Animal、Bullet啥的,这个其实用到的并不多并且开销不算大,还有Mono内存堆管理,只是都写到这了补充上吧。和之前GameObject的处理类似,不过需要一个各类型与object的引用类型之间的装箱与拆箱。
点击查看代码
Dictionary<Type, object> sortedCustomClass = new();
public object GetCustom(Type t)
{
}
public ReleaseCustom(Type t,object o)
{
}
3.4 综合实现
个人习惯先从接口写起,定好规范性与约束性。对于大大小小的对象池、元池、拓展池,都需要拿取Get、回收Release接口及他们的监听OnGet、OnRelease,先照此写个IPool.
点击查看代码
public interface IPool<T> where T:class
{
/// <summary>
/// 从池中获取
/// </summary>
/// <returns></returns>
public T Get();
/// <summary>
/// 放回池中并释放
/// </summary>
public void Release(T obj);
/// <summary>
/// 获取监听
/// </summary>
public void OnGet(T obj);
/// <summary>
/// 放回监听
/// </summary>
public void OnRelease(T obj);
}
因为要实现监听,而不同池需要不同的监听,所以需要一个基础池(元池)父类,要有对应的池的名字(等同其预制体名字)、对应的预制体以及一个ObjectPool:
点击查看代码
public class PoolBase<T> : MonoBehaviour, IPool<T> where T : class
{
/// <summary>
/// 元池名
/// </summary>
public string poolName;
/// <summary>
/// 元池的预制体
/// </summary>
protected T poolPrefab;
/// <summary>
/// 元池 ObjectPool
/// </summary>
public ObjectPool<T> pool;
/// <summary>
/// 从元池中获取
/// </summary>
public virtual T Get()
{
throw new NotImplementedException();
}
/// <summary>
/// 元池拿取的监听事件
/// </summary>
/// <param name="obj">拿取之物</param>
public virtual void OnGet(T obj)
{
}
/// <summary>
/// 元池回收
/// </summary>
/// <param name="obj">回收之物</param>
public virtual void Release(T obj)
{
}
/// <summary>
/// 元池回收的监听事件
/// </summary>
/// <param name="obj">回收之物</param>
public virtual void OnRelease(T obj)
{
}
}
根据这个泛型元池,我们可以按需要写一些具体要使用的派生类了:
点击查看代码
/// <summary>
/// GameObject类型的池
/// </summary>
public class ObjPool : PoolBase<GameObject>
{
//区别于ObjectPool的池、按需使用
public HashSetPool<GameObject> hashpool;
public DictionaryPool<string, GameObject> dicpool;
public LinkedPool<GameObject> linkedpool;
//传入池名及预制体数据
public ObjPool(string name, GameObject prefab)
{
poolName = name;
pool = new ObjectPool<GameObject>(OnCreate, OnGet, OnRelease);
poolPrefab = prefab;
}
//生成监听
GameObject OnCreate()
{
GameObject GO = Instantiate(poolPrefab);
GO.SetActive(false);
return GO;
}
public override GameObject Get()
{
return pool.Get();
}
public override void Release(GameObject GO)
{
pool.Release(GO);
}
public override void OnGet(GameObject GO)
{
// 自定义监听事件
GO.SetActive(true);
}
public override void OnRelease(GameObject GO)
{
// 自定义监听事件
GO.SetActive(false);
}
}
//其他例子,但其实使用很少,也有其他的方法替代
public class MaterialPool : PoolBase<Material>
{
public MaterialPool(string name, Material prefab)
{
poolName = name;
pool = new ObjectPool<Material>(OnCreate, OnGet, OnRelease);
poolPrefab = prefab;
}
Material OnCreate()
{
Material Mt = Instantiate(poolPrefab) ;
return Mt;
}
public override Material Get()
{
return pool.Get();
}
public override void OnGet(Material mt)
{
}
public override void OnRelease(Material mt)
{
}
public override void Release(Material mt)
{
pool.Release(mt);
}
}
点击查看代码
public class CustomPool<V> : IPool<V> where V : class
{
private ObjectPool<V> _CustomPool;
//将生成监听传参,其他监听也可以套用
private Func<V> _Creator;
public CustomPool(Func<V> creator)
{
_Creator = creator ?? throw new ArgumentNullException(nameof(creator));
_CustomPool = new ObjectPool<V>(_Creator, OnGet, OnRelease);
}
public V Get()
{
V v = _CustomPool.Get();
return v;
}
public void Release(V v)
{
_CustomPool.Release(v);
}
public void OnGet(V v)
{
}
public void OnRelease(V v)
{
}
}
搞完自动化了,现在要加上手动挡实现半自动化了。什么?你问为什么不做个全自动化?代码全自动化了程序员不就失业了吗( o`ω′)ノ!好吧,只是笔者笔力不够了...
点击查看代码
//Singleton是一个泛型单例基类
public class PoolMgr : Singleton<PoolMgr>
{
#region GO对象池
/// <summary>
/// GO预制体
/// </summary>
public List<GameObject> poolPrefabs;
/// <summary>
/// GO预制体字典
/// </summary>
/// <returns></returns>
private Dictionary<string, GameObject> _SortedPrefabs = new();
/// <summary>
/// GameObject的总对象池
/// </summary>
public Dictionary<string, ObjPool> SortedPool = new();
void Awake()
{
base.Awake();
DontDestroyOnLoad(gameObject);
SortedPoolInit();
}
/// <summary>
/// 预制体分类
/// </summary>
void SortedPoolInit()
{
foreach (GameObject prefab in poolPrefabs)
{
if (!_SortedPrefabs.ContainsKey(prefab.name))
_SortedPrefabs.Add(prefab.name, prefab);
else
Debug.LogError("----------有重复的prefab名称");
}
}
/// <summary>
/// 获取name池中的一个节点
/// </summary>
public GameObject Get(string name)
{
if (!SortedPool.ContainsKey(name))
{
Debug.LogError("不存在要获取的子对象池--" + name);
return null;
}
return SortedPool[name].Get();
}
/// <summary>
/// 将obj回收进name池
/// </summary>
public void Release(string name, GameObject obj)
{
if (!SortedPool.ContainsKey(name))
{
Debug.LogWarning("不存在要回收进的对象池--" + name );
Destroy(obj);
return;
}
obj.transform.SetParent(transform);
SortedPool[name].Release(obj);
}
#endregion
#region 其他不可序列化的对象池
Dictionary<Type, CustomPool<object>> CustomPoolDic = new();
public object GetCustom(Type t, Func<object> func = null)
{
//使用默认object构建
if (!CustomPoolDic.ContainsKey(t) && func == null)
{
CustomPoolDic.Add(t, new CustomPool<object>(() => { return new object(); }));
}
//使用提供的function构建
else if (!CustomPoolDic.ContainsKey(t) && func != null)
CustomPoolDic.Add(t, new CustomPool<object>(func));
//替换原构造的custom pool 为新的构造,可能有问题,取决于两个构建委托的兼容性
else if (CustomPoolDic.ContainsKey(t) && func != null)
{
CustomPoolDic[t] = new CustomPool<object>(func);
}
return CustomPoolDic[t].Get();
}
public void ReleaseCustom(Type t, object o)
{
if (!CustomPoolDic.ContainsKey(t))
{
o = null;
}
else
CustomPoolDic[t].Release(o);
}
#endregion
}
前一类池的使用非常简单,配置好预制体和单例后只需要关注两个接口:
点击查看代码
GameObject bullet = PoolMgr.Instance.Get("Bullet");
PoolMgr.Instance.Release("Bullet",bullet);
关于后面一类池的使用以及可能出现的问题笔者以WaitForSeconds为例,大家就很好理解了
点击查看代码
//这里第一次从池中拿,wts1为新构建的WaitForSeconds(0.1f)
WaitForSeconds wts1 = (WaitForSeconds)PoolMgr.Instance.GetCustom(typeof(WaitForSeconds), () => { return new WaitForSeconds(0.1f); });
//这里放回去一个WaitForSeconds(0.1f)
PoolMgr.Instance.ReleaseCustom(typeof(WaitForSeconds), wts);
//这里想要拿的是WaitForSeconds(0.2f),但返回的是WaitForSeconds(0.1f)
WaitForSeconds wts2 = (WaitForSeconds)PoolMgr.Instance.GetCustom(typeof(WaitForSeconds), () => { return new WaitForSeconds(0.2f); });
//这里池中没有了,所以会使用新的构建拿出一个WaitForSeconds(0.3f)
WaitForSeconds wts3 = (WaitForSeconds)PoolMgr.Instance.GetCustom(typeof(WaitForSeconds), () => { return new WaitForSeconds(0.3f); });
4.总结
这是一个半成品的对象池,还有许多部分与方面可以优化,但在大部分情况下够用了,时间与笔力有限,下次再与大家一起分享探讨。