LOADING

加载过慢请开启缓存 浏览器默认开启

Unity 常用面向对象设计模式系列(4):对象池模式(Object Pool)


Unity 常用面向对象设计模式系列(4):对象池模式(Object Pool)


引言

在 Unity 项目中,频繁的 InstantiateDestroy 操作会带来严重的性能开销和 GC 压力,尤其是在子弹特效、爆炸效果、敌人复活等高频率创建/销毁场景下。对象池模式(Object Pool)通过重用对象,避免重复分配和销毁,能显著提升帧率稳定性和内存效率。本篇将从模式原理、Unity 实战、泛型实现、高级特性与常见陷阱,全面剖析如何在项目中落地高性能、可维护的对象池系统。


一、对象池模式核心原理

  • 意图
    重用已创建的对象实例,避免高频率的分配与回收开销。

  • 结构

    • 池(Pool):维护一组可复用对象的集合(通常是 Queue<T>Stack<T>)。
    • 获取(Acquire/Get):从池中取出一个对象,若池空则新创建;
    • 归还(Release/Return):将对象重置状态后放回池中,供下次复用。

ObjectPool

Unity 实战示例:子弹对象池

2.1 定义可池化接口

public interface IPoolable {
    /// <summary>每次从池中获取时调用,重置对象状态。</summary>
    void OnAcquire();

    /// <summary>每次归还给池时调用,清理对象状态。</summary>
    void OnRelease();
}

2.2 通用对象池实现

using System.Collections.Generic;
using UnityEngine;

public class ObjectPool<T> where T : MonoBehaviour, IPoolable {
    private readonly T prefab;
    private readonly Transform  parent;
    private readonly Stack<T>   pool;
    private readonly int      maxSize;

    public ObjectPool(T prefab, int initialSize = 10, int maxSize = 100, Transform parent = null) {
        this.prefab   = prefab;
        this.maxSize  = maxSize;
        this.parent   = parent;
        pool = new Stack<T>(initialSize);
        // 预热
        for (int i = 0; i < initialSize; i++) {
            var obj = CreateNew();
            pool.Push(obj);
        }
    }

    private T CreateNew() {
        var go = Object.Instantiate(prefab, parent);
        go.gameObject.SetActive(false);
        return go;
    }

    /// <summary>获取一个对象实例</summary>
    public T Get() {
        T obj = pool.Count > 0 ? pool.Pop() : CreateNew();
        obj.gameObject.SetActive(true);
        obj.OnAcquire();
        return obj;
    }

    /// <summary>将实例归还到池中</summary>
    public void Release(T obj) {
        if (pool.Count >= maxSize) {
            Object.Destroy(obj.gameObject);
            return;
        }
        obj.OnRelease();
        obj.gameObject.SetActive(false);
        pool.Push(obj);
    }
}

2.3 在子弹脚本中使用

public class Bullet : MonoBehaviour, IPoolable {
    public float speed = 20f;
    public Rigidbody rb;
    private ObjectPool<Bullet> pool;

    void Awake() {
        rb = GetComponent<Rigidbody>();
        // 假设 BulletPool 在场景中初始化后赋值
    }

    public void Initialize(ObjectPool<Bullet> pool) {
        this.pool = pool;
    }

    public void Fire(Vector3 pos, Vector3 dir) {
        transform.position = pos;
        rb.velocity = dir.normalized * speed;
    }

    void OnCollisionEnter(Collision col) {
        // 碰撞后归还
        pool.Release(this);
    }

    public void OnAcquire() {
        // 重置物理状态
        rb.velocity = Vector3.zero;
        rb.angularVelocity = Vector3.zero;
    }

    public void OnRelease() {
        // 可添加粒子特效、计数逻辑等
    }
}

2.4 管理池的初始化

public class BulletPoolManager : MonoBehaviour {
    [SerializeField] private Bullet bulletPrefab;
    public static ObjectPool<Bullet> Pool { get; private set; }

    void Awake() {
        Pool = new ObjectPool<Bullet>(
            prefab: bulletPrefab,
            initialSize: 20,
            maxSize: 200,
            parent: transform
        );
        // 将 pool 注入到子弹自身
        foreach (var b in GetComponentsInChildren<Bullet>()) {
            b.Initialize(Pool);
        }
    }
}

2.5 在武器脚本中获取子弹

public class PlayerWeapon : MonoBehaviour {
    public Transform muzzle;
    public float    fireRate = 10f;
    private float   timer = 0f;

    void Update() {
        timer += Time.deltaTime;
        if (timer >= 1f / fireRate && Input.GetButton("Fire1")) {
            timer = 0f;
            var bullet = BulletPoolManager.Pool.Get();
            bullet.Initialize(BulletPoolManager.Pool);
            bullet.Fire(muzzle.position, transform.forward);
        }
    }
}

三、高级特性与扩展

3.1 池预热与动态扩容

  • 预热(Warm-up):在场景加载时提前创建一定数量实例,避免运行时卡顿;
  • 动态扩容:当池耗尽时允许创建新对象,但应限制最大容量,防止无控制增长。

3.2 多类型池管理器

public class PoolManager : MonoBehaviour {
    private static readonly Dictionary<string, object> pools = new();

    public static ObjectPool<T> CreatePool<T>(T prefab, int init, int max, Transform parent = null)
        where T : MonoBehaviour, IPoolable
    {
        var key = prefab.name;
        if (!pools.TryGetValue(key, out var existing)) {
            var pool = new ObjectPool<T>(prefab, init, max, parent);
            pools[key] = pool;
            return pool;
        }
        return (ObjectPool<T>) existing;
    }
}

3.3 多线程安全

  • Unity 主线程应用时无需同步;
  • 若在 Worker Th
  • read 或 Job System 中需要访问池,可用 ConcurrentStack<T> 并加 lock

3.4 池中对象生命周期管理

  • 确保 Scene Unload 时清空池:

    void OnDestroy() {
        foreach (var obj in poolContents) Destroy(obj.gameObject);
    }
    
  • 对跨场景持久化的池使用 DontDestroyOnLoad 管理。


四、性能测量与调优

  1. Profiler → CPU Usage
    • 关注 InstantiateDestroy 调用次数;
  2. GC Alloc Tracker
    • 确保使用池后,GC Alloc 降至近零;
  3. Frame Time
    • 高频生成场景 FPS 平稳性显著提升;
  4. 内存快照
    • 检查池中未释放的对象,防止内存泄漏。

五、注意事项

场景 陷阱 建议
状态残留 对象归还前未重置位置、缩放、事件监听等导致下次行为异常 OnRelease 中完整重置所有状态;
过度预热 预热过多实例浪费内存 根据实际最大并发需求设置初始容量;
忘记归还 忘记调用 Release 导致池耗尽或对象泄漏 强制组件在 OnDisable 或碰撞回调中归还;
多场景管理 池挂在场景中,场景切换导致丢失或重复创建 使用 DontDestroyOnLoad 持久化,或集中在启动场景初始化;
接口耦合 客户端直接依赖 ObjectPool<T>,难以替换或测试 对池进行封装,依赖 IPool<T> 接口,支持 Mock ;

六、小结

  1. 职责单一:对象池只做获取与归还,初始化逻辑放在专用管理器或初始化器中;
  2. 状态复位:实现 IPoolable 接口,确保每次获取和归还时状态一致;
  3. 预热与容量控制:根据场景需求平衡启动卡顿与运行时性能;
  4. 集中管理:使用 PoolManager 统一创建与访问,便于多类型池扩展;
  5. 接口抽象:客户端依赖 IPool<T> 而非具体实现,便于单元测试与动态替换;