Unity简单对象池

SmPool 类

一个对象池管理器,用于高效地管理游戏对象(如 Prefab)。在游戏开发中,经常需要频繁生成和销毁对象(例如子弹、敌人、特效等)。

如果每次都使用 Instantiate 和 Destroy 来创建和销毁对象,会对性能造成影响,尤其是在移动设备或其他性能有限的设备上。

对象池的概念是提前创建一批对象,当需要使用时将其从池中取出,使用完后再放回池中,而不是销毁它们。这样可以大大减少对象创建和销毁的开销。

主要功能

预加载对象:通过 Prepare() 方法,预先创建指定数量的对象,并存储在一个栈(mStock)中。

对象生成与回收:当需要使用对象时,通过 SpawnItem() 方法从池中取出一个对象并激活它;当不再使用时,通过 DespawnItem() 将对象放回池中或销毁。

自动清理:池支持自动清理功能(AutoCull),会定期检查池中的对象数量,并移除多余的对象以节省内存。

动态扩展:当池中的对象不够用时,池会动态扩展,即增加新的对象到池中。

对象重用:当池达到最大容量时,可以选择重用已生成的对象(ReUse 模式),避免频繁创建新对象。

类的组成

Prefab 和 PoolName:每个对象池都会管理一种 Prefab,并通过 PoolName 进行标识。

DontDestroy:设置为 true 时,该池的对象在场景切换时不会被销毁。

AllocationBlockSize:当池需要扩展时,每次增加的对象数量。

MaxPoolSize 和 MinPoolSize:池的最大和最小对象数量。池会根据需求动态调整对象数量。

AutoCull:是否自动清理池中的多余对象。

fnSpawn 和 fnDespawn:自定义生成和回收对象时的行为函数。

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

/// <summary>
/// 管理Prefab池的类
/// </summary>
public class SmPool : MonoBehaviour
{
    /// <summary>
    /// 池的名称
    /// </summary>
    public string PoolName;

    /// <summary>
    /// 池中管理的Prefab,一个池只管理一个对应的预制体
    /// </summary>
    public GameObject Prefab;

    /// <summary>
    /// 在场景切换时保持持久化
    /// </summary>
    public bool DontDestroy = false;

    /// <summary>
    /// 游戏启动时自动准备。如果为false,需要手动调用Prepare()方法
    /// </summary>
    public bool PrepareAtStart = true;

    /// <summary>
    /// 池需要扩展或收缩时的分配的大小
    /// </summary>
    public int AllocationBlockSize = 1;//自行调试

    /// <summary>
    /// 池的最小大小
    /// </summary>
    public int MinPoolSize = 1;//自行调试

    /// <summary>
    /// 池的最大大小(池的最大大小应该是池内池外的和)
    /// </summary>
    public int MaxPoolSize = 1;//自行调试

    /// <summary>
    /// 超出最大池大小时的行为
    /// </summary>
    public PoolExceededMode OnMaxPoolSize = PoolExceededMode.Ignore;

    /// <summary>
    /// 自动清除 items直到池大小达到MaxPoolSize
    /// </summary>
    public bool AutoCull = true;

    /// <summary>
    /// 自动清除的时间间隔,单位为秒
    /// </summary>
    public float CullingSpeed = 1.0f;//自行调试

    /// <summary>
    /// 是否记录所有操作日志
    /// </summary>
    public bool DebugLog = false;

    /// <summary>
    /// 清除间隔时间,单位为秒
    /// </summary>
    public float CullingInterval = 2f;//自行调试

    /// <summary>
    /// 对象从池中拿出时回调
    /// </summary>
    public System.Action<GameObject> fnSpawn;

    /// <summary>
    /// 对象放回池时回调.
    /// 预加载的时候也会被调用
    /// </summary>
    public System.Action<GameObject> fnDespawn;

    /// <summary>
    /// 存放未使用对象的栈(池内)
    /// </summary>
    Stack<GameObject> mStock = new Stack<GameObject>();

