Object Pooling
빈번한 스폰/디스폰의 비용을 제거하는 재사용 패턴
Object Pooling이란?
객체 재사용으로 성능 향상
Object Pooling은 객체를 파괴하지 않고 재사용하는 디자인 패턴입니다. SpawnActor와 Destroy는 비용이 높은 연산이므로, 자주 생성/삭제되는 객체에 적용하면 큰 성능 향상을 얻을 수 있습니다.
비활성 객체들
사용 중인 객체
반환된 객체
적합한 사용 사례
총알, 화살, 마법
파티클, 데미지 텍스트
웨이브 스폰
골드, 아이템
Object Pool Manager 구현
범용 오브젝트 풀 매니저
UCLASS()
class MYGAME_API AObjectPoolManager : public AActor
{
GENERATED_BODY()
public:
// 풀에서 액터 가져오기
UFUNCTION(BlueprintCallable, Category = "Pool")
AActor* GetPooledActor(TSubclassOf<AActor> ActorClass);
// 풀로 반환
UFUNCTION(BlueprintCallable, Category = "Pool")
void ReturnToPool(AActor* Actor);
// 풀 사전 생성 (워밍업)
UFUNCTION(BlueprintCallable, Category = "Pool")
void PrewarmPool(TSubclassOf<AActor> ActorClass, int32 Count);
// 싱글톤 접근
static AObjectPoolManager* Get(UWorld* World);
private:
// 클래스별 비활성 객체 목록
UPROPERTY()
TMap<TSubclassOf<AActor>, TArray<AActor*>> PooledActors;
// 클래스별 활성 객체 목록
UPROPERTY()
TMap<TSubclassOf<AActor>, TArray<AActor*>> ActiveActors;
};
AActor* AObjectPoolManager::GetPooledActor(TSubclassOf<AActor> ActorClass)
{
TArray<AActor*>* Pool = PooledActors.Find(ActorClass);
if (Pool && Pool->Num() > 0)
{
// 풀에서 가져오기
AActor* Actor = Pool->Pop();
// 활성화
Actor->SetActorHiddenInGame(false);
Actor->SetActorEnableCollision(true);
Actor->SetActorTickEnabled(true);
// 활성 목록에 추가
ActiveActors.FindOrAdd(ActorClass).Add(Actor);
// IPoolableActor 인터페이스 호출
if (Actor->Implements<UPoolableActor>())
{
IPoolableActor::Execute_OnSpawnFromPool(Actor);
}
return Actor;
}
// 풀이 비었으면 새로 생성
AActor* NewActor = GetWorld()->SpawnActor<AActor>(ActorClass);
if (NewActor->Implements<UPoolableActor>())
{
IPoolableActor::Execute_OnPoolInitialize(NewActor);
}
ActiveActors.FindOrAdd(ActorClass).Add(NewActor);
return NewActor;
}
void AObjectPoolManager::ReturnToPool(AActor* Actor)
{
if (!Actor) return;
TSubclassOf<AActor> ActorClass = Actor->GetClass();
// IPoolableActor 인터페이스 호출
if (Actor->Implements<UPoolableActor>())
{
IPoolableActor::Execute_OnReturnToPool(Actor);
}
// 비활성화
Actor->SetActorHiddenInGame(true);
Actor->SetActorEnableCollision(false);
Actor->SetActorTickEnabled(false);
// 위치 리셋
Actor->SetActorLocation(FVector(0, 0, -10000));
// 활성 목록에서 제거
if (TArray<AActor*>* ActiveList = ActiveActors.Find(ActorClass))
{
ActiveList->Remove(Actor);
}
// 풀에 반환
PooledActors.FindOrAdd(ActorClass).Add(Actor);
}
void AObjectPoolManager::PrewarmPool(TSubclassOf<AActor> ActorClass, int32 Count)
{
TArray<AActor*>& Pool = PooledActors.FindOrAdd(ActorClass);
for (int32 i = 0; i < Count; i++)
{
AActor* Actor = GetWorld()->SpawnActor<AActor>(ActorClass);
if (Actor->Implements<UPoolableActor>())
{
IPoolableActor::Execute_OnPoolInitialize(Actor);
}
// 즉시 비활성화
Actor->SetActorHiddenInGame(true);
Actor->SetActorEnableCollision(false);
Actor->SetActorTickEnabled(false);
Actor->SetActorLocation(FVector(0, 0, -10000));
Pool.Add(Actor);
}
}
IPoolableActor 인터페이스
풀링 객체의 표준 인터페이스
UINTERFACE(MinimalAPI, Blueprintable)
class UPoolableActor : public UInterface
{
GENERATED_BODY()
};
class IPoolableActor
{
GENERATED_BODY()
public:
// 풀에서 꺼낼 때 호출
UFUNCTION(BlueprintNativeEvent, Category = "Pool")
void OnSpawnFromPool();
// 풀로 반환될 때 호출
UFUNCTION(BlueprintNativeEvent, Category = "Pool")
void OnReturnToPool();
// 최초 풀 생성 시 호출
UFUNCTION(BlueprintNativeEvent, Category = "Pool")
void OnPoolInitialize();
};
투사체 예제
UCLASS()
class APooledProjectile : public AActor, public IPoolableActor
{
GENERATED_BODY()
public:
APooledProjectile()
{
PrimaryActorTick.bCanEverTick = true;
PrimaryActorTick.bStartWithTickEnabled = false;
}
// IPoolableActor 구현
virtual void OnSpawnFromPool_Implementation() override
{
// 상태 초기화
CurrentLifetime = 0.f;
Velocity = FVector::ZeroVector;
// 이펙트 재시작
if (TrailEffect)
{
TrailEffect->Activate(true);
}
}
virtual void OnReturnToPool_Implementation() override
{
// 이펙트 중지
if (TrailEffect)
{
TrailEffect->Deactivate();
}
// 충돌 이벤트 초기화
GetComponents<UPrimitiveComponent>(PrimitiveComponents);
for (UPrimitiveComponent* Comp : PrimitiveComponents)
{
Comp->SetGenerateOverlapEvents(false);
}
}
virtual void OnPoolInitialize_Implementation() override
{
// 최초 생성 시 설정
}
// 발사 함수
void Fire(FVector Direction, float Speed)
{
Velocity = Direction * Speed;
SetActorTickEnabled(true);
for (UPrimitiveComponent* Comp : PrimitiveComponents)
{
Comp->SetGenerateOverlapEvents(true);
}
}
virtual void Tick(float DeltaTime) override
{
Super::Tick(DeltaTime);
// 이동
SetActorLocation(GetActorLocation() + Velocity * DeltaTime);
// 수명 체크
CurrentLifetime += DeltaTime;
if (CurrentLifetime >= MaxLifetime)
{
// 풀로 반환
if (AObjectPoolManager* PoolManager =
AObjectPoolManager::Get(GetWorld()))
{
PoolManager->ReturnToPool(this);
}
}
}
private:
UPROPERTY(VisibleAnywhere)
UNiagaraComponent* TrailEffect;
FVector Velocity;
float CurrentLifetime = 0.f;
float MaxLifetime = 5.f;
TArray<UPrimitiveComponent*> PrimitiveComponents;
};
사용 예시
실제 게임플레이에서의 활용
void AWeapon::Fire()
{
// 풀 매니저에서 투사체 가져오기
AObjectPoolManager* PoolManager =
AObjectPoolManager::Get(GetWorld());
if (!PoolManager) return;
AActor* PooledActor = PoolManager->GetPooledActor(ProjectileClass);
if (APooledProjectile* Projectile =
Cast<APooledProjectile>(PooledActor))
{
// 위치와 방향 설정
Projectile->SetActorLocation(MuzzleLocation);
Projectile->SetActorRotation(MuzzleRotation);
// 발사
Projectile->Fire(GetAimDirection(), ProjectileSpeed);
}
}
// GameMode에서 풀 워밍업
void AMyGameMode::BeginPlay()
{
Super::BeginPlay();
AObjectPoolManager* PoolManager =
AObjectPoolManager::Get(GetWorld());
if (PoolManager)
{
// 게임 시작 시 미리 생성
PoolManager->PrewarmPool(ProjectileClass, 50);
PoolManager->PrewarmPool(DamageNumberClass, 30);
PoolManager->PrewarmPool(HitEffectClass, 20);
}
}
로딩 화면이나 게임 시작 시에 풀을 미리 채워두면 게임 중 첫 스폰 시 히치(hitch)를 방지할 수 있습니다. 예상되는 최대 동시 사용 수만큼 미리 생성하세요.
핵심 요약
- Object Pooling - SpawnActor/Destroy 대신 객체 재사용
- 적합한 대상 - 투사체, 이펙트, 데미지 텍스트, 웨이브 스폰 적
- IPoolableActor - OnSpawnFromPool, OnReturnToPool로 상태 초기화
- 비활성화 방법 - Hidden, Collision 비활성화, Tick 비활성화, 월드 외부로 이동
- PrewarmPool - 로딩 시 미리 생성하여 런타임 히치 방지
풀에서 가져온 객체는 반드시 상태를 초기화해야 합니다. 이전 사용의 데이터가 남아있으면 버그가 발생할 수 있습니다.
도전 과제
배운 내용을 직접 실습해보세요
RPG 원거리 공격 프로젝타일을 위한 오브젝트 풀을 구현하세요. 미리 N개의 프로젝타일을 SpawnActor로 생성하고, SetActorHiddenInGame(true)로 비활성화해 두었다가 재사용하세요.
UNiagaraComponent를 풀링하여 히트 이펙트, 힐 이펙트 등을 재사용하세요. Activate()/Deactivate()로 이펙트를 제어하고, 풀 고갈 시 동적 확장 로직을 추가하세요.
TSubclassOf