Unity Term Book
进阶技术

Object Pooling

Object Pooling 复用实例,避免每次生成都调用 Instantiate/Destroy,从而在发射子弹、特效或敌群时消除 GC 尖峰与卡顿。这是 Unity 高频生成场景的经典优化模式,帮助移动端与主机项目稳住帧率,并与 Prefab、Addressables 协同管理对象的完整生命周期。

想象一下...

与其每次买新咖啡杯 → 用完 → 扔掉(Instantiate → 使用 → Destroy),餐厅用餐具池:备好 20 个杯子,取一个用,再放回架子。不多买、不乱扔——零 Garbage Collection。在 Unity 里,GC.Collect() 像高峰时段冲进厨房的清洁工——掉一帧。池化能阻止这件事。

概念详解

Instantiate/Destroy 的问题:每次 Instantiate() 都会分配新 GameObject、复制组件数据并注册到场景。 Destroy() 则把内存标给 GC。射击游戏每秒可生成 30–50 发子弹——频繁 GC,典型卡顿(每隔几秒“突然掉帧”)。

Object Pool 模式:加载时预生成固定数量。需要时从池中取出 (此前已停用)而不是新建。用完后禁用并归还。内存平稳,不触发 GC。

Unity 2021+ 起, UnityEngine.Pool 提供 ObjectPool<T>LinkedPool<T>GenericPool<T> ——内置,无需手写队列。回调:onCreateonGetonReleaseonDestroy(池满时)。

典型用途:子弹、命中/爆炸特效、飘字伤害、敌人/NPC 生成、脚步音源、UI toast、粒子实例。 每分钟生成不到 1 次的对象不必池化——管理开销不值得。

对象池如何工作

Get()
Pool→ dequeue inactive object → SetActive(true) → returnGame Object
Release()
Game Object→ Reset state → SetActive(false) → enqueue back to poolPool
Pool full?
Release()池已满(countAll == maxSize)→ Destroy(object) 或扩容 pool🗑 Destroy

动手步骤

1

创建 IPoolable 接口

OnGet()(从池取出时重置状态)与 OnRelease()(归还前清理)。

2

创建 ObjectPool

new ObjectPool<T>(onCreate, onGet, onRelease, onDestroy, collectionCheck, defaultCapacity, maxSize)——2021+ 内置。

3

使用 pool.Get() / pool.Release()

用 pool.Get() 替代 Instantiate,用 pool.Release() 替代 Destroy。在 onGet 中重置位置/速度/状态。

4

加载时预热池

游戏开始时生成并立即归还 N 个对象,避免空池导致首帧尖峰。

5

监控 pool.CountAll / pool.CountActive

打日志看池是否够大。countActive/countAll 偏高 → 提高 defaultCapacity。

交互模拟器

对比 Instantiate/Destroy 与 Object Pool——按 Fire 观察 GC 差异。

Pool: 5/12

Allocations

0

GC Events

0

Active Objects

0

代码示例

基础

用 Queue 实现简单子弹池——无需 Unity 包。

using UnityEngine;
using System.Collections.Generic;

public class BulletPool : MonoBehaviour
{
  public GameObject bulletPrefab;
  public int poolSize = 30;
  readonly Queue<GameObject> pool = new Queue<GameObject>();

  void Awake()
  {
      // Pre-warm: create and disable
      for (int i = 0; i < poolSize; i++)
      {
          var go = Instantiate(bulletPrefab);
          go.SetActive(false);
          go.transform.SetParent(transform);
          pool.Enqueue(go);
      }
  }

  public GameObject Get(Vector3 pos, Quaternion rot)
  {
      var go = pool.Count > 0
          ? pool.Dequeue()
          : Instantiate(bulletPrefab); // Expand if empty
      go.transform.SetPositionAndRotation(pos, rot);
      go.SetActive(true);
      return go;
  }

  public void Release(GameObject go)
  {
      go.SetActive(false);
      pool.Enqueue(go);
  }
}

代码示例

进阶

UnityEngine.Pool.ObjectPool<T>(Unity 2021+)——内置自动扩容与回调。

using UnityEngine;
using UnityEngine.Pool;

public class AdvancedBulletPool : MonoBehaviour
{
  public Bullet bulletPrefab;

  ObjectPool<Bullet> pool;

  void Awake()
  {
      pool = new ObjectPool<Bullet>(
          createFunc:   () => Instantiate(bulletPrefab, transform),
          actionOnGet:  b => { b.gameObject.SetActive(true); b.OnGet(); },
          actionOnRelease: b => { b.gameObject.SetActive(false); b.OnRelease(); },
          actionOnDestroy: b => Destroy(b.gameObject), // When the pool is full
          collectionCheck: false,  // Skip duplicate checks (production)
          defaultCapacity: 20,
          maxSize:         50
      );
  }

  public Bullet Spawn(Vector3 pos, Vector3 dir)
  {
      var bullet = pool.Get();
      bullet.transform.position = pos;
      bullet.Init(dir, pool); // Bullet calls pool.Release(this) when done
      return bullet;
  }

  public void LogStats()
  {
      Debug.Log($"Pool: {pool.CountActive} active / {pool.CountAll} total");
  }
}

📌 快速记忆

  • Pool = 复用对象,避免 GC 分配
  • 加载时预热,避免首帧尖峰
  • UnityEngine.Pool.ObjectPool<T>——2021+ 内置
  • 在 onGet 中重置状态,不要在 Update 里
  • collectionCheck=true 仅用于 Editor 调试

⚠️ 常见错误

  • ❌ 对已在池中的对象再次 Release(双重归还)

    归还两次 → 池损坏 → 闪烁或崩溃

    ✅ 开发时 collectionCheck=true;Release() 前做空值检查

  • ❌ Get 时未重置状态

    对象保留上次使用的速度、颜色、血量

    ✅ 在 actionOnGet 回调中完整重置