아마 이번 게시글이 FPS Shooting 태그의 마지막 게시글이 될 것 같다.
이번 시간에는 기존 SpawnActor로 생성하던 발사체 객체에 더 효율적인 오브젝트 풀링을 적용해보자.
1. PoolableActor 생성
오브젝트 풀링의 적용 대상은 Actor이므로 Actor를 상속받는 PoolableActor 클래스를 만들어준다.
PoolableActor.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "PoolableActor.generated.h"
UCLASS()
class PROJECT_4_API APoolableActor : public AActor
{
GENERATED_BODY()
public:
APoolableActor();
protected:
virtual void BeginPlay() override;
public:
virtual void Tick(float DeltaTime) override;
private:
bool IsActive;
public:
virtual void Activate();
virtual void Deactivate();
bool GetIsActive() const { return IsActive; }
};
PoolableActor.cpp
#include "System/PoolableActor.h"
APoolableActor::APoolableActor()
{
PrimaryActorTick.bCanEverTick = true;
}
void APoolableActor::BeginPlay()
{
Super::BeginPlay();
}
void APoolableActor::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}
void APoolableActor::Activate()
{
IsActive = true;
SetActorHiddenInGame(false);
SetActorEnableCollision(true);
SetActorTickEnabled(true);
}
void APoolableActor::Deactivate()
{
IsActive = false;
SetActorHiddenInGame(true);
SetActorEnableCollision(false);
SetActorTickEnabled(false);
}
PoolableActor 클래스는 두 가지 가상함수를 갖는다. 언리얼도 유니티처럼 SetActive같은 편리한 함수가 있으면 좋겠지만 없기 때문에 직접 성능에 영향이 가는 속성들을 중지해주어야한다.
Activate와 Deactivate 함수에서는 Actor클래스가 갖고있는 공통적인 속성을 켜거나 중지시켜 해당 액터가 사라진 것처럼 보이게 하며, 오브젝트 풀링을 적용할 클래스는 해당 PoolableActor를 상속받아 Activate와 Deactivate를 재정의 하여 액터에서 사용중인 컴포넌트나 속성을 켜거나 중지시켜야 한다.
2. GenericPool 클래스 생성
PoolableActor에 대해 오브젝트 풀링을 할 GenericPool 클래스를 만들어 주자. 해당 클래스는 PoolableActor를 상속받는 모든 클래스에 대해 오브젝트 풀을 만들고 적용할 수 있도록 만들었다. 템플릿 함수가 들어가므로 헤더에 구현부가 작성되어야 하기 때문에 cpp 파일에는 아무것도 작성하지 않았다.
정말 기본적인 오브젝트 풀링의 기능만 하며 이용시 InitPool 함수를 호출해 사이즈를 초기화 해주어야 한다.
GenericPool.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "PoolableActor.h"
#include "GenericPool.generated.h"
UCLASS()
class PROJECT_4_API AGenericPool : public AActor
{
GENERATED_BODY()
public:
AGenericPool();
protected:
virtual void BeginPlay() override;
private:
UPROPERTY(EditAnywhere, Category = "Pool")
TArray<APoolableActor*> ObjectPool;
public:
template <typename T>
void InitPool(const int32 Size)
{
UClass* ObjectClass = T::StaticClass();
for (int32 i = 0; i < Size; ++i) {
FActorSpawnParameters SpawnParams;
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
APoolableActor* NewActor = GetWorld()->SpawnActor<APoolableActor>(ObjectClass, FVector::ZeroVector, FRotator::ZeroRotator, SpawnParams);
NewActor->Deactivate();
ObjectPool.Add(NewActor);
}
}
template <typename T>
T* GetObject() {
UClass* ObjectClass = T::StaticClass();
for (APoolableActor* Object : ObjectPool)
{
if (!Object->GetIsActive())
{
Object->Activate();
return Cast<T>(Object);
}
}
// 풀에 사용 가능한 오브젝트가 없으면 새로 생성
T* NewObject = GetWorld()->SpawnActor<T>(ObjectClass);
ObjectPool.Add(NewObject);
return NewObject;
}
template <typename T>
void ReturnObject(T* Object) {
Object->Deactivate();
}
};
3. 기존 Projectile 클래스에 적용하기
이제 이를 기존 Projectile 클래스에 적용해보자.
우선 부모를 AActor에서 APoolableActor로 변경해준다.
Projectile.h
class PROJECT_4_API AProjectile : public APoolableActor
{
// 이전 코드와 동일
public:
void Activate() override;
void Deactivate() override;
void Initialize(const FVector& Location, const FVector& ShootDirection, const uint32 Speed);
}
Projectile.cpp
void AProjectile::Activate()
{
Super::Activate();
GetWorld()->GetTimerManager().SetTimer(
LifeTimerHandle, this, &AProjectile::Deactivate, 5.0f, false
); // 발사 후 5초가 지나면 Deactivate
ProjectileMovementComponent->SetActive(true);
}
void AProjectile::Deactivate()
{
Super::Deactivate();
GetWorld()->GetTimerManager().ClearTimer(LifeTimerHandle);
ProjectileMovementComponent->SetActive(false);
}
각각 Activate와 Deactivate를 재정의하여 Activate에는 Timer를 사용하여 발사 후 5초가 지나면 다시 Deactivate가 되는 코드와 ProjectileMovementComponent 를 활성화 하는 코드를, Deactivate에서는 타이머 핸들러를 중지하고 ProjectileMovementComponent를 비활성화 하는 코드를 추가하였다.
void AProjectile::OnHit(UPrimitiveComponent* HitComponent, AActor* OtherActor, UPrimitiveComponent* OtherComponent, FVector NormalImpulse, const FHitResult& Hit)
{
// 기존 코드
// Destroy() ->
Deactivate();
}
이제 Projectile 객체가 어딘가에 Hit 했을때 호출되는 함수인 OnHit 함수의 마지막에 액터가 Destroy 되는 것이 아니라 Deactivate 되도록 하면 준비는 끝났다.
4. 기존 발사 로직 수정
앞선 과정을 모두 잘 따라왔으면 SpawnActor로 Projectile을 생성하는 WeaponComponent에서 ObjectPool 클래스를 선언하고 BeginPlay에서 초기화해준다.
WeaponComponent.h
class PROJECT_4_API UWeaponComponent : public UActorComponent{
//기존 코드
private:
AGenericPool *ProjectilePool;
}
WeaponComponent.cpp
void UWeaponComponent::BeginPlay()
{
//기존 코드
// Object Pool 초기화
ProjectilePool = GetWorld()->SpawnActor<AGenericPool>();
ProjectilePool->InitPool<AProjectile>(10);
}
그 다음 Projectile을 SpawnActor를 생성하는 부분을 다음과 같이 바꾸어준다. 여기서 주의해야 할 점은 SpawnActor에는 생성하는 Actor의 방향과 위치를 초기화해주지만 오브젝트 풀링을 적용하면서 미리 생성된 위치에 Activate된 액터가 존재하기 때문에 위치와 방향을 초기화해주어야 한다.
void UWeaponComponent::SpawnProjectile(const FVector& MuzzleLocation, const FVector& ShootDirection)
{
AProjectile* Projectile = ProjectilePool->GetObject<AProjectile>();
if (Projectile)
{
Projectile->Activate();
Projectile->Initialize(MuzzleLocation, ShootDirection, WeaponData->ProjectileSpeed, WeaponData->Damage);
}
}
Projectile.cpp
void AProjectile::Initialize(const FVector& Location, const FVector& ShootDirection, const uint32 Speed)
{
SetActorLocation(Location);
ProjectileMovementComponent->InitialSpeed = Speed;
ProjectileMovementComponent->MaxSpeed = Speed;
ProjectileMovementComponent->Velocity = ShootDirection * ProjectileMovementComponent->InitialSpeed;
ProjectileMovementComponent->SetActive(true);
}
여기까지 구현이 완료되었으면 게임을 실행하면 생성된 GenericPool 액터와 미리 생성된 Projectile 액터들을 볼 수가 있다.
'Unreal 5 > FPS Shooting' 카테고리의 다른 글
4. [Unreal 5 / C++] 반동(Camera Shake)과 연사 (2) | 2025.01.07 |
---|---|
3. [Unreal 5 / C++] 동적 크로스헤어 구현 (1) | 2025.01.03 |
2. [Unreal 5 / C++] 발사체 구현 하기 (1) | 2025.01.02 |
1. [Unreal 5 / C++] 슈팅 구현 하기 (0) | 2024.12.31 |