Install
$ agentstack add skill-tjboudreaux-cc-plugin-unity-gamedev-tools-unity-object-pooling ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ● Shell / process execution Used
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Unity Object Pooling
Overview
Object pooling reuses GameObjects instead of instantiating/destroying them, reducing GC pressure and improving performance.
When to Use
- Frequently spawned objects (projectiles, particles, enemies)
- UI list items
- Audio sources
- Network messages
- Any high-frequency instantiation
Basic Object Pool
Generic Pool Implementation
public class ObjectPool where T : class
{
private readonly Stack _pool = new();
private readonly Func _createFunc;
private readonly Action _onGet;
private readonly Action _onRelease;
private readonly Action _onDestroy;
private readonly int _maxSize;
public int CountActive { get; private set; }
public int CountInactive => _pool.Count;
public int CountAll => CountActive + CountInactive;
public ObjectPool(
Func createFunc,
Action onGet = null,
Action onRelease = null,
Action onDestroy = null,
int defaultCapacity = 10,
int maxSize = 1000)
{
_createFunc = createFunc ?? throw new ArgumentNullException(nameof(createFunc));
_onGet = onGet;
_onRelease = onRelease;
_onDestroy = onDestroy;
_maxSize = maxSize;
// Pre-warm pool
for (int i = 0; i 0)
{
obj = _pool.Pop();
}
else
{
obj = _createFunc();
}
_onGet?.Invoke(obj);
CountActive++;
return obj;
}
public void Release(T obj)
{
if (obj == null) return;
_onRelease?.Invoke(obj);
CountActive--;
if (_pool.Count 0)
{
var obj = _pool.Pop();
_onDestroy?.Invoke(obj);
}
CountActive = 0;
}
}
Unity Built-in Pool (2021+)
using UnityEngine.Pool;
public class ProjectileSpawner : MonoBehaviour
{
[SerializeField] private Projectile _prefab;
private ObjectPool _pool;
private void Awake()
{
_pool = new ObjectPool(
createFunc: CreateProjectile,
actionOnGet: OnGetProjectile,
actionOnRelease: OnReleaseProjectile,
actionOnDestroy: OnDestroyProjectile,
collectionCheck: true,
defaultCapacity: 20,
maxSize: 100
);
}
private Projectile CreateProjectile()
{
var proj = Instantiate(_prefab);
proj.SetPool(_pool);
return proj;
}
private void OnGetProjectile(Projectile proj)
{
proj.gameObject.SetActive(true);
}
private void OnReleaseProjectile(Projectile proj)
{
proj.gameObject.SetActive(false);
}
private void OnDestroyProjectile(Projectile proj)
{
Destroy(proj.gameObject);
}
public Projectile Spawn(Vector3 position, Quaternion rotation)
{
var proj = _pool.Get();
proj.transform.SetPositionAndRotation(position, rotation);
return proj;
}
}
Pooled Object Base Class
public abstract class PooledObject : MonoBehaviour where T : PooledObject
{
private IObjectPool _pool;
public void SetPool(IObjectPool pool)
{
_pool = pool;
}
public void ReturnToPool()
{
if (_pool != null)
{
_pool.Release((T)this);
}
else
{
Destroy(gameObject);
}
}
public virtual void OnSpawn() { }
public virtual void OnDespawn() { }
}
// Usage
public class Projectile : PooledObject
{
[SerializeField] private float _lifetime = 5f;
private float _spawnTime;
public override void OnSpawn()
{
_spawnTime = Time.time;
}
public override void OnDespawn()
{
// Reset state
}
private void Update()
{
if (Time.time - _spawnTime > _lifetime)
{
ReturnToPool();
}
}
}
Multi-Prefab Pool
Pool Manager
public class PoolManager : MonoBehaviour
{
public static PoolManager Instance { get; private set; }
private readonly Dictionary> _pools = new();
private void Awake()
{
Instance = this;
}
public GameObject Spawn(GameObject prefab, Vector3 position, Quaternion rotation)
{
if (!_pools.TryGetValue(prefab, out var pool))
{
pool = CreatePool(prefab);
_pools[prefab] = pool;
}
var obj = pool.Get();
obj.transform.SetPositionAndRotation(position, rotation);
return obj;
}
public void Despawn(GameObject obj, GameObject prefab)
{
if (_pools.TryGetValue(prefab, out var pool))
{
pool.Release(obj);
}
else
{
Destroy(obj);
}
}
public void PrewarmPool(GameObject prefab, int count)
{
if (!_pools.ContainsKey(prefab))
{
var pool = CreatePool(prefab, count);
_pools[prefab] = pool;
}
}
private ObjectPool CreatePool(GameObject prefab, int initialSize = 10)
{
Transform poolParent = new GameObject($"Pool_{prefab.name}").transform;
poolParent.SetParent(transform);
return new ObjectPool(
createFunc: () =>
{
var obj = Instantiate(prefab, poolParent);
obj.SetActive(false);
return obj;
},
actionOnGet: obj => obj.SetActive(true),
actionOnRelease: obj => obj.SetActive(false),
actionOnDestroy: obj => Destroy(obj),
defaultCapacity: initialSize,
maxSize: 200
);
}
}
Pooled Spawn Extension
public static class PoolExtensions
{
public static GameObject SpawnPooled(
this GameObject prefab,
Vector3 position,
Quaternion rotation)
{
return PoolManager.Instance.Spawn(prefab, position, rotation);
}
public static void DespawnPooled(this GameObject obj, GameObject prefab)
{
PoolManager.Instance.Despawn(obj, prefab);
}
}
// Usage
var enemy = _enemyPrefab.SpawnPooled(spawnPoint, Quaternion.identity);
// Later...
enemy.DespawnPooled(_enemyPrefab);
Component Pool
Component-Specific Pool
public class ComponentPool where T : Component
{
private readonly ObjectPool _pool;
private readonly Transform _parent;
public ComponentPool(T prefab, Transform parent, int initialSize = 10)
{
_parent = parent;
_pool = new ObjectPool(
createFunc: () =>
{
var obj = UnityEngine.Object.Instantiate(prefab, parent);
obj.gameObject.SetActive(false);
return obj;
},
actionOnGet: c => c.gameObject.SetActive(true),
actionOnRelease: c =>
{
c.gameObject.SetActive(false);
c.transform.SetParent(_parent);
},
actionOnDestroy: c => UnityEngine.Object.Destroy(c.gameObject),
defaultCapacity: initialSize
);
}
public T Get() => _pool.Get();
public void Release(T component) => _pool.Release(component);
}
// Usage for UI
public class DamageNumberPool : MonoBehaviour
{
[SerializeField] private DamageNumber _prefab;
private ComponentPool _pool;
private void Awake()
{
_pool = new ComponentPool(_prefab, transform, 20);
}
public DamageNumber Show(Vector3 worldPos, int damage)
{
var number = _pool.Get();
number.Initialize(worldPos, damage, () => _pool.Release(number));
return number;
}
}
List/Collection Pooling
List Pool
public static class ListPool
{
private static readonly ObjectPool> s_Pool = new(
createFunc: () => new List(),
actionOnRelease: list => list.Clear(),
defaultCapacity: 10,
maxSize: 100
);
public static List Get() => s_Pool.Get();
public static void Release(List list)
{
if (list != null)
s_Pool.Release(list);
}
// Disposable wrapper
public static PooledList GetDisposable()
{
return new PooledList(s_Pool.Get());
}
}
public struct PooledList : IDisposable
{
public List List { get; }
public PooledList(List list)
{
List = list;
}
public void Dispose()
{
ListPool.Release(List);
}
}
// Usage
using (var pooledList = ListPool.GetDisposable())
{
GetEnemiesInRange(pooledList.List);
foreach (var enemy in pooledList.List)
{
// Process
}
} // Automatically returned to pool
StringBuilder Pool
public static class StringBuilderPool
{
private static readonly ObjectPool s_Pool = new(
createFunc: () => new StringBuilder(256),
actionOnRelease: sb => sb.Clear(),
defaultCapacity: 5,
maxSize: 50
);
public static StringBuilder Get() => s_Pool.Get();
public static void Release(StringBuilder sb) => s_Pool.Release(sb);
public static string GetStringAndRelease(StringBuilder sb)
{
string result = sb.ToString();
Release(sb);
return result;
}
}
// Usage
var sb = StringBuilderPool.Get();
sb.Append("Player: ");
sb.Append(playerName);
sb.Append(" Score: ");
sb.Append(score);
string message = StringBuilderPool.GetStringAndRelease(sb);
Audio Pool
Pooled Audio Source
public class AudioPool : MonoBehaviour
{
[SerializeField] private AudioSource _prefab;
[SerializeField] private int _poolSize = 20;
private ObjectPool _pool;
private void Awake()
{
_pool = new ObjectPool(
createFunc: () =>
{
var source = Instantiate(_prefab, transform);
source.gameObject.SetActive(false);
return source;
},
actionOnGet: source => source.gameObject.SetActive(true),
actionOnRelease: source =>
{
source.Stop();
source.clip = null;
source.gameObject.SetActive(false);
},
defaultCapacity: _poolSize,
maxSize: _poolSize * 2
);
}
public void PlayOneShot(AudioClip clip, Vector3 position, float volume = 1f)
{
var source = _pool.Get();
source.transform.position = position;
source.clip = clip;
source.volume = volume;
source.Play();
// Return after clip finishes
StartCoroutine(ReturnAfterPlay(source, clip.length));
}
private IEnumerator ReturnAfterPlay(AudioSource source, float delay)
{
yield return new WaitForSeconds(delay + 0.1f);
_pool.Release(source);
}
}
Performance Considerations
Warm-up Strategy
public class PoolWarmer : MonoBehaviour
{
[SerializeField] private PoolWarmupConfig[] _configs;
private async UniTaskVoid Start()
{
await WarmPools();
}
private async UniTask WarmPools()
{
foreach (var config in _configs)
{
PoolManager.Instance.PrewarmPool(config.Prefab, config.InitialCount);
// Spread over frames
if (config.InitialCount > 10)
{
await UniTask.Yield();
}
}
}
}
[Serializable]
public class PoolWarmupConfig
{
public GameObject Prefab;
public int InitialCount = 10;
}
Pool Statistics
public class PoolStats
{
private readonly Dictionary _metrics = new();
public void RecordGet(string poolName)
{
GetOrCreate(poolName).Gets++;
}
public void RecordRelease(string poolName)
{
GetOrCreate(poolName).Releases++;
}
public void RecordCreate(string poolName)
{
GetOrCreate(poolName).Creates++;
}
public void LogStats()
{
foreach (var (name, metrics) in _metrics)
{
float hitRate = metrics.Gets > 0
? (float)(metrics.Gets - metrics.Creates) / metrics.Gets
: 0;
Debug.Log($"Pool {name}: Gets={metrics.Gets}, Creates={metrics.Creates}, HitRate={hitRate:P1}");
}
}
private PoolMetrics GetOrCreate(string name)
{
if (!_metrics.TryGetValue(name, out var metrics))
{
metrics = new PoolMetrics();
_metrics[name] = metrics;
}
return metrics;
}
private class PoolMetrics
{
public int Gets;
public int Releases;
public int Creates;
}
}
Best Practices
- Pre-warm pools during loading screens
- Set appropriate max sizes - Don't pool forever
- Reset state on release, not on get
- Use composition - PooledObject base class
- Track pool hit rates - Optimize sizes
- Release in OnDisable - Handle scene changes
- Parent inactive objects - Organize hierarchy
- Use built-in pools when available (2021+)
- Pool expensive components - Particle systems, audio
- Profile allocation reduction - Verify GC improvement
Troubleshooting
| Issue | Solution | |-------|----------| | Objects not resetting | Implement OnDespawn/Reset | | Pool growing indefinitely | Set maxSize limit | | Objects active when spawned | SetActive in create, not get | | Memory not decreasing | Clear pools on scene change | | Wrong pool used | Track prefab->pool mapping |
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: tjboudreaux
- Source: tjboudreaux/cc-plugin-unity-gamedev
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.