【unity进阶知识7】对象池的使用,如何封装一个对象池管理器

文章目录

什么是对象池?对象池有什么用?

频繁创建和销毁对象会造成性能的开销。

创建对象的时候,系统会为这个对象开辟一片新的空间。销毁对象的时候,这个对象会变成内存垃圾,当内存垃圾达到一定程度,就会触发垃圾回收机制(及GC),清理内存垃圾,由于此时在清理垃圾,所以程序有可能就会变卡。

为了改善这个问题,我们就可以使用对象池。使用了它之后,程序的性能就能得到提升不那么容易变卡。

对象池的原理

1、当要创建对象的时候,不直接创建,而是先从对象池里面找,如果对象池里有这种类型的对象,就从对象池中取出来用。如果对象池里面没有这种类型的对象,才创建该对象。

2、当要销毁对象的时候,不直接销毁,而是把这个对象放进对象池里面存着,以便下一次创建对象时使用。

对象池的实现

1、从对象池获取对象

从对象池获取一个对象。如果对象池有,从对象池中取出来用。如果对象池没有,则实例化该对象。

csharp 复制代码
/// <summary>
/// 对象池
/// </summary>
public class ObjectPool : MonoBehaviour
{
    // 这个对象池存储的游戏对象的预制体
    public GameObject prefab;

    // 对象池最多能容纳多少个游戏对象。负数表示可以容纳无数个。
    public int capacity = -1;

    // 从这个对象池中取出并正在使用的游戏对象。
    public List<GameObject> usedGameObjectList = new List<GameObject>();

    // 存在这个对象池中没有被使用的游戏对象。
    public List<GameObject> unUsedGameObjectList = new List<GameObject>();

    // 这个对象池中正在使用和没有被使用的游戏对象的总数
    public int TotalGameObjectCount => usedGameObjectList.Count + unUsedGameObjectList.Count;

    /// <summary>
    /// 从对象池获取一个对象。
    /// 如果对象池有,从对象池中取出来用。
    /// 如果对象池没有,则实例化该对象。
    /// </summary>
    /// <param name="position">位置</param>
    /// <param name="rotation">旋转</param>
    /// <param name="parent">父级</param>
    /// <returns>返回这个对象</returns>
    public GameObject Spawn(Vector3 position, Quaternion rotation, Transform parent = null)
    {
        GameObject go;

        // 如果对象池中有,则从对象池中取出来用。
        if (unUsedGameObjectList.Count > 0)
        {
            go = unUsedGameObjectList[0];
            unUsedGameObjectList.RemoveAt(0);
            usedGameObjectList.Add(go);
            go.transform.localPosition = position;
            go.transform.localRotation = rotation;
            go.transform.SetParent(parent, false);
            go.SetActive(true);
        }
        // 如果对象池中没有,则实例化该对象。
        else
        {
            go = Instantiate(prefab, position, rotation, parent);
            usedGameObjectList.Add(go);
        }

        // 如果该游戏对象身上继承MonoBehaviour的脚本中写了名叫OnSpawn的方法,则会执行它们一次。
        go.SendMessage("OnSpawn", SendMessageOptions.DontRequireReceiver);
        return go;
    }

    
}

2、回收对象

隐藏指定的游戏对象,并把它回收进对象池中。

csharp 复制代码
/// <summary>
/// 回收对象
/// </summary>
/// <param name="go">要回收的对象</param>
public void Despawn(GameObject go)
{
    if (go == null) return;

    // 遍历这个对象池中所有正在使用的游戏对象
    for (int i = 0; i < usedGameObjectList.Count; i++)
    {
        if (usedGameObjectList[i] == go)
        {
            // 如果这个对象池的容量不为负数,且容纳的游戏对象已经满,则把0号的游戏对象剔除掉
            if (capacity >= 0 && usedGameObjectList.Count >= capacity)
            {
                if (unUsedGameObjectList.Count > 0)
                {
                    Destroy(unUsedGameObjectList[0]);
                    unUsedGameObjectList.RemoveAt(0);
                }
            }

            // 把游戏对象回收到对象池中
            unUsedGameObjectList.Add(go);
            usedGameObjectList.RemoveAt(i);
            go.SetActive(false);
            go.transform.SetParent(transform, false);

            // 如果该游戏对象身上继承MonoBehaviour的脚本写了名叫OnDespawn的方法,则在回收的时候,会执行一次。
            go.SendMessage("OnDespawn", SendMessageOptions.DontRequireReceiver);
            return;
        }
    }
}

