agentsclimarketplace

Tools unity object pooling

Skill IdoCohen560/claude-unity-game-studio/examples/my-first-game/.claude/skills/tools-unity-object-pooling

AI-powered Unity game development studio for Claude Code - 49 agents, 120+ skills, and a full studio hierarchy that turns a single AI session into a coordinated game dev team. View more about me in my portfolio.

Install
npx -y skills add IdoCohen560/claude-unity-game-studio --skill tools-unity-object-pooling

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 8 stars8 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Object pooling patterns for Unity to reduce GC allocations and improve performance.

SKILL.md

14.3 KB, as published. Nobody here has run it

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<T> where T : class
{
    private readonly Stack<T> _pool = new();
    private readonly Func<T> _createFunc;
    private readonly Action<T> _onGet;
    private readonly Action<T> _onRelease;
    private readonly Action<T> _onDestroy;
    private readonly int _maxSize;
    
    public int CountActive { get; private set; }
    public int CountInactive => _pool.Count;
    public int CountAll => CountActive + CountInactive;
    
    public ObjectPool(
        Func<T> createFunc,
        Action<T> onGet = null,
        Action<T> onRelease = null,
        Action<T> 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 < defaultCapacity; i++)
        {
            var obj = _createFunc();
            _onRelease?.Invoke(obj);
            _pool.Push(obj);
        }
    }
    
    public T Get()
    {
        T obj;
        
        if (_pool.Count > 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 < _maxSize)
        {
            _pool.Push(obj);
        }
        else
        {
            _onDestroy?.Invoke(obj);
        }
    }
    
    public void Clear()
    {
        while (_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<Projectile> _pool;
    
    private void Awake()
    {
        _pool = new ObjectPool<Projectile>(
            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<T> : MonoBehaviour where T : PooledObject<T>
{
    private IObjectPool<T> _pool;
    
    public void SetPool(IObjectPool<T> 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<Projectile>
{
    [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<GameObject, ObjectPool<GameObject>> _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<GameObject> CreatePool(GameObject prefab, int initialSize = 10)
    {
        Transform poolParent = new GameObject($"Pool_{prefab.name}").transform;
        poolParent.SetParent(transform);
        
        return new ObjectPool<GameObject>(
            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<T> where T : Component
{
    private readonly ObjectPool<T> _pool;
    private readonly Transform _parent;
    
    public ComponentPool(T prefab, Transform parent, int initialSize = 10)
    {
        _parent = parent;
        
        _pool = new ObjectPool<T>(
            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<DamageNumber> _pool;
    
    private void Awake()
    {
        _pool = new ComponentPool<DamageNumber>(_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<T>
{
    private static readonly ObjectPool<List<T>> s_Pool = new(
        createFunc: () => new List<T>(),
        actionOnRelease: list => list.Clear(),
        defaultCapacity: 10,
        maxSize: 100
    );
    
    public static List<T> Get() => s_Pool.Get();
    
    public static void Release(List<T> list)
    {
        if (list != null)
            s_Pool.Release(list);
    }
    
    // Disposable wrapper
    public static PooledList<T> GetDisposable()
    {
        return new PooledList<T>(s_Pool.Get());
    }
}

public struct PooledList<T> : IDisposable
{
    public List<T> List { get; }
    
    public PooledList(List<T> list)
    {
        List = list;
    }
    
    public void Dispose()
    {
        ListPool<T>.Release(List);
    }
}

// Usage
using (var pooledList = ListPool<Enemy>.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<StringBuilder> 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<AudioSource> _pool;
    
    private void Awake()
    {
        _pool = new ObjectPool<AudioSource>(
            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<string, PoolMetrics> _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

  1. Pre-warm pools during loading screens
  2. Set appropriate max sizes - Don't pool forever
  3. Reset state on release, not on get
  4. Use composition - PooledObject base class
  5. Track pool hit rates - Optimize sizes
  6. Release in OnDisable - Handle scene changes
  7. Parent inactive objects - Organize hierarchy
  8. Use built-in pools when available (2021+)
  9. Pool expensive components - Particle systems, audio
  10. Profile allocation reduction - Verify GC improvement

Troubleshooting

IssueSolution
Objects not resettingImplement OnDespawn/Reset
Pool growing indefinitelySet maxSize limit
Objects active when spawnedSetActive in create, not get
Memory not decreasingClear pools on scene change
Wrong pool usedTrack prefab->pool mapping

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.