Unity Term Book
Advanced Techniques

Object Pooling

Object Pooling tái sử dụng instance thay vì Instantiate/Destroy mỗi lần spawn — loại bỏ GC spike và giật hình khi bắn đạn, FX hoặc sóng enemy dày đặc.

Hãy tưởng tượng...

Thay vì mỗi lần cần ly cà phê thì mua ly mới → dùng → vứt thùng rác (Instantiate → use → Destroy), nhà hàng dùng Pool rửa bát: có 20 ly sẵn, lấy một cái dùng, xong trả về tủ để dùng lại. Không bao giờ mua thêm, không bao giờ vứt — Zero Garbage Collection. Trong Unity, GC.Collect() là nhân viên dọn rác đột nhập vào bếp giữa giờ cao điểm — gây drop frame. Pool pattern ngăn điều đó xảy ra.

Khái niệm chi tiết

Instantiate/Destroy vấn đề: mỗi lần gọi Instantiate(), Unity cấp phát bộ nhớ mới cho GameObject, copy component data, đăng ký với scene. Destroy() đánh dấu bộ nhớ để GC thu hồi. Trong game bắn súng có thể spawn 30–50 viên đạn/giây — gây GC thường xuyên, dẫn đến stuttering đặc trưng (“lag đột ngột” mỗi vài giây).

Object Pool pattern: Pre-spawn một số lượng object cố định lúc khởi tạo game. Khi cần một object, lấy từ “pool” (deactivate trước đó) thay vì tạo mới. Khi xong, disable và trả về pool. Bộ nhớ không tăng thêm, GC không được trigger.

Từ Unity 2021+, namespace UnityEngine.Pool cung cấp ObjectPool<T> generic, LinkedPool<T>, GenericPool<T> — built-in, không cần implement thủ công. Có sẵn callbacks: onCreate, onGet, onRelease, onDestroy (khi pool đầy).

Các trường hợp dùng pool: đạn, hiệu ứng hit/explosion, damage numbers (floating text), spawning enemy/NPC, footstep audio sources, UI notification toasts, particle system instances. Cân nhắc không dùng pool cho object quá hiếm (spawn <1/phút) vì overhead management không đáng.

Sơ đồ Pool hoạt động

Get()
Pool→ dequeue inactive object → SetActive(true) → returnGame Object
Release()
Game Object→ Reset state → SetActive(false) → enqueue back to poolPool
Pool full?
Release()pool đã đầy (countAll == maxSize) → Destroy(object) hoặc expand pool🗑 Destroy

Hướng dẫn thực hành

1

Tạo IPoolable interface

Tạo interface với OnGet() (reset state khi lấy từ pool) và OnRelease() (cleanup trước khi trả về pool).

2

Khởi tạo ObjectPool

new ObjectPool<T>(onCreate, onGet, onRelease, onDestroy, collectionCheck, defaultCapacity, maxSize) — Unity built-in từ 2021+.

3

Dùng pool.Get() / pool.Release()

Thay Instantiate bằng pool.Get(), thay Destroy bằng pool.Release(). Reset position/velocity/state trong onGet callback.

4

Pre-warm pool lúc loading

Spawn và release ngay lập tức N object lúc game start để tránh spike frame đầu tiên khi pool còn trống.

5

Monitor qua pool.CountAll / pool.CountActive

Log pool stats để biết pool đủ lớn chưa. countActive/countAll cao = cần tăng defaultCapacity.

Trình mô phỏng tương tác

So sánh hiệu năng Instantiate/Destroy vs Object Pool — nhấn Bắn để thấy sự khác biệt GC.

Pool: 5/12

Allocations

0

GC Events

0

Active Objects

0

Ví dụ Code

Cơ bản

Pool đạn đơn giản dùng Queue — triển khai thủ công không cần Unity package.

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: tạo sẵn và 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);
  }
}

Ví dụ Code

Nâng cao

Dùng UnityEngine.Pool.ObjectPool<T> (Unity 2021+) — built-in, có auto-expand và callbacks.

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), // Khi pool đầy
          collectionCheck: false,  // Tắt kiểm tra duplicate (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 tự gọi pool.Release(this) khi xong
      return bullet;
  }

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

📌 Ghi nhớ nhanh

  • Pool = reuse object, tránh GC allocation
  • Pre-warm pool lúc loading để tránh spike frame 1
  • UnityEngine.Pool.ObjectPool<T> — built-in từ 2021+
  • Reset state trong onGet callback, không trong Update
  • collectionCheck=true chỉ trong Editor để debug

⚠️ Lỗi thường gặp

  • ❌ Release object đang active (double release)

    Object bị release 2 lần → pool corrupt → flicker hoặc crash

    ✅ Dùng collectionCheck=true trong dev; null guard trước Release()

  • ❌ Không reset state khi Get

    Object mang state cũ: velocity, color, health từ lần dùng trước

    ✅ Reset hoàn toàn trong actionOnGet callback