Unity 对象池(缓存池)设计与实现

一、为什么需要对象池

在 Unity 项目中,频繁调用 Instantiate / Destroy 创建与销毁 GameObject,通常会引发以下问题:

  • 产生大量 GC(垃圾回收),导致明显卡顿
  • 运行时性能波动较大,尤其在 战斗系统 / UI 列表 / 特效系统 中更为突出
  • 对象生命周期分散,难以统一管理与维护
    对象池(Object Pool / 缓存池) 的核心思想可以概括为一句话:

对象不用就回收,需要时复用,而不是反复创建和销毁。


二、对象池模型

现实概念 程序抽象 说明
柜子 PoolMgr 统一管理所有对象类型
抽屉 PoolData 按对象名区分的子池
物品 GameObject 被缓存与复用的实例

三、核心类结构设计

1. PoolData(抽屉)—— 单一类型对象管理

关键成员

  • Stack<GameObject>:对象缓存容器
  • rootObj:抽屉根节点(仅在开启布局时存在)

取对象流程

  1. 从栈中 Pop 一个对象
  2. 激活对象(SetActive(true)
  3. 断开与抽屉父节点的层级关系

回收对象流程

  1. 失活对象(SetActive(false)
  2. 重新挂到抽屉根节点下
  3. Push 回栈中等待下次复用

2. PoolMgr(柜子)—— 全局对象池管理器

核心成员

Dictionary<string, PoolData> poolDic; // 抽屉集合
GameObject poolObj;                  // Pool 根节点(调试用)
static bool isOpenLayout;             // 是否启用层级整理

四、代码实现(PoolMgr.cs)

using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
/// <summary>
/// 抽屉对象
/// </summary>
public class PoolData
{
    //抽屉根对象 用来布局
    private GameObject rootObj;
    //用来存储抽屉中对象的容器
    private Stack<GameObject> dataList = new Stack<GameObject>();
    //获取容器中对象的数量
    public int Count => dataList.Count;

    //传入name作为抽屉对象物体名 传入poolObj作为抽屉对象的父对象 
    public PoolData(string name, GameObject poolObj)
    {
        if (PoolMgr.isOpenLayout)
        {
            rootObj = new GameObject(name);
            rootObj.transform.parent = poolObj.transform;
        }
    }
    /// <summary>
    /// 取出容器中对象
    /// </summary>
    public GameObject GetObj()
    {
        //取出对象物体
        GameObject obj = dataList.Pop();
        //激活取出的对象
        obj.SetActive(true);
        if (PoolMgr.isOpenLayout)
        {
            //断开取出对象和抽屉对象的父子关系
            obj.transform.parent = null;
        }
        return obj;
    }
    /// <summary>
    /// 将对象放入抽屉中
    /// </summary>
    /// <param name="obj"></param>
    public void PushObj(GameObject obj)
    {
        //失活放入的对象
        obj.SetActive(false);
        if (PoolMgr.isOpenLayout)
        {
            //建立放入对象和抽屉对象的父子关系
            obj.transform.parent = rootObj.transform;
        }
        //将放入对象存入容器dataList中
        dataList.Push(obj);
    }
}

/// <summary>
/// 缓存池模块 管理器
/// </summary>
public class PoolMgr
{
    private static PoolMgr instance;
    public static PoolMgr Instance
    {
        get
        {
            if (instance == null)
            {
                instance = new PoolMgr();
            }
            return instance;
        }
    }
    //柜子容器当中有抽屉的体现
    private Dictionary<string, PoolData> poolDic = new Dictionary<string, PoolData>();
    private GameObject poolObj;
    //是否开启布局功能
    public static bool isOpenLayout = false;
    private PoolMgr() { }
    /// <summary>
    /// 拿东西的方法
    /// </summary>
    /// <param name="name">抽屉容器的名字</param>
    /// <returns>从缓存池中取出的对象</returns>
    public void GetObj(string name, UnityAction<GameObject> callBack)
    {
        GameObject obj;
        //有抽屉 并且 抽屉里 有对象 才去直接拿(同步加载资源)
        if (poolDic.ContainsKey(name) && poolDic[name].Count > 0)
        {
            //弹出栈中的对象 直接返回给外部使用
            obj = poolDic[name].GetObj();
            //取出缓存池对象操作 可用于对象初始化(避免激活失活无法运行Start初始化)
            callBack?.Invoke(obj);
        }
        //否则 就应该去创造
        else
        {
            //异步加载资源
            ResourceRequest rq = Resources.LoadAsync<GameObject>(name);
            rq.completed += (a) =>
            {
                obj = GameObject.Instantiate(rq.asset as GameObject);
                obj.name = name;
                //取出缓存池对象操作 可用于对象初始化(避免激活失活无法运行Start初始化)
                callBack?.Invoke(obj);
            };
        }
    }

    /// <summary>
    /// 往缓存池中放入对象
    /// </summary>
    /// <param name="name">抽屉(对象)的名字</param>
    /// <param name="obj">希望放入的对象 </param>
    public void PushObj(GameObject obj)
    {
        //放入缓存池对象相关操作
        //callBack?.Invoke(obj);
        if (poolObj == null && isOpenLayout)
        {
            poolObj = new GameObject("Pool");
        }
        //没有抽屉 创建抽屉
        if (!poolDic.ContainsKey(obj.name))
        {
            poolDic.Add(obj.name, new PoolData(obj.name, poolObj));
        }
        //往抽屉当中放对象
        poolDic[obj.name].PushObj(obj);
    }

    /// <summary>
    /// 用于清除整个柜子当中的数据
    /// 主要是 切场景时 使用
    /// </summary>
    public void ClearPool()
    {
        poolDic.Clear();
        poolObj = null;
    }
}

五、注意事项

  • GetObj 使用 callBack参数,取出缓存池对象操作 可用于对象初始化(避免激活失活无法运行Start初始化)
  • isOpenLayout 仅用于 调试与层级可视化,正式版本可关闭
  • 切场景必须调用 ClearPool
    • 否则字典仍持有旧场景对象引用
    • 可能导致内存泄漏或空引用异常

六、使用示例

// 取出对象
PoolMgr.Instance.GetObj("Test/Cube", obj =>
{
    obj.transform.position = Vector3.zero;
});

// 回收对象
PoolMgr.Instance.PushObj(obj);

// 切场景并清空缓存池
SceneManager.LoadScene("NewScene");
PoolMgr.Instance.ClearPool();

posted @ 2025-12-14 13:37  高山仰止666  阅读(2)  评论(0)    收藏  举报