首页 > 其他分享 >Unity写个多用对象池

Unity写个多用对象池

时间:2024-07-02 18:10:38浏览次数:1  
标签:写个 多用 name 对象 GameObject void WaitForSeconds Unity public

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> : 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.总结

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

标签:写个,多用,name,对象,GameObject,void,WaitForSeconds,Unity,public
From: https://www.cnblogs.com/OrphanRootNode/p/18259275

相关文章

  • 【技术教程】如何写个小程序,白嫖微软语音合成
    我前几个月写了一款配音小程序,这里面我对接了5个大厂的语音合成接口,具体也不说了,里边最让我头疼的就是微软,因为一开始考虑到它是花美金的,与其他接口对比,比较贵,所以我就研究了一下,通过抓官网接口,确实能实现白嫖。当然现在我的极客配音小程序也接入了官方正版接口,为什么接正版后......
  • Unity 导航路线生成,小地图同步映射, 经过以后地图与小地图删除点位(点击小地图控制导航
    效果:(如下图所示)操作方法:搭建小地图UI截取图片创建地面挂载如下代码:usingSystem.Collections.Generic;usingUnityEngine;usingUnityEngine.UI;[RequireComponent(typeof(MeshFilter),typeof(MeshCollider),typeof(MeshRenderer))]publicclassMap:Mo......
  • 写个时钟(进阶篇)
    在“写个时钟(行为篇)”中,我们通过 JavaScript 动态创建的时针、分针和秒针,并直接在 JavaScript 中通过控制行内样式的 transform属性,设置 rotate 的值,实现指针的旋转。这样的方式,对于 DOM 的控制消耗较大,若放到大项目中,对项目性能具有一定的影响,也让 JavaScript 代......
  • 033基于SSM+Jsp的多用户博客个人网站
    开发语言:Java框架:ssm技术:JSPJDK版本:JDK1.8服务器:tomcat7数据库:mysql5.7(一定要5.7版本)数据库工具:Navicat11开发软件:eclipse/myeclipse/ideaMaven包:Maven3.3.9系统展示管理员登录管理员功能界面博文类型管理界面管理员管理我的收藏留言板管理客服聊天管理学生功......
  • 基于CDMA的多用户水下无线光通信(2)——系统模型和基于子空间的延时估计
      本文首先介绍了基于CDMA的多用户UOWC系统模型,并给出了多用户收发信号的数学模型。然后介绍基于子空间的延时估计算法,该算法只需要已知所有用户的扩频码,然后根据扩频波形的循环移位在观测空间的信号子空间上的投影进行延时估计。1、基于CDMA的多用户UOWC系统模型  首......
  • 【Unity&&C#】委托与事件笔记
    委托委托的定义委托是个类,分为Delegate自定义委托类型,Func有返回值的委托类型,Action无返回值的委托类型Func和Action的底层原理便是用Delegate声明一个委托类型(有返回值和无返回值),并且通过泛型参数(最多十六个)来实现自定义参数类型和参数委托的两种使用情况:模板方法(一般是......
  • unity麦扣x唐老狮3DRPG笔记分享,ProBuilder插件基本介绍(持续更新)
    声明:本文仅用于个人笔记及学习交流,禁止用作任何商业用途唐老师没有讲过这些插件,所以现在还没轮结合到唐老狮的课程的阶段在具体写代码以及介绍unity本体功能的时候唐老师的课程知识点会融入进来另外该插件功能过多,而用的较少所以很多功能就只做介绍,知道大概即可  首......
  • Unity 小游戏转换(一)—— WebGL+XLua导出
    转载或者引用本文内容请注明来源及原作者一、前言小游戏的红海赛道,给游戏市场带来了新的活力。小游戏依托微信、抖音等第三方平台,因为买量成本较低、开箱既玩的特性,使得许多开发厂商开始布局小游戏平台。同时Unity引擎也花费了大量的精力(团结引擎),慢慢更改开发者对于Unity庞大......
  • Unity贪吃蛇改编【详细版】
    Bigandsmallgreedysnakes游戏概述游戏亮点通过对称的美感,设置两条贪吃蛇吧,其中一条加倍成长以及加倍减少,另一条正常成长以及减少,最终实现两条蛇对整个界面的霸占效果。过程中不断记录两条蛇的得分情况,以及吃到毒药的记录,所谓一朝被蛇咬,十年怕井绳。游戏运行的硬件环......
  • 【Unity服务器01】之AssetBundle上传加载u3d模型
    首先打开一个项目导入一个简单的场景导入怪物资源,AssetBundle知识点:1.指定资源的AssetBundle属性标签  (1)找到AssetBundle属性标签(2)A标签代表:资源目录(决定打包之后在哪个文件夹里面)     B标签代表:后缀  (3)设置AB标签      AssetBundle的......