오브젝트 풀
Instantiate와 Destroy 대신 GameObject 인스턴스를 재사용해 프레임 시간을 안정적으로 유지합니다.
Instantiate/Destroy대신Get/Release· 시작 시 프리웜, 영원히 재사용 · No-op 대체 구현:NullObjectPool
CGS는 **IObjectPoolService**를 통해 오브젝트를 빈번하게 스폰하고 파괴하는 비용을 제거합니다. Instantiate(prefab)과 Destroy(go)를 반복 호출하는 대신, Get(prefab)으로 준비된 인스턴스를 받고 Release(instance)로 돌려줍니다. 비싼 작업 — 할당, Awake 호출, GameObject 등록 — 이 핫 패스에서 빠져나와 일회성 프리웜(시작 시 인스턴스를 미리 생성하는 단계)으로 옮겨집니다. 그 결과: 적 30마리를 한꺼번에 스폰하거나, 수십 개의 이펙트를 터뜨리거나, 투사체를 연사해도 더 이상 프레임 히치가 생기지 않습니다.
주요 기능
- 프리팹별 풀. 튜닝 옵션과 함께 프리팹을 한 번 등록하면, 서비스가 그 프리팹의 비활성 인스턴스 스택을 유지합니다.
- 생성·파괴 대신 Get과 Release.
Get은 비활성 인스턴스를 꺼내(없으면 생성) 활성화한 뒤 넘겨줍니다.Release는 비활성화해 다시 넣습니다. - 수명주기 훅.
IPoolable을 구현한 컴포넌트는OnSpawned()와OnDespawned()콜백을 받습니다 — 사용 사이에 상태를 리셋하기 위한, 풀링 버전의Awake/OnDestroy입니다. - 프리웜. 로딩 화면 동안 인스턴스 N개를 미리 생성해, 첫 번째 바쁜 전투 프레임이 할당 비용을 치르지 않게 합니다.
- 진단. 프리팹별 활성·비활성·전체 개수를 언제든 조회할 수 있습니다.
서비스 가져오기
한 번 리졸브하고 캐시하세요 — Update에서 Resolve를 호출하지 마세요:
using CommonGameSystem.Core;
var pool = ServiceLocator.Resolve<IObjectPoolService>();
MonoBehaviour의 Awake나 Start에서 리졸브한다면 클래스에 [DefaultExecutionOrder(100)]를 붙이세요. 풀은 자동 부트스트래퍼가 씬의 어떤 Awake보다 먼저 등록하는 23개 서비스 중 하나이므로, 리졸브하는 시점에는 이미 준비되어 있습니다.
API 레퍼런스
등록
void RegisterPrefab(GameObject prefab, PoolOptions options) // Create a pool for the prefab
bool IsRegistered(GameObject prefab) // Does this prefab have a pool?
void UnregisterPrefab(GameObject prefab) // Destroy the pool and all its instances
같은 프리팹을 두 번 등록하면 ArgumentException을 던집니다 — 시작 시 한 번만 등록하세요.
Get / Release
GameObject Get(GameObject prefab)
GameObject Get(GameObject prefab, Vector3 position, Quaternion rotation)
bool Release(GameObject instance)
Get은 비활성 인스턴스를 꺼내(소진 정책에 따라 새로 생성하기도 함) 활성화하고, 인스턴스가 IPoolable을 구현하면 OnSpawned()를 호출합니다. 인자 세 개짜리 오버로드는 활성화 전에 트랜스폼을 설정합니다. Release는 인스턴스를 비활성화하고, OnDespawned()가 있으면 호출한 뒤 풀에 돌려놓습니다 — 인스턴스가 어떤 풀의 소유도 아니면 false를 반환합니다.
프리웜
void Prewarm(GameObject prefab, int count) // Pre-create instances to kill first-use hiccups
진단과 정리
int CountActive(GameObject prefab) // Instances currently in use
int CountInactive(GameObject prefab) // Instances resting in the pool
int CountTotal(GameObject prefab) // Active + inactive
void Clear(GameObject prefab) // Destroy all inactive instances (registration stays)
void ClearAll() // Clear every pool
튜닝 (PoolOptions)
| 옵션 | 기본값 | 동작 |
|---|---|---|
InitialCapacity | 0 | 등록 시 미리 생성할 인스턴스 수. |
MaxSize | int.MaxValue | 비활성 인스턴스의 상한. 상한을 넘겨 반환된 인스턴스는 보관되지 않고 파괴됩니다. |
OnExhausted | GrowAndDestroy | 비활성 인스턴스가 없을 때 Get의 동작(아래 참조). |
CallIPoolable | true | OnSpawned() / OnDespawned() 훅 호출 여부. |
소진 정책 (PoolExhaustionPolicy)
| 정책 | 풀이 비었을 때의 동작 |
|---|---|
GrowAndDestroy | Get은 항상 새 인스턴스를 생성합니다(절대 null을 반환하지 않음). 반환 시 MaxSize를 넘는 인스턴스는 파괴됩니다. 안전한 기본값. |
GrowAndReuse | Get은 항상 생성하고, 반환된 인스턴스는 항상 보관합니다 — 풀이 무한정 자랍니다. 메모리 증가를 명시적으로 감수하는 옵트인. |
ReturnNull | 활성 개수가 MaxSize에 도달하면 Get이 null을 반환하고, 무엇을 할지는 호출부가 결정합니다(예: 이펙트 생략). |
수명주기 훅 (IPoolable)
풀링되는 프리팹의 아무 컴포넌트에나 구현하세요:
void OnSpawned() // Called right after Get activates the instance — reset state here
void OnDespawned() // Called right before Release deactivates it — clean up here
풀링된 인스턴스는 첫 사용 이후 Awake를 건너뛰므로, 사용마다 필요한 초기화는 OnSpawned에 두어야 합니다.
전체 예제
총알 시스템: 스포너가 등록하고 발사하며, 총알은 이동하고 수명을 카운트다운한 뒤 스스로 반환합니다.
using CommonGameSystem.Core;
using UnityEngine;
[DefaultExecutionOrder(100)]
public class BulletSpawner : MonoBehaviour
{
[SerializeField] private GameObject _bulletPrefab;
private IObjectPoolService _pool;
private void Awake()
{
_pool = ServiceLocator.Resolve<IObjectPoolService>();
_pool.RegisterPrefab(_bulletPrefab, new PoolOptions
{
InitialCapacity = 50, // Pre-create 50 bullets at startup
MaxSize = 100, // Keep up to 100 inactive
OnExhausted = PoolExhaustionPolicy.GrowAndDestroy
});
}
public void Fire(Vector3 position, Quaternion rotation)
{
// With GrowAndDestroy this never returns null.
_pool.Get(_bulletPrefab, position, rotation);
}
}
// On the bullet prefab:
[DefaultExecutionOrder(100)]
public class Bullet : MonoBehaviour, IPoolable
{
[SerializeField] private float _speed = 20f;
[SerializeField] private float _maxLifetime = 3f;
private IObjectPoolService _pool;
private float _lifetime;
private void Awake()
{
_pool = ServiceLocator.Resolve<IObjectPoolService>();
}
public void OnSpawned()
{
// Reset per-use state — pooled instances do not re-run Awake.
_lifetime = _maxLifetime;
}
public void OnDespawned()
{
// Optional cleanup before going back to the pool.
}
private void Update()
{
transform.Translate(Vector3.forward * _speed * Time.deltaTime);
_lifetime -= Time.deltaTime;
if (_lifetime <= 0f)
{
_pool.Release(gameObject); // Back to the pool — never Destroy()
}
}
}
끄기
(디버깅을 위해, 또는 메모리가 넉넉한 플랫폼에서) 풀링을 끄려면 null 구현으로 바꿔 넣으세요:
ServiceLocator.Replace<IObjectPoolService>(new NullObjectPool());
NullObjectPool은 인스턴스를 Instantiate / Destroy로 곧장 처리합니다. 게임 코드는 아무 차이도 느끼지 못합니다 — 성능 이점만 사라질 뿐입니다. 호출부에는 null 체크도, 수정도 필요 없습니다.
흔한 함정
-
메인 스레드 전용. 모든 풀 메서드는 Unity 메인 스레드에서 호출해야 합니다. 워커 스레드에서의 호출은 Debug 빌드에서
InvalidOperationException을 던집니다. -
반환은 풀을 통해, 절대
Destroy로 하지 마세요. 풀링된 인스턴스에UnityEngine.Object.Destroy(instance)를 호출하면 풀의 활성 카운터가 깨집니다. 항상Release(instance)를 사용하세요. -
프리팹은 명시적으로 등록하세요. 등록되지 않은 프리팹에
Get(prefab)을 호출하면 풀이 기본 옵션으로 자동 등록하고 일회성 경고를 로그합니다. 동작은 하지만InitialCapacity,MaxSize,OnExhausted를 튜닝할 기회를 잃습니다 — 미리 등록하세요. -
인자 하나짜리
Get은 이전 트랜스폼을 유지합니다.Get(prefab)은 인스턴스를 반환될 당시의 위치에 그대로 둡니다. 스폰 위치가 중요하다면Get(prefab, position, rotation)을 쓰세요. -
훅 예외는 격리됩니다.
OnSpawned()나OnDespawned()가 예외를 던지면 풀이 예외를 잡아 오류를 로그하고 계속 진행합니다 — 인스턴스는 여전히 정상적으로 활성화 또는 비활성화됩니다. 여러분의 버그가 풀을 망가뜨리지 않습니다. -
IL2CPP 스트리핑. 자체 구성 타입이나 이벤트 타입으로 풀을 확장한다면 IL2CPP 빌드에서 살아남도록
link.xml항목을 추가하세요. 핵심 타입(IObjectPoolService,GameObjectPool,NullObjectPool)은 프레임워크가 이미 보존합니다.
관련 페이지
- Bootstrap — 서비스가 자동으로 시작되는 방식
- Service Locator — 서비스 리졸브와 교체
- Audio — 이펙트 소스에 내부적으로 같은 풀링 아이디어 사용