[UE5] 언리얼5 C++ - Object Pooling 시스템

AnJH·2024년 7월 10일
0

Object Pooling

다수의 객체가 생성되고 파괴되는 과정에서 발생하는 프레임드랍을 최소화하기 위한 기법인 Object Pooling에 대해 설명하고자 한다.

객체가 생성되고 파괴될 때 메모리에 소요되는 비용이 상당하기에 이를 막기 위해 한 번 생성할 때 다수의 객체를 미리 생성해두고 플레이어의 눈에는 감춰두어 보이지 않게 하는 것이다.

만약 객체가 파괴 될 일이 발생한다면 Level에서 Destroy하는 것이 아닌, Hidden시켜서 이후에 재사용을 가능하도록 한다.

이에 대한 시스템은 블루프린트나 C++만을 사용해서 개발할 수도 있으나 범용성을 넓히기 위해 C++과 블루프린트 두 곳에서 사용이 가능한 시스템에 대한 코드를 설명할 것이다.

  • Unreal Interface.h
#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "Poolable.generated.h"

UINTERFACE(MinimalAPI)
class UPoolable : public UInterface
{
	GENERATED_BODY()
};

class OBJPOOLING_API IPoolable
{
	GENERATED_BODY()

public:

	UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Poolable")
	void OnSpawnFromPool();

	UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Poolable")
	void OnReturnToPool();
};

인터페이스란 함수의 이름만 만들어두고 해당 함수를 사용하는 곳에서 정의를 하여 사용하는 것이다.

이를 통해 공통적으로 사용될 수 있는 함수의 재사용성을 높힌다.

OnSpawnFromPool() 함수는 특정 조건에서 객체가 Spawn 될 때 호출되는 함수이다.
OnReturnToPool() 함수는 특정 조건에서 객체가 사라질 때 호출되는 함수이다.

UFUNCTION 프로퍼티의 키워드로 BlueprintNativeEvent를 사용하여 블루프린트와 C++에서 모두 편집이 가능하도록 하였고, BlueprintCallable를 통해 블루프린트에서도 해당 함수를 호출할 수 있도록 하였다.

  • PoolSubSystem.h
#include "CoreMinimal.h"
#include "Subsystems/WorldSubsystem.h"
#include "PoolSubSystem.generated.h"

USTRUCT()
struct FPoolArray {
	GENERATED_BODY()

	UPROPERTY()
	TArray<AActor*> ObjectPool;

    bool IsEmpty() {
        return ObjectPool.IsEmpty();
    }

    AActor* Pop() {
        return ObjectPool.Pop();
    }

    void Add(AActor* ActorToAdd) {
        ObjectPool.Add(ActorToAdd);
    }
};

UCLASS()
class OBJPOOLING_API UPoolSubSystem : public UWorldSubsystem
{
	GENERATED_BODY()
	
public:
    UFUNCTION(BlueprintCallable, Category = "Pool SubSystem")
	void SpawnFromPool(TSubclassOf<AActor> PoolClass, FVector Location, FRotator Rotation, AActor*& SpawnedActor);

	UFUNCTION(BlueprintCallable, Category = "Pool SubSystem")
	void ReturnToPool(AActor* Poolable);

    UFUNCTION(BlueprintCallable, Category = "Pool SubSystem")
    void InitializePool(TSubclassOf<AActor> PoolClass, int32 MaxSize);

private:
	TMap<UClass*, FPoolArray> ObjectPools;

    AActor* GetActorFromPool(TSubclassOf<AActor> PoolClass, FVector Location, FRotator Rotation);
};

직접적인 Object Pooling 시스템을 담당하는 클래스이다.

WorldSubsystem을 상속받아 만들었으며, 이는 Subsystem을 사용할 경우 블루프린트에 곧바로 Access가 가능하기에 자유로운 블루프린트와 C++의 연동을 위해 이를 사용하였다.

인터페이스 내 선언해둔 함수에 접근하기 위한 SpawnFromPool()과 ReturnToPool()함수
객체를 미리 생성해두기 위한 InitializePool() 함수
Pool 내에서 비활성화된 객체를 가져오기 위한 GetActorFromPool ()함수를 선언한다.

객체를 담을 ObjectPools변수는 TMap으로 선언하며 Actor 클래스 타입을 키로 사용하여 FPoolArray 배열에 저장할 예정이다.
FPoolArray 배열은 상단에 USTRUCT() 프로퍼티로 구조체로 선언되어 있으며 ObjectPool 배열을 멤버변수로 갖고,
Pool 내부가 비어있는지 확인하는 IsEmpty()함수, Pool 내부의 객체를 빼고 더하는 Pop(), Add()함수로 구성되어 있다.

  • PoolSubsystem.CPP
# 초기화 함수
void UPoolSubSystem::InitializePool(TSubclassOf<AActor> PoolClass, int32 MaxSize)
{
    FPoolArray& ObjectPool = ObjectPools.FindOrAdd(PoolClass);
    for (int32 i = 0; i < MaxSize; ++i) {
        FActorSpawnParameters SpawnParams;
        SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
        AActor* NewActor = GetWorld()->SpawnActor<AActor>(PoolClass, FVector::ZeroVector, FRotator::ZeroRotator, SpawnParams);
        if (NewActor && PoolClass.Get()->ImplementsInterface(UPoolable::StaticClass())) {
            IPoolable::Execute_OnReturnToPool(NewActor);
            NewActor->SetActorHiddenInGame(true);
            NewActor->SetActorEnableCollision(false);
            ObjectPool.Add(NewActor);
        }
    }
}

먼저 Pool 내부를 채우기 위한 초기화 함수이다.

