오브젝트 풀

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의 AwakeStart에서 리졸브한다면 클래스에 [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)

옵션기본값동작
InitialCapacity0등록 시 미리 생성할 인스턴스 수.
MaxSizeint.MaxValue비활성 인스턴스의 상한. 상한을 넘겨 반환된 인스턴스는 보관되지 않고 파괴됩니다.
OnExhaustedGrowAndDestroy비활성 인스턴스가 없을 때 Get의 동작(아래 참조).
CallIPoolabletrueOnSpawned() / OnDespawned() 훅 호출 여부.

소진 정책 (PoolExhaustionPolicy)

정책풀이 비었을 때의 동작
GrowAndDestroyGet은 항상 새 인스턴스를 생성합니다(절대 null을 반환하지 않음). 반환 시 MaxSize를 넘는 인스턴스는 파괴됩니다. 안전한 기본값.
GrowAndReuseGet은 항상 생성하고, 반환된 인스턴스는 항상 보관합니다 — 풀이 무한정 자랍니다. 메모리 증가를 명시적으로 감수하는 옵트인.
ReturnNull활성 개수가 MaxSize에 도달하면 Getnull을 반환하고, 무엇을 할지는 호출부가 결정합니다(예: 이펙트 생략).

수명주기 훅 (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 — 이펙트 소스에 내부적으로 같은 풀링 아이디어 사용