[Unreal_C++_DarkSoul]#14_발사체
발사체 클래스를 구현합니다.
Overview
- 개요 및 설계
- 코드
- 결과 영상
#0. 개요 및 설계
1. 목적
- Projectile 클래스 구현
- Projectile Spawner 컴포넌트(액터 컴포넌트)
- Object Pooling 구현
2. 설계
- 단일 공격 무기 클래스 내 발사체들을 따로 관리하는 발사체 관리 컴포넌트를 구현합니다.
- 발사체 관리 컴포넌트는 해당 무기 클래스에서 사용하는 발사체들의 객체 풀링을 구현합니다.
#1. 코드
1. Projectile.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Projectile.generated.h"
/* *********************************************************************************************************************
*
* [Remark_23-03-20]
*
* 목적: 충돌과 생명 주기 Delegate
*
* 설명:
* 1. FProjectileBeginOverlap : Projectile의 충돌 이벤트를 이에 동적 바인딩을 수행한 NoComboWeapon에 알립니다.
* 2. FProjectileEndLaunched : Porjectile의 생명 주기 이벤트를 이에 동적 바인딩을 수행한 액터 컴포넌트에 알립니다.
*
* *********************************************************************************************************************/
DECLARE_DYNAMIC_DELEGATE_ThreeParams(FProjectileBeginOverlap, class AActor*, InActor, EProjectileType, InType, uint8, InIndex);
DECLARE_DYNAMIC_DELEGATE_TwoParams(FProjectileEndLaunched, EProjectileType, InType, uint8, InIndex);
UCLASS()
class DARKSOUL_API AProjectile : public AActor
{
GENERATED_BODY()
public:
AProjectile();
protected:
virtual void BeginPlay() override;
protected:
UFUNCTION()
virtual void OnCollision() {}
UFUNCTION()
virtual void OffCollision() {}
public:
UFUNCTION()
virtual void StartLaunched();
UFUNCTION()
virtual void EndLaunched();
protected:
UFUNCTION()
virtual void OnCapsuleBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
public:
FProjectileBeginOverlap OnProjectileBeginOverlap;
FProjectileEndLaunched OnProjectileEndLaunched;
protected:
UPROPERTY(EditDefaultsOnly, Category = "Components")
class UProjectileMovementComponent* ProjectileMovement;
protected:
EProjectileType ProjectileType = EProjectileType::E_Max;
protected:
UPROPERTY(EditDefaultsOnly, Category = "Projectile")
bool bPenetrable = false;
private:
UPROPERTY(VisibleAnywhere, Category = "Life Span")
FTimerHandle OffVisibilityHandler;
UPROPERTY(VisibleAnywhere, Category = "Life Span")
FTimerHandle ExpireHandler;
UPROPERTY(EditDefaultsOnly, Category = "LifeSpan")
float OffVisibilityRate = 0.5f;
protected:
UPROPERTY(EditDefaultsOnly, Category = "Life Span")
float ExpireRate = 0.f;
protected:
/* **********************************************************
*
* [Remark_23-03-20]
*
* 목적: Object Pooling 정보를 담은 구조체 정의
*
* 설명: Object Pooling에 필요한 정보들을 해당 구조체에 저장합니다.
*
* **********************************************************/
struct PoolObjectInfo
{
public:
class AProjectile** Next = nullptr;
uint8 Index = -1;
bool IsActive = false;
bool IsLoaded = false;
}PoolObjectInfo_Struct;
public:
FORCEINLINE void SetIsPenetrable(bool InBool) { bPenetrable = InBool; }
FORCEINLINE bool GetIsPenetrable() { return bPenetrable; }
FORCEINLINE void SetExpireRate(float InFloat) { ExpireRate = InFloat; }
FORCEINLINE float GetExpireRate() { return ExpireRate; }
FORCEINLINE void SetIndex(uint8 InIndex) { PoolObjectInfo_Struct.Index = InIndex; }
FORCEINLINE uint8 GetIndex() { return PoolObjectInfo_Struct.Index; }
FORCEINLINE void SetNext(class AProjectile** InProjectilePtr ) { PoolObjectInfo_Struct.Next = InProjectilePtr; }
FORCEINLINE AProjectile** GetNext() { return PoolObjectInfo_Struct.Next; }
FORCEINLINE void SetIsActive(bool InBool) { PoolObjectInfo_Struct.IsActive = InBool; }
FORCEINLINE bool GetIsActive() { return PoolObjectInfo_Struct.IsActive; }
FORCEINLINE void SetIsLoaded(bool InBool) { PoolObjectInfo_Struct.IsLoaded = InBool; }
FORCEINLINE bool GetIsLoaded() { return PoolObjectInfo_Struct.IsLoaded; }
};
Details
- FPoolObjectInfo 유형의 구조체를 데이터 멤버로 선언합니다. 해당 구조체는 Projectile 객체를 관리하는 ProjectileSpawner 컴포넌트에서 객체 풀링을 구현하기 위해 필요한 속성입니다.
- 관통 여부, 그리고 생명 주기를 관리하는 Timer Handler 등을 데이터 멤버로 선언합니다.
2. AProjectile::EndLaunched()
/* ************************************************************************************************************
*
* [Remark_23-03-20]
*
* 목적: 생명 주기를 관리합니다.
*
* 설명:
* 1. OtherActor가 nullptr인 경우, Expire Rate에 의한 호출이므로, 정상적으로 End Launched가 호출됩니다.
* 2. OtherActor가 nullptr가 아닌 경우, 충돌에 의한 호출이므로, bPenetrate 변수를 통해 관통 발사체 여부를 체크합니다.
* 3. 관통형 발사체(bPenetrate)일 경우, 충돌이 발생해도 지속적으로 이동합니다.
* 4. 관통형 발사체(bPenetrate)가 아닐 경우, 충돌이 발생하면 멈춥니다.
*
* ************************************************************************************************************/
void AProjectile::EndLaunched()
{
// #0. Projectile Stop & Timer Stops
ProjectileMovement->SetVelocityInLocalSpace(FVector(0.f, 0.f, 0.f));
GetWorld()->GetTimerManager().ClearTimer(ExpireHandler);
// #1. Delegate
OnProjectileEndLaunched.ExecuteIfBound(ProjectileType, PoolObjectInfo_Struct.Index);
// #2. Attack Hit Actors 지워주기
AWeapon* OwnerWeapon = Cast<AWeapon>(GetOwner());
CheckTrue(OwnerWeapon == nullptr);
OwnerWeapon->OffCollision();
// #3. Visibility
GetWorld()->GetTimerManager().SetTimer(OffVisibilityHandler, FTimerDelegate::CreateLambda([&]() {
SetActorHiddenInGame(true);
}), OffVisibilityRate, false);
}
Details
- EndLaunched() 가상 함수는 Projectile의 충돌 혹은 생명 주기의 마지막에 호출되는 함수입니다.
- EndLaunched() 함수는 크게 네 가지를 구현합니다.
- 하나는 발사체의 Velocity를 ZeroVector로 설정해, 움직임을 멈춥니다.
- 두 번째는 Projectile Spawner 컴포넌트에게 해당 이벤트를 알릴 Delegate를 실행합니다.
- 세 번째는 충돌 시 피해 전달의 중복을 방지하기 위해 관리하던 피 충돌 액터들을 담은 배열을 비워줍니다.
- 마지막은 Visibility를 꺼줍니다.
3. ProjectileSpawnerComponent.h
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "Projectiles/Projectile.h"
#include "Global/Custom_Structs.h"
#include "C_ProjectileSpawnerComponent.generated.h"
UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class DARKSOUL_API UC_ProjectileSpawnerComponent : public UActorComponent
{
GENERATED_BODY()
public:
UC_ProjectileSpawnerComponent();
protected:
virtual void BeginPlay() override;
// ************************************************************************************
// Method
// ************************************************************************************
private:
void LoadProjectileInfoFromDataTable(TMap<EProjectileType, FProjectileInfo>& OutMap);
public:
UFUNCTION()
void CreatePool(TMap<EProjectileType, FProjectileInfo>& OutMap);
// No Reload
void TeleportProjectileToSpawnLocation(EProjectileType InType, class USkeletalMeshComponent** InComponent, const FName& InName);
// Reload
void AttachProjectile(EProjectileType InType, class USkeletalMeshComponent** InComponent, const FName& InName);
public:
void StartLaunchProjectile(EProjectileType InType);
UFUNCTION()
void EndLaunchedProjectile(EProjectileType InType, uint8 InIndex);
// ************************************************************************************
// Data Member
// ************************************************************************************
private:
UPROPERTY(EditDefaultsOnly, Category = "Projectile")
class UDataTable* DataTable_ProjectilesInfo;
TMap<EProjectileType, TArray<AProjectile*>> MProjectilePool;
TMap<EProjectileType, class AProjectile**> PoolHeads;
public:
FORCEINLINE TMap<EProjectileType, TArray<AProjectile*>> GetProjectilePool() { return MProjectilePool; }
};
Details
- "MProjectilePool"은 TMap 유형의 컨테이너로 발사체의 타입과 발사체 객체를 담은 배열을 한 쌍으로 저장할 수 있도록 했습니다.
- "PoolHeads"도 역시 TMap 유형으로 다음으로 발사할 발사체를 찾기 위한 탐색 작업을 최적화하기 위해 "연결 리스트" 자료구조를 활용했습니다. 다음 발사할 발사체를 찾기 위해서 매번 배열을 탐색하는 비효율적인 작업을 대신해 다음으로 발사 가능한, 즉 "Active"상태가 아닌 발사체를 가리키는 Head를 정의합니다. 그리고, 이미 발사되어 생명 주기가 끝난 발사체를 Head->Next로 설정해주었습니다. 각 발사체가 발사되는 시간과 생명 주기가 끝나는 평균 시간대를 계산해 객체 풀의 사이즈를 측정했습니다. 결과적으로, 다음 발사할 발사체가 없는 사고를 미연에 방지했습니다.
4. ProjectileSpawnerComponent::BeginPlay()
void UC_ProjectileSpawnerComponent::BeginPlay()
{
Super::BeginPlay();
TMap<EProjectileType, FProjectileInfo> MProjectileInfos;
// #0. Load
LoadProjectileInfoFromDataTable(MProjectileInfos);
// #1. Init
TArray<EProjectileType> TmpArr;
MProjectileInfos.GenerateKeyArray(TmpArr);
for (uint8 i = 0; i < TmpArr.Num(); i++)
{
TArray<AProjectile*> TmpArray2;
TmpArray2.Init(nullptr, MProjectileInfos[TmpArr[i]].Max_PoolSize);
MProjectilePool.Add(TmpArr[i], TmpArray2);
PoolHeads.Add(TmpArr[i], &MProjectilePool[TmpArr[i]][0]);
}
// #2. Pool
CreatePool(MProjectileInfos);
// #3. Delegate
ParallelFor(TmpArr.Num(), [&](uint8 Idx) {
for (const auto& Projectile : MProjectilePool[TmpArr[Idx]])
{
Projectile->OnProjectileEndLaunched.BindUFunction(this, "EndLaunchedProjectile");
}
});
}
Details
- 먼저, Editor에서 전달받은 발사체 정보를 담은 DataTable을 로드합니다.
- 그리고, TMap 유형의 객체 풀 내부에 Key 값으로 발사체 종류(EProjectileType), 그리고 Value는 적절한 사이즈의 빈 배열을 삽입합니다.
- 마지막으로, 객체 풀 내 배열에 월드에 스폰한 발사체 객체들을 담아줍니다.
5. ProjectileSpawnerComponent::CreatePool()
void UC_ProjectileSpawnerComponent::CreatePool(TMap<EProjectileType, FProjectileInfo>& OutMap)
{
AActor* OwnerWeapon = GetOwner();
CheckNull(OwnerWeapon);
for (auto& Pair : OutMap)
{
for (uint8 i = 0; i < Pair.Value.Max_PoolSize; i++)
{
AProjectile* TmpProjectile = GetWorld()->SpawnActor<AProjectile>(Pair.Value.ProjectileClass, FVector::ZeroVector, FRotator::ZeroRotator);
TmpProjectile->SetOwner(OwnerWeapon);
TmpProjectile->SetIsPenetrable(Pair.Value.bPenetrable);
TmpProjectile->SetExpireRate(Pair.Value.ExpireRate);
TmpProjectile->SetIndex(i);
TmpProjectile->SetIsActive(false);
TmpProjectile->SetNext(&MProjectilePool[Pair.Key][(i + 1) % Pair.Value.Max_PoolSize]);
TmpProjectile->SetActorHiddenInGame(true);
MProjectilePool[Pair.Key][i] = TmpProjectile;
}
}
}
Details
- 먼저, Editor를 통해 전달받은 데이터 테이블을 바탕으로 객체 풀에 담을 프로젝타일을 Spawn 합니다.
- 그리고 해당 발사체 객체의 PoolOjbjectInfo 구조체 멤버의 필드 값들을 초기화해줍니다.
- 마지막으로, TMap 유형의 객체 풀에서 Key값을 통해 매핑된 배열 안에 발사체 객체를 저장합니다.
6. ProjectileSpawnerComponent::TeleportProjectileToSpawnLocation()
/* ************************************************************************************************************
*
* [Remark] : FAttachmentTransformRules::KeepRelativeTransform vs SnapToTargetNotIncludingScale
*
* 목적:
* 이미 발사 동작 중인 Projectile이 Attach 되어 있던 Component의 움직임에 따라서 위치가 변하며 날라가는 문제 발생
*
* 설명:
* 1. FAttachmentTransformRules::KeepRelativeTransform 으로 Attachment Rule을 설정해서 문제가 발생!
* 2. FAttachmentTransformRules::SnapToTargetNotIncludingScale 으로 변경 후 문제 해결
*
* ************************************************************************************************************/
void UC_ProjectileSpawnerComponent::TeleportProjectileToSpawnLocation(EProjectileType InType, USkeletalMeshComponent** InComponent, const FName& InName)
{
CheckNull(*(PoolHeads[InType]));
(*(PoolHeads[InType]))->TeleportTo((*InComponent)->GetSocketLocation(InName), (*InComponent)->GetComponentRotation() + (*InComponent)->GetRelativeRotation());
(*(PoolHeads[InType]))->SetIsLoaded(true);
(*(PoolHeads[InType]))->SetActorHiddenInGame(false);
}
Details
- 발사체 객체의 Launch 동작을 크게 두 가지로 나누었습니다. 먼저, 발사 지점에서 스폰되는 즉시 이동을 시작하는 발사체의 출발 지점으로 이동시키기 위한 함수를 구현했습니다.
- 발사체의 발사 동작 직전에 TeleportTo() 메서드를 활용해 캐릭터 객체의 소켓 위치로 이동합니다.
7. ProjectileSpawnerComponent::AttachProjectile()
void UC_ProjectileSpawnerComponent::AttachProjectile(EProjectileType InType, USkeletalMeshComponent** InComponent, const FName& InName)
{
CheckNull(*(PoolHeads[InType]));
(*(PoolHeads[InType]))->AttachToComponent((*InComponent), FAttachmentTransformRules::SnapToTargetNotIncludingScale, InName);
(*(PoolHeads[InType]))->SetIsLoaded(true);
(*(PoolHeads[InType]))->SetActorHiddenInGame(false);
}
Details
- 다른 하나는 발사 동작의 출발 지점으로 이동해 장전되어 있는 상태를 일정 시간 유지하는 함수를 구현했습니다.
- 발사체의 발사 동작 직전에 장전되어 있는 상태를 유지하기 위해 AttachToComponent() 함수를 활용했습니다.
8. ProjectileSpawnerComponent::StartLaunched()
void UC_ProjectileSpawnerComponent::StartLaunchProjectile(EProjectileType InType)
{
CheckTrue((*(PoolHeads[InType])) == nullptr || (*(PoolHeads[InType]))->GetIsActive() == true);
(*(PoolHeads[InType]))->SetIsActive(true);
(*(PoolHeads[InType]))->SetIsLoaded(false);
(*(PoolHeads[InType]))->StartLaunched();
PoolHeads[InType] = (*(PoolHeads[InType]))->GetNext();
}
Details
- 발사체 객체의 발사 동작 함수를 구현했습니다.
- 발사 동작을 수행할 발사체 객체의 FObjectPoolInfo 구조체 정보를 업데이트합니다.
- 발사 동작을 수행할 발사체 객체가 담겨 있는 객체 풀 배열의 인덱스를 가리키고 있던 PoolHeads는 자신의 Next가 가리키고 있던 발사체 객체의 위치로 이동합니다.
9. ProjectileSpawner::EndLaunched()
void UC_ProjectileSpawnerComponent::EndLaunchedProjectile(EProjectileType InType, uint8 InIndex)
{
CheckNull(MProjectilePool[InType][InIndex]);
MProjectilePool[InType][InIndex]->SetIsActive(false);
if ((*(PoolHeads[InType]))->GetIsLoaded() == true)
{
(*(PoolHeads[InType]))->SetNext(&MProjectilePool[InType][InIndex]);
}
else
{
MProjectilePool[InType][InIndex]->SetNext((*(PoolHeads[InType]))->GetNext());
PoolHeads[InType] = &MProjectilePool[InType][InIndex];
}
}
Details
- 발사체 객체의 충돌 시점 혹은 생명 주기가 끝난 시점에 호출되는 EndLaunched 함수를 구현합니다.
- 발사체 객체의 FObjectPoolInfo 구조체의 멤버 중 "Active" 필드의 값을 false로 변경합니다.
- 만약, PoolHeads(객체 풀 내 다음 발사될 발사체 객체의 위치를 가리키는 연결 리스트의 시작점)가 가리키고 있는 발사체 객체가 장전된 상태라면, 생명 주기가 끝난 발사체 객체를 PoolHeads->Next가 가리키도록 설정합니다.
- 만약, PoolHeads(객체 풀 내 다음 발사될 발사체 객체의 위치를 가리키는 연결 리스트의 시작점)가 가리키고 있는 발사체 객체가 장전된 상태가 아니라면, 생명 주기가 끝난 발사체 객체를 PoolHeads가 기리 키도록 설정합니다.
#2. 결과 화면
1. 영상
2. 설명
- 게임 시작 시점에 사용할 Projectile 객체들을 Max Pool Size 만큼 생성해 Object Pool안에 저장합니다.
- 발사 동작 수행을 모두 마친 발사체 객체는 다시 FObjectPoolInfo 구조체 정보를 수정해 소멸되지 않고 새롭게 재활용됩니다.