对象池由浅入深第三节:基于 ObjectPoolPro 扩展 Unity 对象池--泛型对象池、自动回收和场景隔离

 对象池由浅入深第一节: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秒自动回收。我们可以这样设置:

  1. ExplosionPrefab 上添加 AutoRecycle 组件,将 Recycle After Time 设为 2。

  2. 使用对象池生成爆炸特效:调用 ObjectPoolPro.Spawn(explosionPrefab) 来获取实例。特效播放开始计时,两秒后 AutoRecycle 协程会自动将其失活并回收。

  3. 若在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博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

吉良吉影NeKoSuKi

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值