PoolMaster is a high-performance, production-ready object pooling system for Unity. Designed for both 2D and 3D games, it provides a comprehensive solution for managing GameObject lifecycles efficiently, reducing GC pressure, and maximizing runtime performance.
For the fastest setup, start with the No‑Code Quick Start: Documentation/no-code-quick-start.md
- Zero-allocation pooling — minimal GC, fast hot paths
- Type-safe API — generic Pool with safety
- Batch ops — spawn/despawn many at once
- Command buffers — thread-safe enqueue, main-thread flush
- Diagnostics — real-time metrics + editor window
- Configurable — precise control over pool behavior
- Events — decoupled, opt-in hooks
- Collection pooling — reuse lists/dicts/sets
- Easy integration — IPoolable + helpers
- Supported Unity: 6.0 – 6.4 (stable)
- Render Pipelines: Built-in, URP, HDRP
- Add me on Discord: misty2023
- Open Package Manager (
Window > Package Manager) - Click
+and selectAdd package from git URL - Enter:
https://github.com/mistyuk/PoolMaster.git
- Download the latest release from GitHub
- Extract to your
Assets/Plugins/PoolMasterfolder
Choose your path: No code setup or full API control.
Perfect for beginners or rapid prototyping. Zero programming required.
- Hierarchy → Right-click → Create Empty → Name it
PoolMaster - Add Component →
PoolMaster Manager
- Select
PoolMaster→ Inspector → Add New Pool - Drag your prefab → Set Prewarm Amount =
10
- Create empty GameObject → Name it
Spawner - Add Component →
PoolMaster Spawner - Drag prefab → Spawn On =
On Start
- Select your prefab (in Project) → Add Component →
PoolMaster Return To Pool - Return Condition =
After Time→ Lifetime =2seconds
Press Play - Objects spawn, live 2 seconds, return to pool automatically.
Full no-code guide: See Documentation/no-code-quick-start.md
Full control for advanced users. Type-safe, high-performance pooling.
using UnityEngine;
using PoolMaster;
public class BulletSpawner : MonoBehaviour
{
[SerializeField] private GameObject bulletPrefab;
void Start()
{
var request = new PoolRequest
{
prefab = bulletPrefab,
initialPoolSize = 20,
shouldPrewarm = true
};
PoolingManager.Instance.GetOrCreatePool<Bullet>(request);
}
}void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
var bullet = PoolingManager.Instance.Spawn(
bulletPrefab,
transform.position,
Quaternion.identity
);
}
}using UnityEngine;
using PoolMaster;
public class Bullet : MonoBehaviour, IPoolable
{
public IPool ParentPool { get; set; }
public bool IsPooled { get; private set; }
public void OnSpawned()
{
IsPooled = true;
// Unity 6 renamed Rigidbody.velocity to .linearVelocity. PoolMaster targets
// Unity 6.0–6.4 (see package.json) so the new name is required.
GetComponent<Rigidbody>().linearVelocity = transform.forward * 20f;
}
public void OnDespawned()
{
IsPooled = false;
}
public void PoolReset()
{
GetComponent<Rigidbody>().linearVelocity = Vector3.zero;
}
void OnCollisionEnter(Collision collision)
{
ParentPool?.Despawn(gameObject);
}
}That's it. Spawn uses the pool, collision returns to pool.
Central singleton for managing all pools.
// Get or create a pool
var pool = PoolingManager.Instance.GetOrCreatePool<T>(request);
// Spawn objects
GameObject obj = PoolingManager.Instance.Spawn(prefab, position, rotation);
GameObject obj = PoolingManager.Instance.Spawn(prefab); // At origin
// Despawn objects
PoolingManager.Instance.Despawn(obj);
// Batch operations — SpawnBatch is an extension method on IPoolControl,
// not a method on PoolingManager. Get the pool first, then call it.
var bulletPool = (IPoolControl)PoolingManager.Instance.GetOrCreatePool<Bullet>(request);
Vector3[] positions = { new Vector3(0, 0, 0), new Vector3(1, 0, 0) };
Quaternion[] rotations = { Quaternion.identity, Quaternion.identity };
int count = bulletPool.SpawnBatch(positions, rotations, parent: null);
// Look up pool by string ID (the one you set in request.poolId).
// By-prefab lookup uses TryGetPool(GameObject, out IPool) instead.
IPool pool = PoolingManager.Instance.GetPool("Bullets");
PoolingManager.Instance.TryGetPool(prefab, out IPool poolByPrefab);
// Global snapshot
PoolSnapshot snapshot = PoolingManager.Instance.GetSnapshot();Generic type-safe pool implementation.
// Create pool directly
var pool = new Pool<Bullet>(
prefab,
request,
poolParent,
poolId
);
// Pool operations
GameObject obj = pool.Spawn(position, rotation, parent);
bool success = pool.Despawn(obj);
pool.PrewarmPool(count);
pool.Clear();
pool.DestroyPool();
// Pool info
int active = pool.ActiveCount;
int inactive = pool.InactiveCount;
int total = pool.Capacity;
PoolMetrics metrics = pool.Metrics;Configuration for pool creation.
var request = new PoolRequest
{
prefab = bulletPrefab,
poolId = "Bullets", // Optional ID for GetPool("Bullets") lookup
initialPoolSize = 50, // Pre-instantiated count at startup
shouldPrewarm = true, // Create initialPoolSize objects immediately
maxPoolSize = 200, // Max instances kept around
allowDynamicExpansion = true, // Grow past maxPoolSize when exhausted
cullExcessObjects = true, // Destroy excess instances when inactive cache > maxPoolSize
initializationTiming = PoolInitializationTiming.OnAwake,
usePoolContainer = true, // Group spawned instances under a named child of PoolingManager
containerName = "Bullet Pool",
category = "Combat" // Optional tag for grouping in diagnostics
};Non-generic pool implementation. Use when the prefab is plain geometry / particles without a pooling component.
// Plain prefab — no PoolableMonoBehaviour, no IPoolable, no scripts.
var request = PoolRequest.Create(plainPrefab, initialSize: 10);
IPool pool = PoolingManager.Instance.GetOrCreateGameObjectPool(plainPrefab, request);
// Same API as Pool<T> from the consumer side
GameObject obj = pool.Spawn(position, rotation, parent);
obj.ReturnToPool();PoolingManager.Spawn(prefab, …) automatically falls back to a GameObjectPool
when the prefab has no IPoolable — previously this was a logged error. If the
prefab does implement IPoolable, GetOrCreateGameObjectPool routes to the
type-safe Pool<T> path so behavior stays consistent.
Force-despawns every active instance, destroys all inactive instances, then
optionally re-prewarms to the original initialPoolSize. Use after modifying
the source prefab at runtime so existing pooled clones get replaced with fresh
ones on the next Spawn.
// Flush + re-prewarm to initialPoolSize
pool.Reseed(rePrewarm: true);
// Flush only — next Spawn will lazily instantiate from the updated prefab
pool.Reseed(rePrewarm: false);Also surfaced as a per-pool button in Window → PoolMaster → Diagnostics.
- Global: Clear All Inactive, Cull Unused (60s)
- Per pool: Clear Inactive, Shrink to 4, Reseed, Destroy
- Singleton resurrection during
OnApplicationQuitblocked;Instancereturnsnullinstead of auto-creating in non-Play mode. [RuntimeInitializeOnLoadMethod(SubsystemRegistration)]resets statics on domain reload — Enter Play Mode without Domain Reload now works correctly.PoolingManager.AddPresetvalidates withPoolRequest.IsValid()and dedups against both live pools and pending presets.PoolingManager.GetAllPoolMetrics/GetSnapshotkey results byPoolIdinstead ofprefab.name— avoids collisions when two prefabs share a name.
Static utility for pooling collections.
// Lists
var list = CollectionPool.GetList<GameObject>();
// ... use list ...
CollectionPool.Return(list);
// HashSets
var set = CollectionPool.GetHashSet<int>();
CollectionPool.Return(set);
// Dictionaries
var dict = CollectionPool.GetDictionary<string, GameObject>();
CollectionPool.Return(dict);// GameObject extension — finds the parent pool via PooledMarker and returns the instance.
gameObject.ReturnToPool();
// Batch spawning is on IPoolControl, not PoolingManager. Cast the pool, then call.
var ctrl = (IPoolControl)pool;
ctrl.SpawnBatch(positions, rotations, parent: null); // arrays of Vector3 and Quaternion
ctrl.SpawnGrid(center, spacing: 0.5f, gridSize: 8); // lays out gridSize² instances in a square grid// Pool lifecycle — all three always available.
PoolingEvents.OnPoolCreated += (poolId, prefab) => {}; // Action<string, GameObject>
PoolingEvents.OnPoolDestroyed += (poolId, prefab) => {}; // Action<string, GameObject>
PoolingEvents.OnPoolPrewarmed += (poolId, count) => {}; // Action<string, int>
// Object creation — always available.
PoolingEvents.OnObjectCreated += (obj, poolId) => {}; // Action<GameObject, string>
// Per-spawn / per-despawn events — guarded by the ENABLE_POOL_LOGS compile define
// to avoid per-call delegate dispatch in release builds. Add ENABLE_POOL_LOGS to
// your Scripting Define Symbols if you need them.
#if ENABLE_POOL_LOGS
PoolingEvents.OnObjectSpawned += (obj, poolId) => {};
PoolingEvents.OnObjectDespawned += (obj, poolId) => {};
#endif
// Performance
PoolingEvents.OnPoolExpanded += (poolId, oldSize, newSize) => {}; // Action<string, int, int>
PoolingEvents.OnPoolCulled += (poolId, objectsCulled) => {}; // Action<string, int>
PoolingEvents.OnPoolExhausted += (poolId, maxSize) => {}; // Action<string, int>Performance comparison vs traditional Instantiate/Destroy:
| Operation | Instantiate/Destroy | PoolMaster | Improvement |
|---|---|---|---|
| Spawn Single Object | ~0.8ms | ~0.002ms | 400x faster |
| Spawn 100 Objects | ~80ms | ~0.2ms | 400x faster |
| Despawn Single Object | ~0.3ms | ~0.001ms | 300x faster |
| GC Allocations (1000 cycles) | ~120 MB | ~0.5 MB | 240x less |
| Frame time impact (60fps) | 5-15ms spikes | <0.1ms | No hitching |
| Batch Size | Individual Spawn | Batch Spawn | Improvement |
|---|---|---|---|
| 10 objects | ~0.02ms | ~0.008ms | 2.5x faster |
| 50 objects | ~0.1ms | ~0.03ms | 3.3x faster |
| 100 objects | ~0.2ms | ~0.05ms | 4x faster |
| 500 objects | ~1.0ms | ~0.2ms | 5x faster |
Benchmarks run on Unity 6000.0.62f1 LTS, Intel i9-13950HX, 32GB RAM DDR5 5200mhz, RTX 4070 mGPU
For thread-safe enqueueing from Jobs or background threads:
// Get command buffer for a pool (looked up by poolId)
var buffer = PoolingManager.Instance.GetCommandBuffer("Bullets");
// Enqueue spawn from a background thread / Job — thread-safe.
buffer.EnqueueSpawn(position, rotation, parent: null);
// Enqueue a batch from a background thread.
buffer.EnqueueSpawnBatch(positions, rotations, parent: null);
// Commands are flushed each frame in PoolingManager.LateUpdate on the main thread.
// All spawning / despawning happens there — your background thread never touches
// Unity APIs directly.// Get metrics
PoolMetrics metrics = pool.Metrics;
Debug.Log($"Total Spawned: {metrics.TotalSpawned}");
Debug.Log($"Total Despawned: {metrics.TotalDespawned}");
Debug.Log($"Reuse Efficiency: {metrics.ReuseEfficiency:P}");
Debug.Log($"Current Active: {metrics.CurrentActive}");
Debug.Log($"Expansion Count: {metrics.ExpansionCount}");
// Open diagnostics window
// Window > PoolMaster > Diagnostics// 1. On Awake (before scene starts)
var request = new PoolRequest
{
prefab = prefab,
initialPoolSize = 50,
shouldPrewarm = true,
initializationTiming = PoolInitializationTiming.OnAwake
};
// 2. On Start (after scene initialized)
request.initializationTiming = PoolInitializationTiming.OnStart;
// 3. Lazy (only when first needed)
request.initializationTiming = PoolInitializationTiming.Lazy;
// 4. Next frame (avoid loading hitches)
request.initializationTiming = PoolInitializationTiming.NextFrame;
// 5. On event (custom timing) — pools wait until you fire their event.
request.initializationTiming = PoolInitializationTiming.OnEvent;
request.eventId = "level_loaded";
PoolingManager.Instance.AddPreset(request);
// Later, when the event fires:
PoolingManager.Instance.TriggerBootstrap("level_loaded");Use the included PooledVfx component for automatic particle system management:
public class PooledVfx : PoolableMonoBehaviour
{
[SerializeField] private bool autoReturnWhenFinished = true;
[SerializeField] private float maxLifetime = 10f;
// Automatically returns to pool when particles finish
}if (pool is IPoolControl poolControl)
{
// Lifecycle / capacity management
poolControl.PrewarmPool(count); // create N inactive instances
poolControl.Clear(); // destroy all inactive (keeps active)
poolControl.ShrinkInactive(targetInactive: 4); // trim cached inactive down to N
poolControl.Reseed(rePrewarm: true); // force-flush + rebuild (added v1.0.2)
poolControl.DestroyPool(); // destroy everything; pool is then invalid
// Batch spawn — extension methods on IPoolControl
poolControl.SpawnBatch(positions, rotations, parent: null);
poolControl.SpawnGrid(center, spacing: 0.5f, gridSize: 8);
// Diagnostics
PoolMetrics m = poolControl.Metrics;
int total = poolControl.TotalCount;
}Add ENABLE_POOL_LOGS to your Scripting Define Symbols:
Edit > Project Settings > Player > Other Settings- Add
ENABLE_POOL_LOGSto Scripting Define Symbols - Logs will completely compile out when the symbol is removed (zero overhead)
PoolMaster uses assembly definitions for clean separation:
PoolMaster- Core runtime assemblyPoolMaster.Editor- Editor-only tools
To reference PoolMaster in your code, add PoolMaster to your assembly definition references.
// Before (Unity ObjectPool)
using UnityEngine.Pool;
var pool = new ObjectPool<GameObject>(
createFunc: () => Instantiate(prefab),
actionOnGet: (obj) => obj.SetActive(true),
actionOnRelease: (obj) => obj.SetActive(false),
actionOnDestroy: (obj) => Destroy(obj),
collectionCheck: true,
defaultCapacity: 20,
maxSize: 100
);
var obj = pool.Get();
pool.Release(obj);
// After (PoolMaster)
using PoolMaster;
var request = new PoolRequest
{
prefab = prefab,
initialPoolSize = 20,
maxPoolSize = 100,
shouldPrewarm = true
};
PoolingManager.Instance.GetOrCreatePool<MyComponent>(request);
var obj = PoolingManager.Instance.Spawn(prefab, position, rotation);
PoolingManager.Instance.Despawn(obj);// Before (Manual pooling)
Queue<GameObject> pool = new Queue<GameObject>();
GameObject Spawn()
{
if (pool.Count > 0)
{
var obj = pool.Dequeue();
obj.SetActive(true);
return obj;
}
return Instantiate(prefab);
}
void Despawn(GameObject obj)
{
obj.SetActive(false);
pool.Enqueue(obj);
}
// After (PoolMaster)
using PoolMaster;
// One-time setup
var request = new PoolRequest { prefab = prefab, initialPoolSize = 10 };
PoolingManager.Instance.GetOrCreatePool<MyComponent>(request);
// Use anywhere
var obj = PoolingManager.Instance.Spawn(prefab, position, rotation);
obj.ReturnToPool(); // Extension method- Always implement IPoolable - Even if empty, it ensures proper lifecycle hooks
- Use PoolableMonoBehaviour - Handles common cleanup patterns automatically
- Pre-warm pools on load - Avoid runtime hitches with
shouldPrewarm = true - Set reasonable max sizes - Use
maxPoolSizeto prevent unbounded memory growth - Enable culling - Use
cullExcessObjects = trueto manage memory - Use batch operations - Spawn/despawn multiple objects in one call when possible
- Profile your pools - Use the diagnostics window to optimize pool sizes
- Disable logs in production - Remove
ENABLE_POOL_LOGSfor zero logging overhead
Q: Can I use PoolMaster with addressables?
A: Yes! Pass the loaded addressable as the prefab parameter.
Q: Does it work with nested prefabs?
A: Yes, PoolMaster handles any GameObject prefab regardless of hierarchy.
Q: What about pooling across scene loads?
A: PoolingManager persists across scenes with DontDestroyOnLoad. Pools survive scene transitions.
Q: Can I have multiple pools for the same prefab?
A: Yes, provide unique poolId parameters when creating pools.
Q: Is it thread-safe?
A: Core pooling must happen on the main thread, but use PoolCommandBuffer for thread-safe enqueueing.
Q: Does it work with ECS/DOTS?
A: PoolMaster is designed for GameObject-based workflows. For DOTS, use Unity's native entity pooling.
MIT License - see LICENSE file for details.
- Enable repo hooks:
git config core.hooksPath .githooks - Verify CSharpier:
csharpier --version(required for formatting) - Format manually if needed:
csharpier format .
See full dev setup: DEVELOPING.md
- Issues: GitHub Issues
- Discussions: GitHub Discussions
- Discord: misty2023
If PoolMaster helps your project, consider giving it a star on GitHub!