3、回收所有对象

把通过这个对象池生成的所有游戏对象全部隐藏并放入对象池中

csharp 复制代码
/// <summary>
/// 回收所有对象,把通过这个对象池生成的所有游戏对象全部隐藏并放入对象池中
/// </summary>
public void DespawnAll()
{
    int count = usedGameObjectList.Count;
    for (int i = 0; i < count; i++)
    {
        Despawn(usedGameObjectList[0]);
    }
}

4、预先往这个对象池中加载指定数量的游戏对象

csharp 复制代码
/// <summary>
/// 预先往这个对象池中加载指定数量的游戏对象
/// </summary>
/// <param name="amount">预先生成游戏对象数量</param>
public void Preload(int amount = 1)
{
    if (prefab == null) return;
    if (amount <= 0) return;

    for (int i = 0; i < amount; i++)
    {
        GameObject go = Instantiate(prefab, Vector3.zero, Quaternion.identity);
        go.SetActive(false);
        go.transform.SetParent(transform, false);
        unUsedGameObjectList.Add(go);
        go.name = prefab.name;
    }
}

5、最终代码

csharp 复制代码
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// 对象池
/// </summary>
public class ObjectPool : MonoBehaviour
{
    //这个对象池存储的游戏对象的预制体
    public GameObject prefab;

    //对象池最多能容纳多少个游戏对象。负数表示可以容纳无数个。
    public int capacity = -1;

    //从这个对象池中取出并正在使用的游戏对象
    public List<GameObject> usedGameObjectsList = new List<GameObject>();

    //存在这个对象池中并没有被使用的游戏对象
    public List<GameObject> unUsedGameObjectsList = new List<GameObject>();

    //这个对象池中正在使用和没有被使用的游戏对象的总数
    public int TotalGameObjectsCount { get => usedGameObjectsList.Count + unUsedGameObjectsList.Count; }

    /// <summary>
    /// <para>从对象池获取一个对象</para>
    /// <para>如果对象池中有,则从对象池中取出来用。</para>
    /// <para>如果对象池中没有,则实例化该对象。</para>
    /// </summary>
    /// <param name="position">生成游戏对象的位置</param>
    /// <param name="rotation">生成游戏对象的旋转</param>
    /// <param name="parent">生成的游戏对象的父物体</param>
    /// <returns>返回这个对象</returns>
    public GameObject Spawn(Vector3 position, Quaternion rotation, Transform parent = null)
    {
        GameObject go;

        if (unUsedGameObjectsList.Count > 0)
        {
            go = unUsedGameObjectsList[0];

            unUsedGameObjectsList.RemoveAt(0);

            usedGameObjectsList.Add(go);

            go.transform.localPosition = position;

            go.transform.localRotation = rotation;

            go.transform.SetParent(parent, false);

            go.SetActive(true);
        }
        else
        {
            go = Instantiate(prefab, position, rotation, parent);

            usedGameObjectsList.Add(go);
        }

        //如果该游戏对象身上继承Monobehavior的脚本中写了名叫OnSpawn的方法,则会执行它们。
        go.SendMessage("OnSpawn", SendMessageOptions.DontRequireReceiver);

        return go;
    }

