对象池由浅入深第一节:Unity对象池教程:原理与实践-CSDN博客
对象池由浅入深第二节:对象池由浅入深第二节:基于 UnityEngine.Pool 封装工具类 ObjectPoolPro-CSDN博客
对象池由浅入深第三节:对象池由浅入深第三节:基于 ObjectPoolPro 扩展 Unity 对象池--泛型对象池、自动回收和场景隔离-CSDN博客
前言
在上一节中,我们实现了一个功能完善的对象池工具 ObjectPoolPro,用于优化频繁生成和销毁的 GameObject 对象。本节将在此基础上进一步扩展对象池功能,增加更高级的特性,包括:
-
泛型对象池 GenericObjectPool<T>: 支持非 GameObject 类型的对象复用,可用于逻辑对象(例如 Buff、行为树节点、路径点等)的池化管理。
-
AutoRecycle 自动回收组件: 一个可挂载在 GameObject 上的脚本,在物体失活(OnDisable)或指定时间后,自动将该对象回收到对象池,免去手工回收的繁琐。
-
场景隔离机制: 为对象池增加“按场景ID分类”的管理,在不同场景使用各自的对象池,并在场景切换(通过 SceneManager.sceneUnloaded 事件)时自动清理当前场景相关的池,防止跨场景的资源残留。
接下来,我们将分三部分详细讲解每个特性的实现,每部分提供完整的示例代码(包含详细中文注释)和实践讲解。
一、实现泛型对象池 GenericObjectPool<T>
为什么需要泛型对象池? 在游戏开发中,不仅仅是游戏物体需要池化,很多纯逻辑对象也会被频繁创建和销毁,例如技能 Buff 类、AI 的行为树节点、路径点对象等。频繁 new 和 GC 这些对象同样会影响性能。通过泛型对象池,我们可以重复利用这些非 GameObject 对象,减少 GC 压力。
下面我们实现一个通用的泛型对象池类 GenericObjectPool<T>,用于管理任意引用类型对象的获取和回收。该类使用 C# 泛型和约束 where T : class, new()
来确保类型 T 有无参构造函数,以便在池空时可以直接创建新实例。
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 通用泛型对象池,用于复用非GameObject的逻辑对象。
/// </summary>
public class GenericObjectPool<T> where T : class, new()
{
private Stack<T> pool; // 使用栈(Stack)保存可复用对象
private int maxCount; // 池子的最大容量
/// <summary>构造函数:初始化对象池。</summary>
/// <param name="initialCount">预创建对象的数量。</param>
/// <param name="maxCount">池最大容量,上限为多少对象。</param>
public GenericObjectPool(int initialCount = 0, int maxCount = int.MaxValue)
{
this.pool = new Stack<T>();
this.maxCount = maxCount;
// 预创建一定数量的对象放入池中,避免初次使用时大量new
for (int i = 0; i < initialCount; i++)
{
pool.Push(new T());
}
}
/// <summary>从对象池获取一个对象。</summary>
/// <returns>池中可用的对象实例,如果池为空则创建新实例。</returns>
public T Get()
{
if (pool.Count > 0)
{
// 池里有闲置对象,直接弹出复用
return pool.Pop();
}
else
{
// 池为空,创建新对象(使用T的无参构造函数)
return new T();
}
}
/// <summary>回收对象,将对象放回池中备用。</summary>
/// <param name="obj">要回收的对象实例。</param>
public void Release(T obj)
{
if (obj == null) return;
// 如果池已满,不再回收该对象(直接放弃,让GC回收,避免占用过多内存)
if (pool.Count >= maxCount)
{
// 可选:如果对象实现了某接口,可在这里调用其Reset方法清理状态
// 超出容量的对象直接舍弃,不放入池。
}
else
{
// 将对象压入栈,供下次复用
pool.Push(obj);
}
}
/// <summary>清空池中所有对象。</summary>
public void Clear()
{
pool.Clear();
}
/// <summary>当前池内剩余对象数量。</summary>
public int Count
{
get { return pool.Count; }
}
}
实现解析: 上面的 GenericObjectPool<T>
使用了栈(Stack)作为内部存储结构,这在对象池中很常见。Get() 方法从栈顶弹出一个对象,如果池为空则新建一个 T 对象返回。Release() 方法则将对象压回栈中,如果已经达到设定的最大容量 maxCount
,则放弃该对象(不再入池)以防止池无限增长占用内存。这里可以根据需要设置 maxCount
来控制池大小;如果不传则默认为 int.MaxValue
表示不限制数量。另外提供了 Clear()
方法可以一次性清空池,以及 Count
属性方便调试查看池内剩余对象数量。
使用示例: 假设我们有一个表示 Buff 效果的类,例如:
// 示例逻辑类:Buff(仅用于演示,可根据实际需求拓展属性和方法)
public class Buff
{
public int id;
public float duration;
public void Reset()
{
// 重置 Buff 状态(示例)
id = 0;
duration = 0f;
}
}
现在我们可以使用 GenericObjectPool<Buff>
来管理 Buff 对象的复用:
// 创建一个用于 Buff 的对象池,预创建10个对象,最大容量50
GenericObjectPool<Buff> buffPool = new GenericObjectPool<Buff>(initialCount: 10, maxCount: 50);
// 从池中获取 Buff 对象
Buff buff = buffPool.Get();
buff.id = 101;
buff.duration = 5.0f;
// ... 使用 buff 对象 ...
Debug.Log($"使用 Buff {buff.id}, 持续时间 {buff.duration} 秒");
// 使用完毕后,将 Buff 对象回收到池中
buff.Reset(); // 可选:复用前重置对象状态
buffPool.Release(buff);
通过上述方式,我们就无需每次都 new Buff()
或等待垃圾回收。当需要大量逻辑对象(如 Buff、任务、AI节点等)反复使用时,泛型对象池能够显著降低内存分配和GC压力,提高游戏性能。
二、编写 AutoRecycle 自动回收组件
为什么需要 AutoRecycle? 在很多情况下,我们生成一个临时的游戏对象(例如特效、子弹、抛洒物等),希望它在一段时间后或用完即弃时自动回收到对象池,而不需要手动调用回收方法。如果遗漏回收,不仅浪费内存,还可能导致场景切换时遗留无用的对象。为了解决这个问题,我们可以编写一个 AutoRecycle 脚本组件,把它挂载到预制体上,使对象在失活(OnDisable)或达到回收时间时自动返回池中。
下面是 AutoRecycle.cs 脚本的实现:
using System.Collections;
using UnityEngine;
/// <summary>
/// 挂载在需要自动回收的对象上,支持在失活或延时后自动归还对象池。
/// </summary>
public class AutoRecycle : MonoBehaviour
{
[Tooltip("对象启用后自动回收的延迟时间(秒)。<=0 表示不使用延时自动回收。")]
public float recycleAfterTime = 0f; // 延迟回收时间
private Coroutine recycleTimer; // 内部使用的协程引用
// 当对象启用时(Spawn后),如果设置了自动回收时间,则启动协程计时
void OnEnable()
{
if (recycleAfterTime > 0f)
{
// 启动延时回收的协程
recycleTimer = StartCoroutine(AutoRecycleTimer());
}
}
// 当对象被禁用时(例如手动 SetActive(false) 或场景切换销毁前),立即回收到对象池
void OnDisable()
{
// 停止计时协程(如果尚未完成),避免重复回收
if (recycleTimer != null)
{
StopCoroutine(recycleTimer);
recycleTimer = null;
}
// 调用对象池进行回收
ObjectPoolPro.Recycle(this.gameObject);
}
// 协程:等待 recycleAfterTime 秒后自动禁用对象,从而触发回收
private IEnumerator AutoRecycleTimer()
{
// 等待指定秒数
yield return new WaitForSeconds(recycleAfterTime);
// 如果等待结束时对象仍然是激活状态,则将其Disable,触发OnDisable进行回收
if (this.gameObject.activeSelf)
{
// 将对象设置为非激活,Unity会调用其OnDisable方法
this.gameObject.SetActive(false);
//(注意:OnDisable中会执行真正的回收操作)
}
}
}
实现解析: 我们在 OnEnable 中判断如果 recycleAfterTime
大于 0,就启动一个协程 AutoRecycleTimer()
来等待指定时间。协程等待结束后,再次检查对象是否仍然处于激活状态,如果是,则调用 gameObject.SetActive(false)
将对象失活。这样做的好处是利用 OnDisable 回调统一处理回收逻辑,避免直接在协程中调用回收可能出现的竞态条件。在 OnDisable 中,我们首先停止并清除尚未完成的协程(如果对象提前被禁用,协程也会自动停止,但出于稳妥我们手动停止以防万一),然后调用 ObjectPoolPro.Recycle(this.gameObject)
将当前对象归还池中。
使用说明: 将 AutoRecycle
脚本添加到需要自动回收的预制体上,并根据需求设置 RecycleAfterTime
延迟秒数:
-
按失活回收: 如果不设置延迟(
recycleAfterTime <= 0
),则当该对象被手动禁用时,会立刻触发 OnDisable,自动回收到池。例如,一个敌人死亡时其GameObject被设置为不可见,这时 AutoRecycle 会检测到 OnDisable 并将其回收到池中,无需额外代码。 -
按定时回收: 如果设置了延迟时间,例如 5 秒,那么对象每次激活后会在5秒计时结束时自动失活自己并回收。常见用于粒子特效、抛射物等场景,例如子弹壳在掉落5秒后自动消失归还,爆炸特效播完2秒后自动回收等。
实践示例: 假设我们有一个爆炸特效预制体 ExplosionPrefab,我们希望它在生成后2秒自动回收。我们可以这样设置:
-
在 ExplosionPrefab 上添加 AutoRecycle 组件,将
Recycle After Time
设为 2。 -
使用对象池生成爆炸特效:调用
ObjectPoolPro.Spawn(explosionPrefab)
来获取实例。特效播放开始计时,两秒后 AutoRecycle 协程会自动将其失活并回收。 -
若在2秒内手动禁用了该特效对象,AutoRecycle 的 OnDisable 仍然会保证回收逻辑被执行。
通过 AutoRecycle,我们大大简化了临时对象的生命周期管理,在脚本中无需反复调用回收函数,一切交给组件自动处理,减少了遗忘回收导致问题的风险。
三、增加场景隔离的对象池管理机制
为什么需要场景隔离? 当游戏切换场景时,之前场景中缓存的对象如果不清理,可能会在内存中滞留,甚至误被下一场景重用,造成意想不到的行为。例如,在场景A中创建了一批子弹对象池,切换到场景B时,这些子弹对象仍挂在内存中(尤其如果对象池管理器是全局单例且标记为 DontDestroyOnLoad
),这不仅浪费内存,还可能因为场景不一致导致错误。理想情况是不同场景拥有各自的对象池,场景卸载时自动清空相关池子,真正做到资源隔离。
为此,我们修改 ObjectPoolPro 工具以支持按场景ID注册对象池。核心思路是:使用一个数据结构将对象池与场景关联,每个场景有自己的池字典;在场景卸载事件中,清理该场景的所有池和对象。
下面是修改后的 ObjectPoolPro 部分代码,实现场景隔离管理:
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public static class ObjectPoolPro
{
// 按场景ID分类的对象池字典:每个场景映射到一个<预制体, 对象列表>的字典
private static Dictionary<int, Dictionary<GameObject, List<GameObject>>> poolsByScene
= new Dictionary<int, Dictionary<GameObject, List<GameObject>>>();
// 辅助字典:记录每个实例对象对应的原始预制体,用于回收时快速查找
private static Dictionary<GameObject, GameObject> prefabMap = new Dictionary<GameObject, GameObject>();
// 静态构造函数:订阅场景卸载事件
static ObjectPoolPro()
{
SceneManager.sceneUnloaded += OnSceneUnloaded;
}
/// <summary>注册对象池(按当前场景)。如果池已存在则不重复注册。</summary>
/// <param name="prefab">需要池化的预制体对象</param>
/// <param name="initialCount">预初始化对象数量</param>
public static void RegisterPool(GameObject prefab, int initialCount = 0)
{
int sceneId = SceneManager.GetActiveScene().buildIndex; // 当前场景ID(BuildIndex)
// 如果当前场景还没有注册任何池,则先初始化一个字典
if (!poolsByScene.ContainsKey(sceneId))
{
poolsByScene[sceneId] = new Dictionary<GameObject, List<GameObject>>();
}
var scenePools = poolsByScene[sceneId];
// 若该预制体在当前场景还未建立池,则创建新池列表
if (!scenePools.ContainsKey(prefab))
{
scenePools[prefab] = new List<GameObject>();
// 预创建 initialCount 个实例,放入池中备用
for (int i = 0; i < initialCount; i++)
{
GameObject obj = Object.Instantiate(prefab);
obj.SetActive(false);
// 记录该实例与预制体的关联,并加入池列表
prefabMap[obj] = prefab;
scenePools[prefab].Add(obj);
}
}
// 如果池已存在且已初始化,则不重复处理
}
/// <summary>从对象池获取对象实例。如果池空则创建新实例。</summary>
public static GameObject Spawn(GameObject prefab)
{
int sceneId = SceneManager.GetActiveScene().buildIndex;
// 确保当前场景的该预制体池存在(不存在则自动初始化0个)
RegisterPool(prefab, initialCount: 0);
var scenePools = poolsByScene[sceneId];
GameObject obj;
// 从池列表取出一个对象
if (scenePools[prefab].Count > 0)
{
obj = scenePools[prefab][0];
scenePools[prefab].RemoveAt(0); // 移除取出的实例
obj.SetActive(true);
}
else
{
// 池中没有剩余对象,新创建实例
obj = Object.Instantiate(prefab);
}
// 建立实例与预制体映射(如果新创建的对象)
prefabMap[obj] = prefab;
return obj;
}
/// <summary>回收对象实例,将其放回对象池。</summary>
public static void Recycle(GameObject obj)
{
if (obj == null) return;
int sceneId = SceneManager.GetActiveScene().buildIndex;
// 找到该对象对应的原始预制体
GameObject prefab;
if (!prefabMap.TryGetValue(obj, out prefab))
{
// 未找到映射,说明此对象不在池管理中,直接销毁
Object.Destroy(obj);
return;
}
// 将对象设置为非激活并放回池列表
obj.SetActive(false);
if (poolsByScene.ContainsKey(sceneId) && poolsByScene[sceneId].ContainsKey(prefab))
{
poolsByScene[sceneId][prefab].Add(obj);
}
else
{
// 理论上不会发生:如果当前场景无此池,则新建一个列表后加入
poolsByScene[sceneId] = poolsByScene[sceneId] ?? new Dictionary<GameObject, List<GameObject>>();
poolsByScene[sceneId][prefab] = poolsByScene[sceneId][prefab] ?? new List<GameObject>();
poolsByScene[sceneId][prefab].Add(obj);
}
}
/// <summary>场景卸载事件处理:清理该场景的所有对象池。</summary>
private static void OnSceneUnloaded(Scene scene)
{
int sceneId = scene.buildIndex;
if (!poolsByScene.ContainsKey(sceneId)) return;
// 遍历该场景的所有池,销毁其中的所有对象
foreach (var kv in poolsByScene[sceneId])
{
List<GameObject> poolList = kv.Value;
foreach (GameObject obj in poolList)
{
Object.Destroy(obj);
prefabMap.Remove(obj); // 从映射表移除
}
}
// 移除该场景的池字典条目,释放引用
poolsByScene.Remove(sceneId);
Debug.Log($"[ObjectPoolPro] 场景{sceneId}卸载,已清理相关对象池。");
}
}
实现解析: 上述代码将 ObjectPoolPro 修改为了 静态类(假定其在整个游戏生命周期常驻)。核心改动是在于引入了 poolsByScene
字典和 prefabMap
:
-
poolsByScene
:以场景ID为键,值为该场景的预制体池字典。每个预制体对应一个List<GameObject>
用于存放未使用的对象实例。我们使用场景的 BuildIndex 作为场景标识(当然也可以用场景名称字符串)。 -
prefabMap
:记录对象实例与其原始预制体的对应关系。因为在 Recycle 时,我们只有对象实例,需要知道应放回哪一个池。这可以通过预制体引用来定位池列表。我们在每次实例化对象时,都在prefabMap
中登记prefabMap[实例] = 预制体
。这样在回收时快速找到所属的预制体类型。如果发现回收的对象不在映射中(通常不应该发生),保险起见直接销毁处理。
RegisterPool
方法负责在当前场景中创建一个特定预制体的池,并可以预先生成一定数量的对象放入池中备用。Spawn
方法则尝试从池获取对象,没有可用对象时会实例化新的。需要注意的是,我们每次 Spawn 都调用 RegisterPool(prefab, 0)
来保证池存在(如果之前未注册过,会建立空池)。这样即使不显式注册,也能在第一次 Spawn 时自动初始化池。Recycle
方法将对象失活后放回池列表中,并保持映射关系不变,方便下次取用。
场景卸载的清理: 在静态构造函数中,我们订阅了 SceneManager.sceneUnloaded
事件。当某个场景卸载时,Unity会调用 OnSceneUnloaded(Scene scene)
。我们在该回调中取得卸载的场景ID,然后:
-
遍历对应场景的所有对象池列表,将里面的 GameObject 都销毁 (
Object.Destroy
),以释放场景占用的内存和资源。同时从prefabMap
中移除这些对象的映射条目。 -
将该场景的池字典从
poolsByScene
中移除,清空对这些对象和列表的引用。最终,该场景所有池就完全被清理了。
通过这一机制,场景A的对象不会“泄漏”到场景B中,每次切换场景都能确保上一个场景的池已彻底释放。此外,如果你的对象池管理器是一个挂载了 DontDestroyOnLoad
的对象,那么跨场景保留池管理代码依然有效,但实际对象会根据场景分类管理,不会混用。
使用说明: 使用场景隔离的对象池系统时,要稍微注意以下事项:
-
池注册: 建议在每个场景初始化时(例如
Start()
或场景管理脚本中)调用ObjectPoolPro.RegisterPool(prefab, initialCount)
为本场景所需的每种预制体创建对象池。这可以预加载对象,避免游戏过程中第一次生成对象的卡顿。如果忘记注册也没关系,首次Spawn
会自动注册池。 -
Spawn 和 Recycle: 和之前用法相同,直接使用
ObjectPoolPro.Spawn(prefab)
获取对象实例,用完后调用ObjectPoolPro.Recycle(obj)
回收即可。重要的是,不要在场景切换后继续使用旧场景的对象——本机制会在场景卸载时销毁它们。如果尝试访问,将发现对象已被销毁或不存在。一般情况下,场景卸载意味着旧场景对象用不到了,因此这不是问题。 -
场景索引匹配: 我们使用 BuildIndex 作为场景ID。如果你的项目没有在 Build Settings 中设置场景索引,或者需要使用场景名称,也可以改用
Scene.name
作为字典键。使用索引的好处是避免重名场景冲突,并且整数键效率更高。
小提示: 如果你的游戏使用**多场景加载(additive)**并行存在多个场景,也可以使用类似方式扩展。本例中假定每次只有一个活动场景,当卸载时清理对应池。如果同时有多个场景,你可以在 Spawn/Recycle 时使用对象所属场景作为键(例如 obj.scene.buildIndex
而不总是当前活动场景),并在 sceneUnloaded
回调中清理卸载场景。根据具体需求稍作调整即可。
通过以上三部分的实现,我们为 ObjectPoolPro 工具增添了强大的扩展功能。现在,它不仅能管理传统的 GameObject 实例池,还支持任意类型对象的复用(逻辑层对象池),可以通过组件自动管理对象的生命周期回收,并且在场景切换时自动隔离和清理,提高了资源管理的安全性。在实际项目中,这些改进将有效减少垃圾回收和内存泄漏风险,提升游戏运行效率和稳定性。希望这个进阶教程对你有所帮助,能够在 Unity 开发中更加得心应手地运用对象池优化游戏性能!
对象池由浅入深第一节:Unity对象池教程:原理与实践-CSDN博客
对象池由浅入深第二节:对象池由浅入深第二节:基于 UnityEngine.Pool 封装工具类 ObjectPoolPro-CSDN博客
对象池由浅入深第三节:对象池由浅入深第三节:基于 ObjectPoolPro 扩展 Unity 对象池--泛型对象池、自动回收和场景隔离-CSDN博客