Object Pooling 패턴
스폰/파괴 비용을 최소화하여 프레임 스파이크를 방지합니다
Object Pooling 개념
왜 풀링이 필요한가
Object Pooling은 게임플레이 중 액터 스폰 비용을 줄이기 위해 미리 일정 수의 액터를 스폰해두는 기법입니다.
풀링이 해결하는 문제
🐢 스폰 시 랙 스파이크
🗑️ GC 오버헤드
📊 메모리 단편화
⚡ 즉시 활성화
♻️ 재사용 (GC 없음)
📦 연속 메모리
"스폰하지 말고, 활성화하라. 파괴하지 말고, 비활성화하라."
Poolable Actor 인터페이스
풀링 가능한 액터 설계
// IPoolableActor.h
UINTERFACE(MinimalAPI)
class UPoolableActor : public UInterface
{
GENERATED_BODY()
};
class IPoolableActor
{
GENERATED_BODY()
public:
// 풀에서 꺼낼 때 호출
virtual void OnPooled() = 0;
// 풀로 반환할 때 호출
virtual void OnUnpooled() = 0;
// 리셋 (재사용 전 초기화)
virtual void ResetForPool() = 0;
};
// AProjectile.h
UCLASS()
class AProjectile : public AActor, public IPoolableActor
{
GENERATED_BODY()
public:
virtual void OnPooled() override;
virtual void OnUnpooled() override;
virtual void ResetForPool() override;
};
// AProjectile.cpp
void AProjectile::OnPooled()
{
// 활성화
SetActorHiddenInGame(false);
SetActorEnableCollision(true);
SetActorTickEnabled(true);
}
void AProjectile::OnUnpooled()
{
// 비활성화
SetActorHiddenInGame(true);
SetActorEnableCollision(false);
SetActorTickEnabled(false);
SetActorLocation(FVector(0, 0, -10000)); // 화면 밖으로
}
void AProjectile::ResetForPool()
{
// 상태 초기화
Velocity = FVector::ZeroVector;
Damage = DefaultDamage;
LifeTime = DefaultLifeTime;
}
Pool Manager 구현
World Subsystem 기반 풀 관리
InitializePool()
레벨 로드 시 미리 스폰
GetFromPool()
풀에서 액터 가져오기
ReturnToPool()
사용 후 풀로 반환
// UObjectPoolSubsystem.h
UCLASS()
class UObjectPoolSubsystem : public UWorldSubsystem
{
GENERATED_BODY()
private:
// 클래스별 풀
TMap<TSubclassOf<AActor>, TArray<AActor*>> Pools;
// 풀 설정
TMap<TSubclassOf<AActor>, int32> PoolSizes;
public:
// 풀 초기화
void InitializePool(TSubclassOf<AActor> ActorClass, int32 Size);
// 풀에서 가져오기
AActor* GetFromPool(TSubclassOf<AActor> ActorClass,
const FTransform& SpawnTransform);
// 풀로 반환
void ReturnToPool(AActor* Actor);
};
void UObjectPoolSubsystem::InitializePool(
TSubclassOf<AActor> ActorClass, int32 Size)
{
TArray<AActor*>& Pool = Pools.FindOrAdd(ActorClass);
PoolSizes.Add(ActorClass, Size);
// 미리 스폰
for (int32 i = 0; i < Size; ++i)
{
AActor* Actor = GetWorld()->SpawnActor<AActor>(ActorClass);
if (IPoolableActor* Poolable = Cast<IPoolableActor>(Actor))
{
Poolable->OnUnpooled(); // 비활성 상태로 시작
}
Pool.Add(Actor);
}
}
AActor* UObjectPoolSubsystem::GetFromPool(
TSubclassOf<AActor> ActorClass,
const FTransform& SpawnTransform)
{
TArray<AActor*>* Pool = Pools.Find(ActorClass);
if (Pool && Pool->Num() > 0)
{
AActor* Actor = Pool->Pop();
Actor->SetActorTransform(SpawnTransform);
if (IPoolableActor* Poolable = Cast<IPoolableActor>(Actor))
{
Poolable->ResetForPool();
Poolable->OnPooled();
}
return Actor;
}
// 풀이 비어있으면 새로 스폰 (fallback)
return GetWorld()->SpawnActor<AActor>(ActorClass, SpawnTransform);
}
void UObjectPoolSubsystem::ReturnToPool(AActor* Actor)
{
if (!Actor) return;
if (IPoolableActor* Poolable = Cast<IPoolableActor>(Actor))
{
Poolable->OnUnpooled();
}
TArray<AActor*>& Pool = Pools.FindOrAdd(Actor->GetClass());
Pool.Add(Actor);
}
주의사항
풀링 구현 시 고려사항
사운드나 VFX가 자동 활성화되지 않도록 주의하세요. 프리스폰 시점에 모든 이펙트가 동시에 재생될 수 있습니다.
Destroy() 대신 ReturnToPool()을 사용하세요. 실수로 파괴하면 풀 크기가 줄어들어 결국 스폰 비용이 발생합니다.
ResetForPool() 체크리스트
🔢 상태 변수
HP, 속도, 데미지 등 모든 게임플레이 변수
🧩 컴포넌트
자식 컴포넌트 상태도 함께 리셋
⏱️ 타이머
활성 타이머 모두 ClearTimer
📢 델리게이트
바인딩된 델리게이트 정리
⚡ 물리
Velocity, AngularVelocity 초기화
🎭 애니메이션
애니메이션 상태 리셋
핵심 요약
- Object Pooling: 스폰/파괴 대신 활성화/비활성화로 비용 절감
- IPoolableActor: OnPooled, OnUnpooled, ResetForPool 인터페이스
- World Subsystem: 레벨 단위 풀 관리, 클래스별 풀 분리
- Destroy() 금지: 항상 ReturnToPool() 사용
- 상태 초기화: 재사용 전 모든 변수와 컴포넌트 리셋 필수
다음 강의에서는 캐시 친화적 데이터 구조와 Data Oriented Design으로 CPU 캐시 효율을 극대화하는 방법을 다룹니다.
도전 과제
배운 내용을 직접 실습해보세요
총알 Actor를 50개 미리 스폰하여 풀에 보관하는 시스템을 구현하세요. Deactivate/Activate 패턴으로 재사용하고, SpawnActor 대비 프레임 히칭 감소를 측정합니다.
풀이 고갈되면 자동으로 10개씩 추가 생성하는 확장 풀을 구현하세요. SetActorHiddenInGame(), SetActorEnableCollision(), SetActorTickEnabled()를 활용하여 비활성화/활성화를 처리합니다.
TSubclassOf