    /// <summary>
    /// 存放已生成的对象列表(池外)
    /// </summary>
    public List<GameObject> mSpawned = new List<GameObject>();

    /// <summary>
    /// 上次清除的时间
    /// </summary>
    float mLastCullingTime;

    /// <summary>
    /// 当前池中对象数
    /// </summary>
    public int InStock
    {
        get { return mStock.Count; }
    }

    /// <summary>
    /// 当前生成对象数
    /// </summary>
    public int Spawned
    {
        get { return mSpawned.Count; }
    }

    #region ### Unity 回调 ###

    /// <summary>
    /// 初始化池,需要手动传入预制体/某对象的引用
    /// 会根据PrepareAtStart属性预加载池,默认是预加载
    /// </summary>
    /// <param name="pf">预制体</param>
    /// <param name="min">最小尺寸</param>
    /// <param name="max">最大尺寸</param>
    public void Init(GameObject pf, int min, int max)
    {
        DoInit(pf, min, max);
    }

    /// <summary>
    /// 初始化池,不传入引用,而是传入一个预制体路径.
    /// 注意:是Resources文件夹下的路径
    /// 会根据PrepareAtStart属性预加载池,默认是预加载
    /// </summary>
    /// <param name="sgPf">预制体路径</param>
    /// <param name="min">最小尺寸</param>
    /// <param name="max">最大尺寸</param>
    public void Init(string sgPf, int min, int max)
    {
        GameObject pf = Resources.Load<GameObject>(sgPf); // 加载资源
        DoInit(pf, min, max);
    }

    /// <summary>
    /// 创建一个池,这会在场景中生成一个对象,但没初始化.
    /// </summary>
    /// <param name="sg">池属性名</param>
    /// <param name="parent">池对象的父物体</param>
    /// <param name="cullingInterval"></param>
    /// <returns>返回池</returns>
    public static SmPool CreatePool(string sg, Transform parent, float cullingInterval = 2f)
    {
        GameObject go = new GameObject(sg); // 创建池对象
        Transform tm = go.transform;
        tm.SetParent(parent); // 设置父对象
        tm.localPosition = Vector3.zero; // 初始化位置
        tm.localScale = Vector3.one; // 初始化缩放
        SmPool pm = go.AddComponent<SmPool>();
        pm.PoolName = sg;
        pm.CullingInterval = cullingInterval;
        return pm;
    }

    /// <summary>
    /// 实际初始化池
    /// </summary>
    /// <param name="go">池管理那个预制体</param>
    /// <param name="min">池的最小尺寸</param>
    /// <param name="max">池的最大尺寸</param>
    void DoInit(GameObject go, int min, int max)
    {
        this.Prefab = go; // 设定管理的Prefab
        this.MinPoolSize = min; // 最小池大小
        this.MaxPoolSize = max; // 最大池大小
        //
        if (PoolName.Length == 0)
            Debug.LogWarning("SmartPool: 缺少池名,属于'" + gameObject.name + "'!");
        if (DontDestroy)
            DontDestroyOnLoad(gameObject); // 设置为在场景切换中不销毁对象

        if (Prefab == null)
        {
            Debug.LogError("SmartPool: 池'" + PoolName + "'缺少Prefab!");
        }

        if (PrepareAtStart)
            Prepare(); // 如果自动准备,则调用Prepare()
    }

    void OnEnable()
    {
        StartCoroutine(__LateTick()); // 开始协程进行定期清除
    }

    /// <summary>
    /// 协程定时自动清除池中对象
    /// </summary>
    /// <returns></returns>
    IEnumerator __LateTick()
    {
        WaitForSeconds wfs = new WaitForSeconds(CullingInterval); // 定义清除间隔时间
        for (;;) //无限循环
        {
            if (AutoCull && Time.time - mLastCullingTime > CullingSpeed)
            {
                mLastCullingTime = Time.time; // 更新上次清除时间
                Cull(true); // 执行清除
            }

            yield return wfs;
        }
    }

