Unity Object Pooling 심화
오브젝트 풀링은 GameObject의 반복적인 Instantiate/Destroy 비용을 제거하는 가장 기본적인 최적화 기법입니다. Unity 2021부터는 UnityEngine.Pool.ObjectPool<T>가 내장되어 별도 구현 없이 바로 사용할 수 있습니다.
1. 내장 ObjectPool 기본 사용
섹션 제목: “1. 내장 ObjectPool 기본 사용”using UnityEngine;using UnityEngine.Pool;
public class BulletPool : MonoBehaviour{ [SerializeField] private Bullet bulletPrefab; [SerializeField] private int defaultCapacity = 20; [SerializeField] private int maxSize = 100;
private IObjectPool<Bullet> _pool;
void Awake() { _pool = new ObjectPool<Bullet>( createFunc: CreateBullet, actionOnGet: OnGetFromPool, actionOnRelease: OnReleaseToPool, actionOnDestroy: OnDestroyPoolObject, collectionCheck: true, // 중복 반환 감지 (개발 시 true) defaultCapacity: defaultCapacity, maxSize: maxSize ); }
private Bullet CreateBullet() { var bullet = Instantiate(bulletPrefab); bullet.Pool = _pool; // 총알이 스스로 반환할 수 있도록 참조 전달 return bullet; }
private void OnGetFromPool(Bullet bullet) => bullet.gameObject.SetActive(true); private void OnReleaseToPool(Bullet bullet) => bullet.gameObject.SetActive(false); private void OnDestroyPoolObject(Bullet bullet) => Destroy(bullet.gameObject);
public Bullet Get() => _pool.Get();}public class Bullet : MonoBehaviour{ public IObjectPool<Bullet> Pool { get; set; } [SerializeField] private float lifetime = 3f;
void OnEnable() { CancelInvoke(); Invoke(nameof(ReturnToPool), lifetime); }
private void ReturnToPool() => Pool?.Release(this);}2. 풀 워밍업 (Prewarm)
섹션 제목: “2. 풀 워밍업 (Prewarm)”레벨 시작 시 풀을 미리 채워두면 첫 번째 스파이크를 제거할 수 있습니다.
public class PoolPrewarmer : MonoBehaviour{ [SerializeField] private BulletPool bulletPool; [SerializeField] private int prewarmCount = 20;
IEnumerator Start() { // 프레임 분산: 한 프레임에 몰아서 생성하면 히치 발생 var list = new List<Bullet>(prewarmCount); for (int i = 0; i < prewarmCount; i++) { list.Add(bulletPool.Get()); if (i % 5 == 0) yield return null; // 5개마다 프레임 양보 } // 전부 반환 foreach (var b in list) b.Pool.Release(b); }}3. 제네릭 풀 매니저
섹션 제목: “3. 제네릭 풀 매니저”여러 프리팹 타입을 하나의 매니저로 관리하는 패턴입니다.
using System.Collections.Generic;using UnityEngine;using UnityEngine.Pool;
public class PoolManager : MonoBehaviour{ public static PoolManager Instance { get; private set; }
private readonly Dictionary<GameObject, ObjectPool<GameObject>> _pools = new();
void Awake() { if (Instance != null) { Destroy(gameObject); return; } Instance = this; DontDestroyOnLoad(gameObject); }
private ObjectPool<GameObject> GetOrCreatePool(GameObject prefab) { if (!_pools.TryGetValue(prefab, out var pool)) { pool = new ObjectPool<GameObject>( createFunc: () => Instantiate(prefab), actionOnGet: obj => obj.SetActive(true), actionOnRelease: obj => obj.SetActive(false), actionOnDestroy: Destroy, defaultCapacity: 10, maxSize: 200 ); _pools[prefab] = pool; } return pool; }
public GameObject Get(GameObject prefab) => GetOrCreatePool(prefab).Get();
public void Release(GameObject prefab, GameObject instance) => GetOrCreatePool(prefab).Release(instance);}4. LinkedPool — 스택 기반 경량 풀
섹션 제목: “4. LinkedPool — 스택 기반 경량 풀”LinkedPool<T>는 ObjectPool<T>보다 메모리 오버헤드가 적은 연결 리스트 기반 풀입니다. 풀 크기를 동적으로 늘려야 하는 경우에 적합합니다.
// LinkedPool은 maxSize 초과 시 오브젝트를 파괴하지 않고 계속 보유var linkedPool = new LinkedPool<Bullet>( createFunc: () => Instantiate(bulletPrefab).GetComponent<Bullet>(), actionOnGet: b => b.gameObject.SetActive(true), actionOnRelease: b => b.gameObject.SetActive(false), actionOnDestroy: b => Destroy(b.gameObject));5. 풀 크기 동적 조정
섹션 제목: “5. 풀 크기 동적 조정”실제 게임에서는 부하가 예측 불가능합니다. 풀 사용량을 모니터링하고 동적으로 확장하는 로직을 추가합니다.
public class AdaptivePool<T> where T : Component{ private readonly ObjectPool<T> _pool; private int _peakCount = 0; private int _currentCount = 0;
public int PeakCount => _peakCount;
public AdaptivePool(System.Func<T> factory, int initial = 10, int max = 500) { _pool = new ObjectPool<T>( factory, actionOnGet: _ => _currentCount++, actionOnRelease: _ => _currentCount--, maxSize: max, defaultCapacity: initial ); }
public T Get() { var obj = _pool.Get(); _peakCount = Mathf.Max(_peakCount, _currentCount); return obj; }
public void Release(T obj) => _pool.Release(obj);}6. 성능 비교
섹션 제목: “6. 성능 비교”| 방식 | Instantiate 비용 | GC 압력 | 코드 복잡도 |
|---|---|---|---|
| 매번 Instantiate | 높음 | 높음 (Destroy) | 낮음 |
| ObjectPool | 없음 (재사용) | 낮음 | 보통 |
| LinkedPool | 없음 | 매우 낮음 | 보통 |
| 커스텀 배열 풀 | 없음 | 없음 | 높음 |
- Unity 내장
ObjectPool<T>은collectionCheck: true로 개발 중 중복 반환 버그를 조기에 발견한다. - 레벨 로드 시
Prewarm을 프레임 분산하여 실행하면 로딩 직후 히치를 방지할 수 있다. - 여러 프리팹을 관리할 때는
Dictionary<GameObject, ObjectPool<GameObject>>패턴을 사용한다. - 반환된 오브젝트의 상태(위치, 속도, 이벤트 구독 등)를
OnGetFromPool에서 반드시 초기화해야 한다.