본문으로 건너뛰기
Unreal Engine C++ 기초집LESSON 22

Object Pool 기초

난이도초급
예상 시간45분
선수지식21강의 EnemySpawner

22강. Object Pool 기초

1. 이번 강의에서 해결할 문제

적이 빠르게 생성·제거되는 상황에서 프레임 시간이 순간적으로 튑니다. 무조건 Pool을 적용하지 않고 측정 결과를 근거로 생성 비용과 메모리 사용량을 교환합니다.

2. 학습 목표

3. 핵심 개념

Pool은 Actor를 미리 만들고 숨김·충돌 비활성 상태로 보관하다 필요할 때 다시 사용합니다. Epic의 일반 성능 고려사항은 Spawn과 Destroy가 대부분의 상황에서는 충분히 안전하며 제한된 CPU 예산에서 Pool이 대안이 될 수 있다고 설명합니다.

Pool은 항상 빠른 정답이 아닙니다. 최대 수만큼 메모리를 계속 사용하고, 체력, Timer, AI 이동, Delegate와 파티클 상태를 빠짐없이 초기화해야 합니다. 먼저 Unreal Insights나 stat 명령으로 Spike가 재현되는지 확인합니다.

4. 단계별 실습

파일 경로: Source/TopDownSurvival/Systems/EnemyPool.h

EnemyPool.h
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "EnemyPool.generated.h"

class AEnemyBase;
class UEnemyDataAsset;

UCLASS()
class TOPDOWNSURVIVAL_API AEnemyPool : public AActor
{
GENERATED_BODY()

public:
AEnemyPool();
AEnemyBase* Acquire(UEnemyDataAsset* Data, const FTransform& Transform);
void Release(AEnemyBase* Enemy);

protected:
virtual void BeginPlay() override;

UPROPERTY(EditDefaultsOnly, Category = "Pool", meta = (ClampMin = "1"))
int32 InitialSize = 20;

UPROPERTY(EditDefaultsOnly, Category = "Pool")
TSubclassOf<AEnemyBase> PooledClass;

private:
UPROPERTY()
TArray<TObjectPtr<AEnemyBase>> PooledEnemies;
};

파일 경로: Source/TopDownSurvival/Systems/EnemyPool.cpp

EnemyPool.cpp
#include "Systems/EnemyPool.h"

#include "Data/EnemyDataAsset.h"
#include "Enemies/EnemyBase.h"

AEnemyPool::AEnemyPool()
{
PrimaryActorTick.bCanEverTick = false;
}

void AEnemyPool::BeginPlay()
{
Super::BeginPlay();
if (!PooledClass) return;

for (int32 Index = 0; Index < InitialSize; ++Index)
{
FActorSpawnParameters Params;
Params.SpawnCollisionHandlingOverride =
ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
AEnemyBase* Enemy = GetWorld()->SpawnActor<AEnemyBase>(
PooledClass, FVector::ZeroVector, FRotator::ZeroRotator, Params);
if (Enemy)
{
Release(Enemy);
PooledEnemies.Add(Enemy);
}
}
}

AEnemyBase* AEnemyPool::Acquire(
UEnemyDataAsset* Data, const FTransform& Transform)
{
for (AEnemyBase* Enemy : PooledEnemies)
{
if (IsValid(Enemy) && Enemy->IsAvailableForPool())
{
Enemy->InitializeForReuse(Data, Transform);
Enemy->SetActorHiddenInGame(false);
Enemy->SetActorEnableCollision(true);
return Enemy;
}
}
return nullptr;
}

void AEnemyPool::Release(AEnemyBase* Enemy)
{
if (!IsValid(Enemy)) return;
Enemy->PrepareForPool();
Enemy->SetActorEnableCollision(false);
Enemy->SetActorHiddenInGame(true);
}

EnemyBase에는 재사용 상태를 명시적으로 추가합니다.

EnemyBase.h · Pool API
public:
void InitializeForReuse(UEnemyDataAsset* Data, const FTransform& Transform);
void PrepareForPool();
bool IsAvailableForPool() const { return bInPool; }

private:
bool bInPool = false;
EnemyBase.cpp · Pool 상태 초기화
void AEnemyBase::InitializeForReuse(
UEnemyDataAsset* Data, const FTransform& Transform)
{
bInPool = false;
SetActorTransform(Transform);
InitializeFromData(Data);
TargetPawn = UGameplayStatics::GetPlayerPawn(this, 0);
GetWorldTimerManager().SetTimer(
ThinkTimerHandle, this, &AEnemyBase::Think, ThinkInterval, true);
}

void AEnemyBase::PrepareForPool()
{
bInPool = true;
GetWorldTimerManager().ClearTimer(ThinkTimerHandle);
TargetPawn = nullptr;

if (AAIController* AI = Cast<AAIController>(GetController()))
{
AI->StopMovement();
}
}

HealthComponent의 ConfigureHealth가 현재 체력을 초기화하고 BeginPlay의 Death Delegate 바인딩은 한 번만 유지합니다. 이 초기화가 불완전하면 이전 생명의 상태가 다음 생명에 새어 나옵니다.

5. 코드가 동작하는 이유

BeginPlay에서 최대 예상 수 일부를 미리 생성해 플레이 중 초기화 Spike를 앞당깁니다. Acquire는 숨겨진 Actor를 찾고 모든 런타임 상태를 초기화한 뒤 활성화합니다. Release는 Destroy 대신 비활성화합니다. 빈 Pool에서 null을 반환하므로 Spawner는 생성 건너뛰기나 확장 정책을 선택해야 합니다.

6. 자주 하는 실수와 해결법

7. 직접 실습

8. 이해 점검 질문 3개

9. 핵심 요약

MINI QUIZ

Object Pool 기초 미니 퀴즈

선택 즉시 정답과 해설을 확인할 수 있습니다. 결과는 이 브라우저에만 저장됩니다.

0 / 2
  1. 문제 1“Object Pool 기초” 실습 결과를 해석하는 올바른 기준은 무엇인가요?
  2. 문제 2“Object Pool 기초”에서 ‘숨김만 하고 충돌·AI·Timer 유지’ 문제가 생겼습니다. 가장 알맞은 진단 또는 대응은 무엇인가요?
LESSON STATUS

학습을 마쳤나요?

직접 실습과 점검 질문까지 확인한 뒤 완료로 표시하세요.

22강. Object Pool 기초 미완료 상태