카테고리 없음

언리얼 C++) 엔진 최적화: 오브젝트 풀링 (Object Pooling)

eclipse2 2026. 6. 4. 19:46

언리얼 엔진 최적화: 오브젝트 풀링 (Object Pooling)

1. 오브젝트 풀링(Object Pooling)이란?

  • 개념: 오브젝트가 필요할 때마다 새로 만들고(생성), 다 쓰면 파괴(삭제)하는 대신, 게임 시작 시 미리 왕창 만들어 두고(Pool) 필요할 때 꺼내 쓰고 다 쓰면 다시 보관함에 반납하는 기법입니다.
  • 왜 사용하는가? (도입 이유)
    • 동적 생성(메모리 할당)과 삭제(가비지 컬렉션)는 CPU를 많이 쓰고 느린 작업입니다.
    • 총알 난사처럼 짧은 순간에 생성/삭제가 미친 듯이 반복되면 치명적인 프레임 드랍(렉)이 발생합니다.
  • 작동 방식 (총알 예시):
    1. 게임 시작 전, 보관함(Pool)에 총알 100개를 미리 만들어 둡니다. (비활성화/숨김 상태)
    2. 플레이어가 총을 쏘면, 보관함에서 총알 하나를 꺼내 총구 위치로 순간이동시키고 활성화합니다.
    3. 총알이 목표물에 맞거나 멀리 날아가면, 파괴하지 않고 다시 비활성화하여 보관함으로 돌려보냅니다(Recycle).

2. 오브젝트 풀링 시스템 설계 구조 (3단계)

이번에 구현한 오브젝트 풀링 시스템은 3개의 주요 스크립트로 역할이 나뉘어 있습니다.

① FPooledObjectData (설정 데이터 구조체)

풀(Pool)에 어떤 액터를 몇 개나 만들어 둘 것인지 결정하는 레시피 역할입니다.

C++
 
USTRUCT(BlueprintType)
struct FPooledObjectData
{
	GENERATED_BODY()

	FPooledObjectData() { PoolSize = 1; CanGrow = false; ActorName = "default"; }

	UPROPERTY(EditAnywhere) TSubclassOf<AActor> ActorTemplate; // 스폰할 액터 (붕어빵 틀)
	UPROPERTY(EditAnywhere) int PoolSize;                      // 미리 만들어둘 개수
	UPROPERTY(EditAnywhere) bool CanGrow;                      // 모자라면 더 만들 것인가?
	UPROPERTY(EditAnywhere) FString ActorName;                 // 액터의 이름
};

② UPooledObject (추적 관리용 컴포넌트)

보관함에서 찍어낸 액터들에 찰싹 달라붙어, 자신이 현재 사용 중인지 아닌지를 기록하고 반납을 요청하는 네임택 역할을 합니다.

C++
 
UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class UPooledObject : public UActorComponent
{
	GENERATED_BODY()
public:	
	void Init(class AMyObjectPool* Owner); // 자신을 관리하는 보관함(Pool) 기억하기
    
	UFUNCTION(BlueprintCallable)
	void RecycleSelf(); // 다 썼으니 나 좀 다시 집어넣어 달라고 요청

	UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly)
	bool bIsPoolActive; // 현재 맵에서 활성화되어 날아가고 있는가?

private:
	TObjectPtr<class AMyObjectPool> ObjectPool; // 주소 기억용
};

③ AMyObjectPool (오브젝트 풀 매니저 액터)

레시피(FPooledObjectData)를 보고 실제로 액터들을 생성하여 품고 있는 보관함 본체입니다. 플레이어가 총알을 요구하면 꺼내주고, 반납을 처리합니다.

C++
 
USTRUCT(BlueprintType)
struct FSingleObjectPool
{
	GENERATED_BODY()
	// 만들어둔 컴포넌트들을 배열로 묶어서 관리 (총알용, 파티클용 등 분리)
	UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly)
	TArray<TObjectPtr<class UPooledObject>> PooledObjects; 
};

UCLASS()
class AMyObjectPool : public AActor
{
	GENERATED_BODY()
public:	
    // 꺼내주기와 반납받기 함수들
	UFUNCTION(BlueprintCallable) AActor* GetPooledActor(FString Name);
	UFUNCTION(BlueprintCallable) void RecyclePooledObject(class UPooledObject* PoolCompRef);
	UFUNCTION(BlueprintCallable) void RecycleActor(AActor* PooledActor);
	
	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	TArray<FPooledObjectData> PooledObjectData; // 에디터에서 설정할 레시피들

protected:
	UPROPERTY(VisibleAnywhere, BlueprintReadWrite)
	TArray<FSingleObjectPool> Pools; // 실제 생성된 오브젝트들의 보관소
};

3. 핵심 로직: 게임 시작 시 미리 만들어두기 (BeginPlay)

가장 중요한 풀(Pool) 초기화 과정입니다. 레시피를 순회하며 런타임에 액터를 생성하고, 그 액터에 동적으로 UPooledObject 컴포넌트를 붙여주는 작업이 진행됩니다.

C++
 
void AMyObjectPool::BeginPlay()
{
	Super::BeginPlay();
	
	FActorSpawnParameters SpawnParams;

    // 1. 등록된 레시피(풀 데이터) 종류만큼 반복
	for (int PoolIndex = 0; PoolIndex < PooledObjectData.Num(); PoolIndex++)
	{
		FSingleObjectPool CurrentPoolIndex;
		SpawnParams.Name = FName(*PooledObjectData[PoolIndex].ActorName);
		SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn; // 겹쳐도 무조건 스폰

        // 2. 설정한 PoolSize(개수)만큼 반복해서 액터 찍어내기
		for (int ObjectIndex = 0; ObjectIndex < PooledObjectData[PoolIndex].PoolSize; ObjectIndex++)
		{
            // 액터 스폰
			AActor* SpawnedActor = GetWorld()->SpawnActor(PooledObjectData[PoolIndex].ActorTemplate, &FVector::ZeroVector, &FRotator::ZeroRotator, SpawnParams);
			SpawnedActor->SetActorLabel(SpawnedActor->GetName()); // 에디터에서 보기 좋게 이름 세팅

            // 🌟 동적으로 액터 컴포넌트 생성 및 부착 (가장 중요)
			UPooledObject* PoolComp = NewObject<UPooledObject>(SpawnedActor);
			PoolComp->RegisterComponent();
			SpawnedActor->AddInstanceComponent(PoolComp);

            // 컴포넌트 초기화 및 보관함에 담기
			PoolComp->Init(this);
			CurrentPoolIndex.PooledObjects.Add(PoolComp);

            // 3. 당장 쓸 게 아니니까 눈에 안 보이게 숨기고 기능 끄기
			SpawnedActor->SetActorHiddenInGame(true);
			SpawnedActor->SetActorEnableCollision(false);
			SpawnedActor->SetActorTickEnabled(false);

            // 보관함(매니저) 액터에 자식으로 깔끔하게 붙여두기
			SpawnedActor->AttachToActor(this, FAttachmentTransformRules::SnapToTargetNotIncludingScale);
		}
		Pools.Add(CurrentPoolIndex);
	}
}