사용자가 지정한 MaxSize만큼 객체를 Spawn하며 이때 SpawnParameter는 충돌과 관련없이 어떤 일이 있어도 스폰을 하겠다는 AlwaySpawn으로 지정하였다.

이에 따라 Spawn되는 위치는 모두 ZeroVector로 지정했으며 생성된 모든 객체는 SetActorHiddenInGame() 함수를 통해 Level내에서 사용자에게 보이지 않도록 감춰주었다.

충돌 또한 Spawn이 되었을 때만 충돌이 발생하여야 하기 떄문에 SetActorEnableCollision() 함수를 통해 NoCollision 상태로 만들어두었다.

이렇게 세팅된 하나의 객체는 Pool 내부로 추가되고, 이렇게 MaxSize만큼 반복문이 실행되어 Pool 내부가 채워지는 것이다.

# 객체 소환
void UPoolSubSystem::SpawnFromPool(TSubclassOf<AActor> PoolClass, FVector Location, FRotator Rotation, AActor*& SpawnedActor)
{
    SpawnedActor = GetActorFromPool(PoolClass, Location, Rotation);
}

AActor* UPoolSubSystem::GetActorFromPool(TSubclassOf<AActor> PoolClass, FVector Location, FRotator Rotation)
{
    FPoolArray& ObjectPool = ObjectPools.FindOrAdd(PoolClass);
    if (!ObjectPool.IsEmpty())
    {
        AActor* Actor = ObjectPool.Pop();
        if (Actor) {
            Actor->SetActorLocationAndRotation(Location, Rotation);
            Actor->SetActorHiddenInGame(false);
            IPoolable::Execute_OnSpawnFromPool(Actor);
            return Actor;
        }
    }

    FActorSpawnParameters SpawnParams;
    SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
    AActor* NewActor = GetWorld()->SpawnActor<AActor>(PoolClass, Location, Rotation, SpawnParams);
    if (NewActor && PoolClass.Get()->ImplementsInterface(UPoolable::StaticClass())) {
        IPoolable::Execute_OnSpawnFromPool(NewActor);
    }
    return NewActor;
}

다음은 객체를 Spawn하는 함수이다.

사용자가 지정한 Class, Location, Rotation에 맞춰 객체가 Spawn되며 이때 GetActorFromPool() 함수를 호출하게 된다.

GetActorFromPool() 함수 내부에서는 Pool이 비어있지 않다면 Pool 내부의 객체를 하나 지우고, 이를 화면에 띄운다.

만약 Pool 내부가 비어있다면 객체를 새로 생성한다.

# 객체 반환
void UPoolSubSystem::ReturnToPool(AActor* Poolable)
{
    if (!Poolable) return;

    UClass* ActorClass = Poolable->GetClass();

    if (ActorClass->ImplementsInterface(UPoolable::StaticClass()))
    {
        IPoolable::Execute_OnReturnToPool(Poolable);
        Poolable->SetActorHiddenInGame(true);
        Poolable->SetActorEnableCollision(false);
        FPoolArray& ObjectPool = ObjectPools.FindOrAdd(ActorClass);
        ObjectPool.Add(Poolable);
    }
    else
    {
        Poolable->Destroy();
    }
}

사용자가 지정한 특정 조건을 만족했을 때, 객체를 반환시키는 함수이다.

객체를 Level에서 삭제시키는 것이 아닌 Hidden으로 만들고 Collision을 없앰으로써 마치 사용자에게 삭제된 것과 같은 효과를 주며 이는 실질적으로 Pool 내부로 다시 들어가게 된다.

따라서 이처럼 구현된 동작방식은 이미 생성되어있는 객체를 돌려 쓰는 것과 마찬가지기에 성능 향상에 큰 도움이 된다.

  • PoolableActor.CPP
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Poolable.h"
#include "PoolableActor.generated.h"


UCLASS()
class OBJPOOLING_API APoolableActor : public AActor, public IPoolable
{
	GENERATED_BODY()
	
public:	
	// Sets default values for this actor's properties
	APoolableActor();

protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;

public:	
	// Called every frame
	virtual void Tick(float DeltaTime) override;

public:
	virtual void OnSpawnFromPool_Implementation() override;
	virtual void OnReturnToPool_Implementation() override;

	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components")
	class UBoxComponent* RootBoxComponent;
};

마지막으로 직접적으로 Spawn될 객체를 만들었다.

인터페이스를 선언할 당시에 BlueprintNativeEvent로 만들었기에 해당 함수를 사용할 땐, _Implementation을 붙여서 사용해야 한다.

이렇게 만든 시스템에 대한 간단한 테스트를 진행하기 위해 블루프린트 노드를 구성해보았다.
테스트를 진행하기 위해 다음과 같이 한 것임으로 여러 방식으로 변형하여 사용할 수 있다.

  • PlayerCharacter

플레이어가 생성될 때 먼저 Pool 내부를 초기화시킨다.

SubSystem 내부에 함수를 선언해두고 BlueprintCallable로 만들었기에 위와 같이 사용이 가능하다.

노드에는 초기화시킬 객체와 객체의 수를 지정해준다.

1번을 누르면 객체가 Spawn되도록 해보았다.

미리 Pool 내부에 생성해둔 객체를 클래스로 지정한다.
객체는 한 번에 20개가 생성될 것이고, 사용자가 지정해준 Location과 Rotation값에 생성된다.
Spawn이 된 객체는 Collision을 지니고 있어야 하기에 ActorEnableCollision을 True로 해주었다.

  • PoolableActor

Spawn이 된 객체의 반환조건이다.

간단하게 Player와 충돌했을 경우 Pool 내부로 반환되도록 설계해보았다.

0개의 댓글