본문으로 건너뛰기
cccskills
무료GitHub에 공개

tools-unity-object-pooling

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

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

연동 도구Unity
설치 방법 보기

포함된 파일(1)

  • SKILL.md14.3 KB

SKILL.md(원문)

설치하기 전에 에이전트에게 전달되는 지시 내용을 확인할 수 있습니다.

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

리뷰

아직 리뷰가 없습니다. 사용해 본 소감을 남겨 주세요.

댓글

로그인 없이 쓸 수 있습니다. 사용 후기, 질문, 작동하지 않은 환경 등을 편하게 남겨 주세요.

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

보내면 바로 공개됩니다. 이메일 주소나 전화번호 같은 개인 정보는 쓰지 마세요. URL은 2개까지입니다.

0 / 1,000자

이 양식은 reCAPTCHA로 보호되며 Google 개인정보처리방침 및 서비스 약관이 적용됩니다.

같은 저장소의 스킬

adopt

무료

Brownfield onboarding — audits existing project artifacts for template format compliance (not just existence), classifies gaps by impact, and produces a numbered migration plan. Run this when joining an in-progress project or upgrading from an older template version. Distinct from /project-stage-detect (which checks what exists) — this checks whether what exists will actually work with the template's skills.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

IdoCohen560/claude-unity-game-studio222026년 7월 8일 업데이트

Creates an Architecture Decision Record (ADR) documenting a significant technical decision, its context, alternatives considered, and consequences. Every major technical choice should have an ADR.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

IdoCohen560/claude-unity-game-studio222026년 7월 8일 업데이트

Validates completeness and consistency of the project architecture against all GDDs. Builds a traceability matrix mapping every GDD technical requirement to ADRs, identifies coverage gaps, detects cross-ADR conflicts, verifies engine compatibility consistency across all decisions, and produces a PASS/CONCERNS/FAIL verdict. The architecture equivalent of /design-review.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

IdoCohen560/claude-unity-game-studio222026년 7월 8일 업데이트

art-bible

무료

Guided, section-by-section Art Bible authoring. Creates the visual identity specification that gates all asset production. Run after /brainstorm is approved and before /map-systems or any GDD authoring begins.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

IdoCohen560/claude-unity-game-studio222026년 7월 8일 업데이트

Audits game assets for compliance with naming conventions, file size budgets, format standards, and pipeline requirements. Identifies orphaned assets, missing references, and standard violations.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

IdoCohen560/claude-unity-game-studio222026년 7월 8일 업데이트

Generate per-asset visual specifications and AI generation prompts from GDDs, level docs, or character profiles. Produces structured spec files and updates the master asset manifest. Run after art bible and GDD/level design are approved, before production begins.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

IdoCohen560/claude-unity-game-studio222026년 7월 8일 업데이트

IdoCohen560의 스킬 모두 보기

이 스킬의 문제 신고