我欲穿花寻路 直入白云深处

[Unity]记一个对象池模块

1.介绍

游戏开发中我们会频繁使用到预制体来优化内存,优化性能,增强游戏表现。

当要使用的预制体次数很多,创建销毁很频繁时,为了方便管理、提升性能,我们需要一个对象池。

一般使用单例+一个预制体+一个存储类型就能做出一个简单的对象池。

但当我们需要对很多种物体进行对象池管理时、当我们需要对很多类型的物体进行对象池管理时,当我们需要对对象的使用周期进行监听时、当我们需要一些简便的管理以及为协作者提供接口时…这时我们就需要仔细思考一下如何升级我们的对象池了。

2.需要实现的功能

  1. 实现多物体的对象池,提供简单方便的接口
  2. 实现多类型的对象池,提供简单方便的接口
  3. 为每种每类对象的生命周期提供监听方法的接口

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> : UnityEngine.Object, 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>
    /// <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 (!SortedPool.ContainsKey(prefab.name))
            {
                SortedPool.Add(prefab.name, new ObjPool(prefab.name, prefab));
                Debug.Log("------------添加池 " + prefab.name);
            }
            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.总结

这是一个半成品的对象池,还有许多部分与方面可以优化,但在大部分情况下够用了,时间与笔力有限,下次再与大家一起分享探讨。

posted @ 2024-07-02 18:02  根结点可不就是孤儿  阅读(25)  评论(0编辑  收藏  举报