Unity中的对象池ObjPool/PoolManager
1.先贴代码
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;/// <summary>
/// 对象池管理器 - 用于高效管理游戏对象的创建、回收和复用
/// 继承自MonoSingleton确保全局唯一性
/// </summary>
public class PoolManager : MonoSingleton<PoolManager>
{/// <summary>/// 对象池字典 - 存储每个预制体路径对应的对象队列/// </summary>private Dictionary<string, Queue<GameObject>> poolDict = new Dictionary<string, Queue<GameObject>>();/// <summary>/// 预制体缓存字典 - 缓存已加载的预制体以避免重复加载/// </summary>private Dictionary<string, GameObject> prefabDict = new Dictionary<string, GameObject>();/// <summary>/// 对象与路径的映射字典 - 用于追踪每个对象对应的预制体路径/// </summary>private Dictionary<GameObject, string> objToPathDict = new Dictionary<GameObject, string>();/// <summary>/// 当前场景名称 - 用于检测场景切换/// </summary>private string currentSceneName;protected override void Awake(){base.Awake();DontDestroyOnLoad(this);// 记录当前场景名称currentSceneName = SceneManager.GetActiveScene().name;// 注册场景切换事件SceneManager.sceneLoaded += OnSceneLoaded;}/// <summary>/// 场景加载完成时的回调处理/// </summary>/// <param name="scene">加载的场景</param>/// <param name="mode">加载模式</param>private void OnSceneLoaded(Scene scene, LoadSceneMode mode){// 检测到场景切换,清理失效的对象if (currentSceneName != scene.name){currentSceneName = scene.name;CleanupInvalidObjects();}}/// <summary>/// 清理失效的对象(通常是场景切换后被销毁的对象)/// </summary>private void CleanupInvalidObjects(){// 先收集所有需要更新的键值对,避免在遍历时修改字典Dictionary<string, Queue<GameObject>> updatedPools = new Dictionary<string, Queue<GameObject>>();foreach (var poolPair in poolDict){Queue<GameObject> queue = poolPair.Value;Queue<GameObject> newQueue = new Queue<GameObject>();// 检查队列中的每个对象是否仍然有效while (queue.Count > 0){GameObject obj = queue.Dequeue();if (obj != null && !obj.Equals(null)){newQueue.Enqueue(obj);}else{// 从映射字典中移除失效对象if (objToPathDict.ContainsKey(obj)){objToPathDict.Remove(obj);}}}// 如果队列发生了变化,记录需要更新的队列if (newQueue.Count != poolPair.Value.Count){updatedPools[poolPair.Key] = newQueue;}}// 在遍历完成后统一更新字典foreach (var updatePair in updatedPools){poolDict[updatePair.Key] = updatePair.Value;}// 清理映射字典中的失效引用List<GameObject> invalidObjects = new List<GameObject>();foreach (var objPair in objToPathDict){if (objPair.Key == null || objPair.Key.Equals(null)){invalidObjects.Add(objPair.Key);}}foreach (var invalidObj in invalidObjects){objToPathDict.Remove(invalidObj);}}/// <summary>/// 从对象池获取游戏对象/// </summary>/// <param name="path">预制体在Resources文件夹中的路径</param>/// <param name="callback">获取对象后的回调函数</param>public void GetObj(string path, Action<GameObject> callback){if (string.IsNullOrEmpty(path)){Debug.LogError("对象池:路径不能为空");callback?.Invoke(null);return;}// 确保路径格式正确,移除可能的前缀和后缀path = path.Replace("Resources/", "").Replace(".prefab", "");// 如果该路径的对象池不存在,创建新的队列if (!poolDict.ContainsKey(path)){poolDict[path] = new Queue<GameObject>();}Queue<GameObject> poolQueue = poolDict[path];GameObject obj = null;// 创建临时列表来处理需要重新排队的对象List<GameObject> objectsToRequeue = new List<GameObject>();// 尝试从对象池中获取可用的对象while (poolQueue.Count > 0 && obj == null){GameObject pooledObj = poolQueue.Dequeue();// 检查对象是否仍然有效且未激活if (pooledObj != null && !pooledObj.Equals(null)){if (!pooledObj.activeSelf){obj = pooledObj;}else{// 对象有效但已激活,暂存起来稍后重新排队objectsToRequeue.Add(pooledObj);}}else{// 对象已被销毁,从映射字典中移除if (objToPathDict.ContainsKey(pooledObj)){objToPathDict.Remove(pooledObj);}}}// 将需要重新排队的对象放回队列foreach (var objToRequeue in objectsToRequeue){poolQueue.Enqueue(objToRequeue);}// 如果没有可用对象,创建新对象if (obj == null){obj = CreateNewObject(path);if (obj == null){callback?.Invoke(null);return;}}// 激活对象并执行回调obj.SetActive(true);callback?.Invoke(obj);}/// <summary>/// 创建新的游戏对象/// </summary>/// <param name="path">预制体路径</param>/// <returns>创建的游戏对象</returns>private GameObject CreateNewObject(string path){// 加载预制体并缓存if (!prefabDict.ContainsKey(path)){GameObject prefab = Resources.Load<GameObject>(path);if (prefab == null){Debug.LogError($"对象池:预制体加载失败,路径:{path}");return null;}prefabDict[path] = prefab;}// 实例化对象GameObject obj = Instantiate(prefabDict[path]);obj.name = path; // 保持名称一致性// 添加到映射字典objToPathDict[obj] = path;return obj;}/// <summary>/// 释放对象回对象池/// </summary>/// <param name="obj">要释放的游戏对象</param>public void ReleaseObj(GameObject obj){if (obj == null || obj.Equals(null))return;// 检查对象是否来自对象池if (!objToPathDict.TryGetValue(obj, out string path)){Debug.LogWarning($"对象池:尝试释放非对象池管理的对象 {obj.name}");return;}// 设置对象的父级并停用obj.transform.SetParent(transform);obj.SetActive(false);// 重置对象状态(可选,根据需要实现)ResetObjectState(obj);// 将对象放回对应的池中if (poolDict.TryGetValue(path, out Queue<GameObject> queue)){queue.Enqueue(obj);}}/// <summary>/// 重置对象状态(可根据项目需求自定义)/// </summary>/// <param name="obj">要重置的对象</param>private void ResetObjectState(GameObject obj){// 停止所有正在运行的协程MonoBehaviour[] monoBehaviours = obj.GetComponentsInChildren<MonoBehaviour>();foreach (var mb in monoBehaviours){if (mb != null){mb.StopAllCoroutines();}}// 重置Transform(但不重置WorldSpace Canvas的缩放)Canvas canvas = obj.GetComponent<Canvas>();if (canvas != null && canvas.renderMode == RenderMode.WorldSpace){// WorldSpace Canvas保持其缩放设置obj.transform.localRotation = Quaternion.identity;}else{// 普通对象重置所有Transform属性obj.transform.localPosition = Vector3.zero;obj.transform.localRotation = Quaternion.identity;obj.transform.localScale = Vector3.one;}// 重置CanvasGroup透明度CanvasGroup canvasGroup = obj.GetComponent<CanvasGroup>();if (canvasGroup != null){canvasGroup.alpha = 1f;}// 重置Rigidbody状态Rigidbody rb = obj.GetComponent<Rigidbody>();if (rb != null){rb.velocity = Vector3.zero;rb.angularVelocity = Vector3.zero;}Rigidbody2D rb2d = obj.GetComponent<Rigidbody2D>();if (rb2d != null){rb2d.velocity = Vector2.zero;rb2d.angularVelocity = 0f;}// 重置动画状态Animator animator = obj.GetComponent<Animator>();if (animator != null){animator.Rebind();}}/// <summary>/// 预加载指定数量的对象到池中/// </summary>/// <param name="path">预制体路径</param>/// <param name="count">预加载数量</param>public void PreloadObjects(string path, int count){if (string.IsNullOrEmpty(path) || count <= 0)return;path = path.Replace("Resources/", "").Replace(".prefab", "");for (int i = 0; i < count; i++){GetObj(path, (obj) =>{if (obj != null){ReleaseObj(obj);}});}}/// <summary>/// 获取指定路径对象池的当前大小/// </summary>/// <param name="path">预制体路径</param>/// <returns>对象池大小</returns>public int GetPoolSize(string path){path = path.Replace("Resources/", "").Replace(".prefab", "");if (poolDict.TryGetValue(path, out Queue<GameObject> queue)){return queue.Count;}return 0;}/// <summary>/// 清空指定路径的对象池/// </summary>/// <param name="path">预制体路径</param>public void ClearPool(string path){path = path.Replace("Resources/", "").Replace(".prefab", "");if (poolDict.TryGetValue(path, out Queue<GameObject> queue)){while (queue.Count > 0){GameObject obj = queue.Dequeue();if (obj != null && !obj.Equals(null)){if (objToPathDict.ContainsKey(obj)){objToPathDict.Remove(obj);}Destroy(obj);}}poolDict.Remove(path);}// 清理预制体缓存if (prefabDict.ContainsKey(path)){prefabDict.Remove(path);}}/// <summary>/// 清空所有对象池/// </summary>public void ClearAllPools(){foreach (var poolPair in poolDict){while (poolPair.Value.Count > 0){GameObject obj = poolPair.Value.Dequeue();if (obj != null && !obj.Equals(null)){Destroy(obj);}}}poolDict.Clear();prefabDict.Clear();objToPathDict.Clear();}/// <summary>/// 获取对象池的统计信息/// </summary>/// <returns>统计信息字符串</returns>public string GetPoolStats(){System.Text.StringBuilder sb = new System.Text.StringBuilder();sb.AppendLine("=== 对象池统计信息 ===");sb.AppendLine($"管理的对象池数量: {poolDict.Count}");sb.AppendLine($"缓存的预制体数量: {prefabDict.Count}");sb.AppendLine($"追踪的对象数量: {objToPathDict.Count}");sb.AppendLine("各池详情:");foreach (var poolPair in poolDict){sb.AppendLine($" {poolPair.Key}: {poolPair.Value.Count} 个对象");}return sb.ToString();}/// <summary> /// 组件销毁时的清理工作/// </summary>protected override void OnDestroy(){base.OnDestroy();// 取消事件注册SceneManager.sceneLoaded -= OnSceneLoaded;// 清理所有对象池ClearAllPools();}
}
2.用法
基础用法
1. 获取对象
// 从Resources加载"Prefabs/Bullet.prefab"
PoolManager.Instance.GetObj("Prefabs/Bullet", go =>
{if(go != null){go.transform.position = transform.position;go.GetComponent<Bullet>().Launch();}
});
2. 回收对象
// 子弹命中后回收
public class Bullet : MonoBehaviour
{void OnCollisionEnter(){PoolManager.Instance.ReleaseObj(gameObject);}
}
//延时自动回收
public float lifeTimer;private void OnEnable()
{StartCoroutine(AutoReleaseTimer(gameObject, lifeTimer));
}private IEnumerator AutoReleaseTimer(GameObject obj, float time)
{yield return new WaitForSeconds(time);PoolManager.Instance.ReleaseObj(obj);
}
3. 预加载对象
// 游戏初始化时预加载10个子弹
void Start()
{PoolManager.Instance.PreloadObjects("Prefabs/Bullet", 10);
}
高级功能
1. 获取对象池状态
// 调试时查看池状态
void DebugPools()
{Debug.Log(PoolManager.Instance.GetPoolStats());// 输出示例:// === 对象池统计信息 ===// 管理的对象池数量: 3// Prefabs/Bullet: 15 个对象// Prefabs/Explosion: 5 个对象
}
2. 清理对象池
// 关卡结束时清理特定池
void OnLevelComplete()
{PoolManager.Instance.ClearPool("Prefabs/Enemy");
}// 游戏退出时清理所有池
void OnApplicationQuit()
{PoolManager.Instance.ClearAllPools();
}
3. 自定义重置逻辑
// 如需特殊重置逻辑,继承并扩展
public class CustomPoolManager : PoolManager
{protected override void ResetObjectState(GameObject obj){base.ResetObjectState(obj); // 保持基础重置// 自定义重置var particle = obj.GetComponent<ParticleSystem>();if(particle) particle.Stop();obj.GetComponent<Rigidbody>().isKinematic = true;}
}
使用注意事项
-
路径规范:
-
预制体必须放在
Resources
目录下 -
使用相对路径(如
"UI/Popups/MessageBox"
)
-
-
对象生命周期:
-
永远使用
ReleaseObj()
代替Destroy()
-
场景切换时自动清理无效对象
-
-
特殊组件处理:
-
WorldSpace Canvas 不会重置缩放
-
自动停止所有协程
-
重置 Rigidbody 物理状态
-
-
错误处理:
-
尝试释放非池管理对象会触发警告
-
加载失败会返回 null 并报错
-
最佳实践
预加载策略:
// 主菜单加载时预加载常用资源
void LoadMainMenu()
{PreloadObjects("Effects/Fire", 5);PreloadObjects("Projectiles/Rocket", 8);PreloadObjects("UI/DamageText", 10);
}
结合场景管理:
// 场景卸载时清理相关对象
void OnSceneUnload(string sceneName)
{if(sceneName == "CombatScene"){ClearPool("Enemies/Zombie");ClearPool("Weapons/Laser");}
}
性能监控:
void Update()
{// 每30秒输出池状态if(Time.frameCount % 1800 == 0)Debug.Log(GetPoolStats());
}
⚠️ 重要:
对象池管理的对象禁止直接使用 Destroy()
,必须通过 ReleaseObj()
回收