Unity / C#
Beginner
Unity 2021.1+ / 2022 / Unity 6
Unity C# 제네릭 오브젝트 풀링 (UnityEngine.Pool 최적화)
Instantiate/Destroy 반복으로 인한 GC 가비지와 프레임 드랍을 원천 차단하는 UnityEngine.Pool.IObjectPool 기반 제네릭 풀 구현 및 실전 컴포넌트 래퍼.
#Unity #C# #ObjectPool #Performance #GC-Optimization
왜 써야 하는가?탄막 슈팅의 총알, 파티클 이펙트, 몬스터 스폰 등 자주 생성/소멸하는 오브젝트에
Instantiate()와 Destroy()를 호출하면 C# 가비지 컬렉터(GC)에 심각한 힙 할당이 누적되어 주기적인 화면 멈춤(Spike) 현상이 발생합니다. Unity 2021부터 기본 제공되는 내장 UnityEngine.Pool을 사용하면 추가 에셋 없이도 안전하고 빠른 풀링 시스템을 구축할 수 있습니다.1. 제네릭 컴포넌트 풀 (ComponentPool<T>)
프리팹을 인스턴스화하고 풀 반환/대여 시 상태를 초기화해 주는 MonoBehaviour용 래퍼 클래스입니다.
using System;
using UnityEngine;
using UnityEngine.Pool;
namespace LyscStudio.Patterns
{
/// <summary>
/// UnityEngine.Pool.ObjectPool 기반의 컴포넌트 풀 래퍼
/// </summary>
public class ComponentPool<T> : IDisposable where T : Component
{
private readonly T _prefab;
private readonly Transform _parent;
private readonly IObjectPool<T> _pool;
public int ActiveCount => _pool.CountActive;
public int InactiveCount => _pool.CountInactive;
public ComponentPool(
T prefab,
Transform parent = null,
int defaultCapacity = 20,
int maxCapacity = 200,
bool collectionCheck = true)
{
_prefab = prefab;
_parent = parent;
_pool = new ObjectPool<T>(
createFunc: OnCreateItem,
actionOnGet: OnGetItem,
actionOnRelease: OnReleaseItem,
actionOnDestroy: OnDestroyItem,
collectionCheck: collectionCheck, // 이미 반환된 객체의 중복 반환 검사
defaultCapacity: defaultCapacity,
maxSize: maxCapacity
);
}
private T OnCreateItem()
{
T instance = UnityEngine.Object.Instantiate(_prefab, _parent);
instance.gameObject.SetActive(false);
return instance;
}
private void OnGetItem(T item)
{
item.gameObject.SetActive(true);
}
private void OnReleaseItem(T item)
{
if (item != null && item.gameObject.activeSelf)
{
item.gameObject.SetActive(false);
}
}
private void OnDestroyItem(T item)
{
if (item != null)
{
UnityEngine.Object.Destroy(item.gameObject);
}
}
public T Get() => _pool.Get();
public void Release(T item) => _pool.Release(item);
public void Clear() => _pool.Clear();
public void Dispose()
{
Clear();
}
}
}
2. 실제 총알(Bullet) 오브젝트 적용 예시
풀에서 꺼낸 오브젝트가 수명을 다했을 때 스스로 풀로 돌아가도록 콜백을 전달하는 패턴입니다.
using System;
using UnityEngine;
public class PooledBullet : MonoBehaviour
{
[SerializeField] private float _speed = 20f;
[SerializeField] private float _lifeTime = 2.5f;
private Action<PooledBullet> _returnToPool;
private float _timer;
public void Initialize(Action<PooledBullet> returnAction)
{
_returnToPool = returnAction;
_timer = 0f;
}
private void Update()
{
transform.position += transform.forward * (_speed * Time.deltaTime);
_timer += Time.deltaTime;
if (_timer >= _lifeTime)
{
Return();
}
}
private void OnTriggerEnter(Collider other)
{
// 충돌 로직 처리 후 풀로 반환
Return();
}
private void Return()
{
if (_returnToPool != null)
{
_returnToPool(this);
_returnToPool = null; // 중복 반환 방지
}
}
}
3. 주요 옵션 및 성능 비교
| 항목 | 일반 Instantiate/Destroy | UnityEngine.Pool |
|---|---|---|
| 프레임 타임 (1000개 연타) | 12ms ~ 28ms (GC Spike 발생) | 0.8ms ~ 1.5ms (극도로 안정적) |
| GC Alloc (Update 매 프레임) | 초당 수십 KB ~ MB 누적 | 0 B (최초 풀 생성 이후 제로 할당) |
| collectionCheck 옵션 | 해당 없음 | true 시 중복 반환 시 예외를 던져 안정성 보장 (빌드 시 조건부 권장) |
| maxSize 초과 시 | 계속 힙 메모리 증가 | 초과분은 풀에 쌓아두지 않고 자동으로 Destroy 호출 |
4. 주의점 및 메모리 누수 방지 팁
- 이중 반환(Double Release) 버그: 이미 풀에 들어있는 오브젝트를 다른 곳에서 다시
Release()하면 풀 큐에 같은 인스턴스가 2번 들어가서 다음Get()시 예측할 수 없는 버그가 생깁니다.collectionCheck: true를 유지하여 개발 단계에서 즉시 적발하십시오. - 컴포넌트 잔여 상태 리셋:
Rigidbody.velocity = Vector3.zero,TrailRenderer.Clear(), 각종 애니메이터 파라미터는OnGetItem또는 풀에서 꺼낼 때 반드시 리셋해 주어야 이전 상태가 튀는 현상을 막을 수 있습니다. - 씬 전환 시 풀 정리: 씬이 바뀔 때 파괴된(Destroyed) 오브젝트의 참조가 풀에 남아있지 않도록
OnDestroy에서 풀의Dispose()또는Clear()를 명시적으로 호출하십시오.