    void Reset()
    {
        // 重置所有参数
        PoolName = "";
        Prefab = null;
        DontDestroy = false;
        AllocationBlockSize = 1;
        MinPoolSize = 1;
        MaxPoolSize = 1;
        OnMaxPoolSize = PoolExceededMode.Ignore;
        DebugLog = false;
        AutoCull = true;
        CullingSpeed = 1f;
        mLastCullingTime = 0;
    }

    #endregion

    #region ### 池的操作方法 ###

    /// <summary>
    /// 删除池中的所有实例,销毁对象并清空引用
    /// </summary>
    void Clear()
    {
        if (DebugLog)
            Debug.Log("SmartPool (" + PoolName + "): 清除所有实例 " + Prefab.name);
        foreach (GameObject go in mSpawned)
            Destroy(go); // 销毁所有生成的对象
        mSpawned.Clear(); // 清空已生成列表
        foreach (GameObject go in mStock)
            Destroy(go); // 销毁库存中的对象
        mStock.Clear(); // 清空库存栈
    }

    /// <summary>
    /// 收缩库存至符合MaxPoolSize
    /// </summary>
    public void Cull()
    {
        Cull(false);
    }

    /// <summary>
    /// 收缩库存至符合MaxPoolSize
    /// </summary>
    /// <param name="smartCull">如果为true,则最大移除AllocationBlockSize个对象</param>
    public void Cull(bool smartCull)
    {
        int toCull = (smartCull)
            ? Mathf.Min(AllocationBlockSize, mStock.Count - MaxPoolSize)
            : mStock.Count - MaxPoolSize;

        while (toCull-- > 0)
        {
            GameObject item = mStock.Pop(); // 从库存栈中移除
            Destroy(item); // 销毁对象
        }
    }

    /// <summary>
    /// 回收对象到池中
    /// 如果没有放回回调那么失活,否则不失活
    /// 注意:这个对象如果不是这个池生成的会被销毁
    /// </summary>
    /// <param name="item"></param>
    public void DespawnItem(GameObject item)
    {
        if (!item) return;
        if (IsSpawned(item))
        {
            mSpawned.Remove(item); // 从已生成列表移除
            mStock.Push(item); // 加入库存栈
            if (fnDespawn == null)
            {
                item.SetActive(false); // 如果没有自定义回收行为,禁用对象
            }
            else
                fnDespawn(item); // 调用自定义回收行为
        }
        else
        {
            GameObject.Destroy(item); // 如果不在已生成列表中,直接销毁对象
        }
    }

    /// <summary>
    /// 回收池中所有对象
    /// </summary>
    public void DespawnAllItems()
    {
        while (mSpawned.Count > 0)
            DespawnItem(mSpawned[0]); // 回收所有对象
    }

    /// <summary>
    /// 销毁已生成的对象而不是回收它
    /// </summary>
    /// <param name="item">需要销毁的对象</param>
    public void KillItem(GameObject item)
    {
        if (!item)
        {
            return;
        }

        mSpawned.Remove(item); // 从已生成列表移除
        Destroy(item); // 销毁对象
    }

    /// <summary>
    /// 判断对象是否由该池管理
    /// </summary>
    /// <param name="item">对象</param>
    /// <returns>如果对象由该池管理,返回true</returns>
    public bool IsManagedObject(GameObject item)
    {
        if (!item)
        {
            return false;
        }

        if (mSpawned.Contains(item) || mStock.Contains(item))
            return true;
        else
            return false;
    }

    /// <summary>
    /// 判断对象是否由该池生成
    /// </summary>
    /// <param name="item">对象</param>
    /// <returns>如果对象由该池生成,返回true</returns>
    public bool IsSpawned(GameObject item)
    {
        if (!item)
        {
            return false;
        }

        return (mSpawned.Contains(item)); // 判断对象是否在已生成列表中
    }