    /// <summary>
    /// <para>隐藏指定的游戏对象,并把它回收进对象池中。</para>
    /// </summary>
    /// <param name="go">要放入对象池的游戏对象</param>
    public void Despawn(GameObject go)
    {
        if (go == null) return;

        //遍历这个对象池中所有正在使用的游戏对象
        for (int i = 0; i < usedGameObjectsList.Count; i++)
        {
            if (usedGameObjectsList[i] == go)
            {
                //如果这个对象池的容量不为负数,且容纳的游戏对象已经满了,则把0号的游戏对象删掉,确保之后新的游戏对象能放入到池子中。
                if (capacity >= 0 && unUsedGameObjectsList.Count >= capacity)
                {
                    if (unUsedGameObjectsList.Count > 0)
                    {
                        Destroy(unUsedGameObjectsList[0]);
                        unUsedGameObjectsList.RemoveAt(0);
                    }
                }

                //把游戏对象放入到对象池中
                unUsedGameObjectsList.Add(go);
                usedGameObjectsList.RemoveAt(i);
				
				//如果该游戏对象身上继承Monobehavior的脚本中写了名叫OnDespawn的方法,则会执行它们。
                go.SendMessage("OnDespawn", SendMessageOptions.DontRequireReceiver);
                
                go.SetActive(false);

                go.transform.SetParent(transform, false);

                return;
            }
        }
    }

    /// <summary>
    /// 把通过这个对象池生成的所有游戏对象,全部隐藏并放入对象池中
    /// </summary>
    public void DespawnAll()
    {
        int count = usedGameObjectsList.Count;

        for (int i = 1; i <= count; i++)
        {
            Despawn(usedGameObjectsList[0]);
        }

        //清空列表
        usedGameObjectsList.Clear();
    }

    /// <summary>
    /// <para>在这个对象池中预加载指定数量的游戏对象。</para>
    /// </summary>
    /// <param name="amount">要预加载的数量</param>
    public void Preload(int amount = 1)
    {
        if (prefab == null) return;

        if (amount <= 0) return;

        for (int i = 1; i <= amount; i++)
        {
            GameObject go = Instantiate(prefab, Vector3.zero, Quaternion.identity);
            go.SetActive(false);
            go.transform.SetParent(transform, false);
            unUsedGameObjectsList.Add(go);
            go.name = prefab.name;
        }
    }
}

封装对象池管理器

1、对象池管理器代码

csharp 复制代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// 对象池管理器
/// </summary>
public class ObjectPoolsManager : Singleton<ObjectPoolsManager>
{
    //所有对象池的父物体
    GameObject poolsParent;

    //所有对象池共同父物体的名字
    readonly string poolsParentName = "ObjectPools";

    //当前所有对象池的列表
    public List<ObjectPool> objectPoolsList = new List<ObjectPool>();

    //键:由对象池生成的并且正在使用的游戏对象
    //值:这个游戏对象所属的对象池
    public Dictionary<GameObject, ObjectPool> objectsDictionary = new Dictionary<GameObject, ObjectPool>();

    /// <summary>
    /// <para>从对象池获取一个对象</para>
    /// <para>如果对象池中有,则从对象池中取出来用。</para>
    /// <para>如果对象池中没有,则实例化该对象。</para>
    /// </summary>
    /// <param name="prefab">要生成的游戏对象的预制体</param>
    /// <param name="position">生成游戏对象的位置。如果没有设置父物体,则是世界坐标。如果设置了父物体,则是局部坐标。</param>
    /// <param name="rotation">生成游戏对象的旋转</param>
    /// <param name="parent">>生成游戏对象的父物体</param>
    /// <returns>返回游戏对象</returns>
    public GameObject Spawn(GameObject prefab, Vector3 position, Quaternion rotation, Transform parent = null)
    {
        if (prefab == null) return null;

        //生成对象池的父物体
        CreatePoolsParentIfNull();

        //通过预制体来查找它所属的对象池
        ObjectPool objectPool = FindPoolByPrefabOrCreatePool(prefab);

        //从对象池获取一个对象
        GameObject go = objectPool.Spawn(position, rotation, parent);

        //把生成的游戏对象与它所属的对象池记录到字典
        objectsDictionary.Add(go, objectPool);

        return go;
    }

