PART 7 - 강의 3/8

Object Pooling

빈번한 스폰/디스폰의 비용을 제거하는 재사용 패턴

01

Object Pooling이란?

객체 재사용으로 성능 향상

Object Pooling은 객체를 파괴하지 않고 재사용하는 디자인 패턴입니다. SpawnActor와 Destroy는 비용이 높은 연산이므로, 자주 생성/삭제되는 객체에 적용하면 큰 성능 향상을 얻을 수 있습니다.

Pool
비활성 객체들
Active
사용 중인 객체
Pool
반환된 객체

적합한 사용 사례

💥
투사체

총알, 화살, 마법

이펙트

파티클, 데미지 텍스트

👾
적 NPC

웨이브 스폰

🎁
아이템 드롭

골드, 아이템

02

Object Pool Manager 구현

범용 오브젝트 풀 매니저

ObjectPoolManager.h
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; };
ObjectPoolManager.cpp
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); } }
03

IPoolableActor 인터페이스

풀링 객체의 표준 인터페이스

IPoolableActor.h
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(); };

투사체 예제

PooledProjectile.h
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; };
04

사용 예시

실제 게임플레이에서의 활용

C++ - Weapon.cpp
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); } }
PrewarmPool 타이밍

로딩 화면이나 게임 시작 시에 풀을 미리 채워두면 게임 중 첫 스폰 시 히치(hitch)를 방지할 수 있습니다. 예상되는 최대 동시 사용 수만큼 미리 생성하세요.

SUMMARY

핵심 요약

핵심 포인트
  • Object Pooling - SpawnActor/Destroy 대신 객체 재사용
  • 적합한 대상 - 투사체, 이펙트, 데미지 텍스트, 웨이브 스폰 적
  • IPoolableActor - OnSpawnFromPool, OnReturnToPool로 상태 초기화
  • 비활성화 방법 - Hidden, Collision 비활성화, Tick 비활성화, 월드 외부로 이동
  • PrewarmPool - 로딩 시 미리 생성하여 런타임 히치 방지
주의사항

풀에서 가져온 객체는 반드시 상태를 초기화해야 합니다. 이전 사용의 데이터가 남아있으면 버그가 발생할 수 있습니다.

PRACTICE

도전 과제

배운 내용을 직접 실습해보세요

실습 1: 프로젝타일 오브젝트 풀 구현

RPG 원거리 공격 프로젝타일을 위한 오브젝트 풀을 구현하세요. 미리 N개의 프로젝타일을 SpawnActor로 생성하고, SetActorHiddenInGame(true)로 비활성화해 두었다가 재사용하세요.

실습 2: 이펙트 풀링 시스템

UNiagaraComponent를 풀링하여 히트 이펙트, 힐 이펙트 등을 재사용하세요. Activate()/Deactivate()로 이펙트를 제어하고, 풀 고갈 시 동적 확장 로직을 추가하세요.

심화 과제: 범용 오브젝트 풀 매니저

TSubclassOf를 키로 하는 범용 풀 매니저를 UWorldSubsystem으로 구현하세요. GetFromPool(), ReturnToPool() 인터페이스를 제공하고, 풀 크기 자동 조절과 통계 수집 기능을 추가하세요.