    /// <summary>
    /// 生成实例并将其添加(放回)池
    /// 如果放回池中的回调存在那么不失活,否则失活
    /// </summary>
    /// <param name="no">生成的数量</param>
    void Populate(int no)
    {
        GameObject go;
        while (no > 0)
        {
            go = Instantiate<GameObject>(Prefab); // 实例化Prefab
            go.transform.SetParent(transform); // 设置父对象
            go.name = Prefab.name; // 设置名称
            mStock.Push(go); // 加入库存栈
            no--;
            if (fnDespawn == null)
                go.SetActive(false); // 如果没有自定义行为,禁用对象
            else
                fnDespawn(go); // 调用自定义行为
        }
    }

    /// <summary>
    /// 预加载池.首先清空池并重新填充池到最小大小
    /// </summary>
    public void Prepare()
    {
        Clear(); // 清空池
        mStock = new Stack<GameObject>(MinPoolSize); // 初始化库存栈
        Populate(MinPoolSize); // 填充池
    }

    /// <summary>
    /// 生成实例并激活它,添加到已生成列表中
    /// </summary>
    /// <returns>生成的实例</returns>
    public GameObject SpawnItem()
    {
        GameObject item = null;
        // 如果库存没有对象,则生成更多
        if (InStock == 0)
        {
            //如果没达到池的数量上限或池本身忽略上限,那么会填充池
            if (Spawned < MaxPoolSize || OnMaxPoolSize == PoolExceededMode.Ignore)
                Populate(AllocationBlockSize); // 填充池
        }

        // 如果有对象在库存中,则取出
        if (InStock > 0)
        {
            item = mStock.Pop();
        }
        else if (OnMaxPoolSize == PoolExceededMode.ReUse)
        {
            item = mSpawned[0]; // 重用已生成对象
            mSpawned.RemoveAt(0);
        }

        if (item != null)
        {
            mSpawned.Add(item); // 添加到已生成列表中
            if (fnSpawn == null)//todo 这里如果放入池的时候失活,拿出的时候不一定会激活
            {
                item.SetActive(true); // 激活对象
                item.transform.localPosition = Vector3.zero; // 重置位置
            }
            else
                fnSpawn(item); // 调用自定义生成行为
        }

        return item;
    }

    #endregion
}

/// <summary>
/// 定义超出MaxPoolSize时的反应
/// </summary>
[System.Serializable]
public enum PoolExceededMode : int
{
    /// <summary>
    /// 忽略MaxPoolSize
    /// </summary>
    Ignore = 0,

    /// <summary>
    /// 当超出MaxPoolSize时停止生成
    /// </summary>
    StopSpawning = 1,

    /// <summary>
    /// 当超出MaxPoolSize时重用已生成对象
    /// </summary>
    ReUse = 2
}
相关推荐
Murphy20232 小时前
.net4.0 调用API(form-data)上传文件及传参
开发语言·c#·api·httpwebrequest·form-data·uploadfile·multipart/form-
我曾经是个程序员3 小时前
C#Directory类文件夹基本操作大全
服务器·开发语言·c#
鸿喵小仙女4 小时前
C# WPF读写STM32/GD32单片机Flash数据
stm32·单片机·c#·wpf
一个不正经的林Sir4 小时前
C#WPF基础介绍/第一个WPF程序
开发语言·c#·wpf
码农君莫笑14 小时前
使用blazor开发信息管理系统的应用场景
数据库·信息可视化·c#·.net·visual studio
可喜~可乐17 小时前
C# WPF开发
microsoft·c#·wpf
666和77721 小时前
C#的单元测试
开发语言·单元测试·c#
小码编匠1 天前
WPF 星空效果:创建逼真的宇宙背景
后端·c#·.net
超龄魔法少女1 天前
[Unity] ShaderGraph动态修改Keyword Enum,实现不同效果一键切换
unity·技术美术·shadergraph
蔗理苦1 天前
2024-12-24 NO1. XR Interaction ToolKit 环境配置
unity·quest3·xr toolkit