    /// <summary>
    /// <para>隐藏指定的游戏对象,并把它回收进对象池中。</para>
    /// </summary>
    /// <param name="go">要放入对象池的游戏对象</param>
    /// <param name="delayTime">延迟多少秒执行</param>
    public void Despawn(GameObject go, float delayTime = 0)
    {
        if (go == null) return;

        //开启协程,延迟执行回收到对象池的逻辑。
        MonoManager.Instance.StartCoroutine(DespawnCoroutine(go, delayTime));
    }
    IEnumerator DespawnCoroutine(GameObject go, float delayTime = 0)
    {
        //等待指定秒数
        if (delayTime > 0)
            yield return CoroutineTool.WaitForSeconds(delayTime);

        //先从象池生成的正在使用的游戏对象的字典中找指定的游戏对象
        if (objectsDictionary.TryGetValue(go, out ObjectPool pool))
        {
            objectsDictionary.Remove(go);

            //把这个游戏对象放入找到的对象池
            pool.Despawn(go);
        }
        else
        {
            //获取这个游戏对象所属的对象池
            pool = FindPoolByUsedGameObject(go);
            if (pool != null)
                pool.Despawn(go);
        }
    }

    /// <summary>
    /// 把所有通过对象池生成的对象全部隐藏,并回收进对象池中。
    /// </summary>
    public void DespawnAll()
    {
        for (int i = 0; i < objectPoolsList.Count; i++)
        {
            objectPoolsList[i].DespawnAll();
        }
        objectsDictionary.Clear();
    }

    /// <summary>
    /// <para>在对象池中预加载指定数量的游戏对象。</para>
    /// </summary>
    /// <param name="prefab">游戏对象的预制体</param>
    /// <param name="amount">要预加载的数量</param>
    public void Preload(GameObject prefab, int amount = 1)
    {
        if (prefab == null) return;
        if (amount <= 0) return;

        //通过预制体来查找它所属的对象池
        ObjectPool pool = FindPoolByPrefabOrCreatePool(prefab);

        //预加载指定数量的游戏对象
        pool.Preload(amount);
    }

    /// <summary>
    /// <para>返回指定的预制体所属的对象池的容量。</para>
    /// <para>如果该对象池不存在,则会创建它,然后返回-1。</para>
    /// </summary>
    /// <param name="prefab">预制体</param>
    public int GetCapacity(GameObject prefab)
    {
        ObjectPool pool = FindPoolByPrefabOrCreatePool(prefab);

        return pool.capacity;
    }

    /// <summary>
    /// <para>设置指定的预制体所属的对象池的容量。</para>
    /// <para>如果该对象池不存在,则会创建它,然后再设置它的容量。</para>
    /// </summary>
    /// <param name="prefab">预制体</param>
    /// <param name="capacity">要设置的容量。如果设置为负数,则表示这个对象池可以容纳无数个游戏对象。</param>
    public void SetCapacity(GameObject prefab, int capacity = -1)
    {
        ObjectPool pool = FindPoolByPrefabOrCreatePool(prefab);

        pool.capacity = capacity;
    }

    /// <summary>
    ///生成对象池的父物体
    /// </summary>
    void CreatePoolsParentIfNull()
    {
        if (poolsParent == null)
        {
            //清空列表和字典,避免上一个场景的数据的影响。
            objectPoolsList.Clear();
            objectsDictionary.Clear();

            //生成一个空物体
            poolsParent = new GameObject(poolsParentName);
        }
    }

    /// <summary>
    /// <para>查找指定的预制体所属的对象池</para>
    /// </summary>
    /// <param name="prefab">预制体</param>
    ObjectPool FindPoolByPrefab(GameObject prefab)
    {
        if (prefab == null) return null;

        for (int i = 0; i < objectPoolsList.Count; i++)
        {
            if (objectPoolsList[i].prefab == prefab)
                return objectPoolsList[i];
        }
        return null;
    }

    /// <summary>
    /// <para>查找指定的游戏对象所属的对象池</para>
    /// </summary>
    /// <param name="go">要查找的游戏对象</param>
    ObjectPool FindPoolByUsedGameObject(GameObject go)
    {
        if (go == null) return null;

        for (int i = 0; i < objectPoolsList.Count; i++)
        {
            ObjectPool pool = objectPoolsList[i];

            //遍历每一个对象池正在使用的所有游戏对象
            for (int j = 0; j < pool.usedGameObjectsList.Count; j++)
            {
                if (pool.usedGameObjectsList[j] == go)
                    return pool;
            }
        }

        return null;
    }

    /// <summary>
    /// <para>通过预制体来查找它所属的对象池</para>
    /// </summary>
    /// <param name="prefab">预制体</param>
    ObjectPool FindPoolByPrefabOrCreatePool(GameObject prefab)
    {
        //生成对象池的父物体
        CreatePoolsParentIfNull();

        //查找并返回该预制体所属的对象池
        ObjectPool objectPool = FindPoolByPrefab(prefab);

        //如果该对象池不存在,则创建一个。
        if (objectPool == null)
        {
            //创建一个对象池
            objectPool = new GameObject($"ObjectPool-{prefab.name}").AddComponent<ObjectPool>();

            //设置这个对象池所管理的预制体
            objectPool.prefab = prefab;

            //把生成的对象池放到父物体中,方便管理。
            objectPool.transform.SetParent(poolsParent.transform);

            //记录这个对象池
            objectPoolsList.Add(objectPool);
        }

        return objectPool;
    }
}

2、测试调用

csharp 复制代码
public class PoolTest : MonoBehaviour
{
    private void OnGUI()
    {
        if (GUI.Button(new Rect(0, 0, 150, 70), "生成游戏对象"))
        {
            GameObject prefab = ResourcesManager.Instance.Load<GameObject>("Prefabs/Cube");

            GameObject go = ObjectPoolsManager.Instance.Spawn(prefab, transform.position, Quaternion.identity, transform);

            go.GetComponent<Rigidbody>().AddForce(Vector3.forward * 1000f);//加力

            //5秒后回收
            ObjectPoolsManager.Instance.Despawn(go, 5f);
        }
    }
}

效果

3、生成和回收游戏对象时自动调用的方法

在预制体添加脚本,书写生成和回收游戏对象时自动调用的方法,可以在上面书写对象初始化方法,重置对象参数

csharp 复制代码
public class Cube : MonoBehaviour {
    //生成游戏对象时自动调用
    void OnSpawn(){
        Debug.Log("生成游戏对象");
    }

    //回收游戏对象时自动调用
    void OnDespawn(){
        Debug.Log("回收游戏对象");
        
        //重置参数
        transform.position = Vector3.zero;
        GetComponent<Rigidbody>().velocity = Vector3.zero;
    }
}

效果

完结

赠人玫瑰,手有余香!如果文章内容对你有所帮助,请不要吝啬你的点赞评论和关注,你的每一次支持都是我不断创作的最大动力。当然如果你发现了文章中存在错误或者有更好的解决方法,也欢迎评论私信告诉我哦!

好了,我是向宇https://xiangyu.blog.csdn.net

一位在小公司默默奋斗的开发者,闲暇之余,边学习边记录分享,站在巨人的肩膀上,通过学习前辈们的经验总是会给我很多帮助和启发!如果你遇到任何问题,也欢迎你评论私信或者加群找我, 虽然有些问题我也不一定会,但是我会查阅各方资料,争取给出最好的建议,希望可以帮助更多想学编程的人,共勉~

相关推荐
Algorithm1576几秒前
云原生相关的 Go 语言工程师技术路线(含博客网址导航)
开发语言·云原生·golang
shinelord明10 分钟前
【再谈设计模式】享元模式~对象共享的优化妙手
开发语言·数据结构·算法·设计模式·软件工程
Monly2116 分钟前
Java(若依):修改Tomcat的版本
java·开发语言·tomcat
boligongzhu17 分钟前
DALSA工业相机SDK二次开发(图像采集及保存)C#版
开发语言·c#·dalsa
Eric.Lee202117 分钟前
moviepy将图片序列制作成视频并加载字幕 - python 实现
开发语言·python·音视频·moviepy·字幕视频合成·图像制作为视频
7yewh20 分钟前
嵌入式Linux QT+OpenCV基于人脸识别的考勤系统 项目
linux·开发语言·arm开发·驱动开发·qt·opencv·嵌入式linux
waicsdn_haha31 分钟前
Java/JDK下载、安装及环境配置超详细教程【Windows10、macOS和Linux图文详解】
java·运维·服务器·开发语言·windows·后端·jdk
_WndProc33 分钟前
C++ 日志输出
开发语言·c++·算法
windwind200035 分钟前
游戏关卡设计方法的杂感
游戏·关卡设计
qq_4335545442 分钟前
C++ 面向对象编程:+号运算符重载,左移运算符重载
开发语言·c++