본문 바로가기

Unreal Engine 4/C++

<Unreal C++> 63 - Action RPG (FireStorm)

 

필요한 개념


캐릭터 주변으로 박스 콜리전에 붙은 불기동을 플레이 시킨 후 불기둥이 적에 닿으면 연속적으로 피격을 일으키는 무기를 제작할 것임

* BP_CAttachment_FireStorm 생성


* DA_FireStorm 세팅
- BP_CAttachment_FireStorm으로 세팅
- 사방으로 뛰어 다닐 수 있게 PawnControl 해제
- 파티클 이펙트 세팅
- CanMove false해서 움직일 수 없게 해주고, 나중에는 움직일 수 있게 풀어줄 것이다.
- FireStorm 몽타주 세팅
- CDoAction_FireStorm 클래스 세팅

* FireStorm_Montage 세팅

 

 

BeginAction으로 FireStorm 등장시킴
EndAction으로 종료

Tip) BeginAction과 EndAction은 CDoAction을 상속받은 클래스에서 원하는 대로 정의할 수 있음

* CDoAction을 상속 받는 CDoAction_FireStorm 클래스 생성

* CActionComponent에 FireStorm모드 추가

UENUM(BlueprintType)
enum class EActionType: uint8
{
	Unarmed, Fist, OneHand, TwoHand, Warp, FireStorm, Max,
};



UFUNCTION(BlueprintPure)
	FORCEINLINE bool IsFireStormMode() { return Type == EActionType::FireStorm; }




void UCActionComponent::SetFireStormMode()
{
	SetMode(EActionType::FireStorm);
}



Tip) 컴파일을 자주해줘야 속도가 빨라짐
그나마 컴파일할 크기가 줄어드니까 속도가 빨라짐


* 프로젝트 세팅에 액션 매핑 키 추가
FireStorm -> G

* CPlayer에 입력 이벤트 연결을 해준다.

PlayerInputComponent->BindAction("FireStorm", EInputEvent::IE_Pressed, this, &ACPlayer::OnFireStorm);

 

void ACPlayer::OnFireStorm()
{
	CheckFalse(State->IsIdleMode());

	Action->SetFireStormMode();
}

 


* BP_CPlayer에 Action에 DA_FireStorm 세팅

 

 

 


Tip) 객체 소유 관련 문제
박스를 찾아오고 박스에 다가 파티클을 붙일 것이다.
그런데 소유하지 않는게 좋다. 그런데 소유해야 되는 경우는 어쩔 수 없이 소유하게 되는데,
언리얼엔진에 있는 타입들은 소유해도 상관 없다.
언리얼엔진 타입들은 수정되지 않을 거여서,
반면 우리가 만든 것들은 수정될 여지가 있다.

 

ex) CDoAction_FireStorm에서는 BoxCollision을 사용하기 위해 CAttachment를 소유하는 것 보다, CAttachment가 소유한 BoxComponent를 소유하는게 맞다.

Attachment를 찾아서 콜하는것은 문제가 없음(소유하지 않는다면)
BoxComponent는 소유해도 됨(언리얼엔진꺼여서)


불기둥이 플레이어 주위를 회전한다.
박스에다 파티클을 붙이고 박스를 회전시켜줄 것이다.


CDoAction_FireStorm

// BP에서 세팅가능하게 처리
private:
	UPROPERTY(EditAnywhere)
		float Time = 5.0f; // 불기둥의 시간

	UPROPERTY(EditAnywhere)
		float Distance = 200.0f; // 불기둥과 플레이어 사이의 간격

	UPROPERTY(EditAnywhere)
		float Speed = 100.0f; // 불기둥이 도는 속도



void ACDoAction_FireStorm::Begin_DoAction()
{
	// Box콜리젼에다가 파티클 이펙트 붙여줌
	Attached = UGameplayStatics::SpawnEmitterAttached(Datas[0].Effect, Box, "");
	Attached->SetRelativeLocation(Datas[0].EffectTransform.GetLocation());
	Attached->SetRelativeScale3D(Datas[0].EffectTransform.GetScale3D());

	// Box의 Owner로 attachment가져옴
	//ACAttachment* attachment = Cast<ACAttachment>(Box->GetOwner());
	//
	//// 시작때 꺼줬었다.
	//attachment->OnCollision(); // 충돌체를 켜줌
}



void ACDoAction_FireStorm::End_DoAction()
{
	// 불기둥을 봐야하니까 이동허용
	Status->SetMove();

	// 일정 시간뒤에 IdleMode로 돌려준다.(불기둥이 끝나야 IdleMode)
	FTimerDynamicDelegate timerDelegate;
	timerDelegate.BindUFunction(this, "Finish");

	// 우리는 이전의 UKismetSystemLibrary::K2_SetTimer()를 썼었는데
	// 이번에는 델리게이트로 써봄
	// 1. 델리게이트 2. 몇 초뒤 실행 3. 반복여부
	UKismetSystemLibrary::K2_SetTimerDelegate(timerDelegate, Time, false);
}

 

void ACDoAction_FireStorm::Finish()
{
	// IdleMode 상태로 돌려줌
	State->SetIdleMode();

	// 불기둥을 날려줌
	Attached->DestroyComponent();
}

 



그림 3_1 참고

void ACDoAction_FireStorm::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

	CheckFalse(*bEquipped);
	// 액션모드일때만 실행(불기둥 등장할때는 액션모드)
	CheckFalse(State->IsActionMode());

	FVector location = OwnerCharacter->GetActorLocation();

	// 일정한 속도를 유지하기 위해 DeltaTime을 곱해준다.
	Angle += Speed * DeltaTime;
	// angle이 360과 가까운지(오차가 실수형이기에 무조건 있다.) -> 짐벌락이 발생할 수 있기에
	// 오차값이 쌓여서...
	// 축의 오차값이 누적되서, 한쪽은 도는데, 한쪽은 안돌 수 있음(짐벌락 발생)
	if (FMath::IsNearlyEqual(Angle, 360))
		Angle = 0.0f;

	// 회전 값(어느거리에서 어느 방향으로 회전할지)
	// x축에 넣었다.
	// x축 전방이다. 전방에서 Distance값만큼 떨어트리기 위해
	FVector axis = FVector(Distance, 0, 0);
	// RotateAngleAxis : 지정한 방향으로 지정한 각도만큼 회전된 위치를 리턴
	// 1. 얼마만큼 회전할건지, 2. 어느방향으로 회전할건지(회전할 축)
	// 즉, Z축(수직축)인 오른쪽으로 빙글빙글 Angle만큼 돌게 된다.
	// 빙글빙글 도는 것은 수직축 회전이다.
	FVector value = axis.RotateAngleAxis(Angle, FVector(0, 0, 1));
	// 즉, 수직축 회전을 하는데 Distance만큼 떨어져서 회전을 해서 그 위치를 구함

	// 플레이어 위치를 기준으로 회전
	location += value;

	// 박스에다 위치 세팅
	Box->SetWorldLocation(location);
}




* BP_CDoAction_FireStorm 생성

 

얘는 변수값을 우리가 BP에서 세팅가능하도록 해놔서, BP로 따로 만든것임
- Time : 유지시간
- Distance : 거리
- Speed : 회전속도

- DA_FireStorm에 BP_CDoAction_FireStorm 세팅

// Begin_DoAction()

// Box의 Owner로 attachment가져옴
ACAttachment* attachment = Cast<ACAttachment>(Box->GetOwner());

// 시작때 꺼줬었다.
attachment->OnCollision(); // 충돌체를 켜줌
// Finish()
ACAttachment* attachment = Cast<ACAttachment>(Box->GetOwner());
attachment->OffCollision(); // 충돌체를 꺼줌




충돌한 애들에 대해서 계속 데미지를 주어야 한다.
그런데 생각해보면 Box는 BeginOverlap(), EndOverlap() 밖에 없다.
즉, 충돌중은 없다.
-> 그래서, 충돌이 유지되는 이벤트는 없으므로 충돌이 시작하면 배열에 넣고 충돌이 끊나면 배열에서 제거한다.
일정 시간마다 배열에 있는 캐릭터들에 데미지를 준다.
우리가 정의했던 OnAttachmentBeginOverlap(), OnAttachmentEndOverlap()을 사용함

TArray<class ACharacter*> HittedCharacters;



UPROPERTY(EditAnywhere)
	float HittedTime = 0.25f; // 0.25초마다 Hitted됨

UFUNCTION()
	void Hitted();



void ACDoAction_FireStorm::OnAttachmentBeginOverlap(ACharacter* InAttacker, AActor* InAttackCauser, ACharacter* InOtherCharacter)
{
	// AddUnique : 기존에 추가 되어있는 값이 있다면 추가하지 않음으로 하나의 값만 유지됨
	HittedCharacters.AddUnique(InOtherCharacter);
}

 

void ACDoAction_FireStorm::OnAttachmentEndOverlap(ACharacter* InAttacker, AActor* InAttackCauser, ACharacter* InOtherCharacter)
{
	// 제거됨
	HittedCharacters.Remove(InOtherCharacter);
}






// Begin_DoAction()
// Hitted 일정시간 만큼 반복해서 콜 됨
UKismetSystemLibrary::K2_SetTimer(this, "Hitted", HittedTime, true);

 

// Finish()
// 제거 시켜줌
UKismetSystemLibrary::K2_ClearTimer(this, "Hitted");




* 맞으면 Launch되는 것을 없애려면 받은 Causer가 뭐냐에 따라 정의하면 된다.


기존에는 끝난 Angle에 이어서 FireStorm이 생성되었다.

 

* Angle의 초기화를 랜덤으로 입력해 랜덤한 위치에서 시작할 수 있도록 처리함

// Begin_DoAction()

// 불기둥의 값이 랜덤한 값으로 시작되게
Angle = UKismetMathLibrary::RandomFloatInRange(0, 360);

 

 

 

 

 

CActionComponent.h 추가된 내용


더보기
...

UENUM(BlueprintType)
enum class EActionType: uint8
{
	Unarmed, Fist, OneHand, TwoHand, Warp, FireStorm, Max,
};

...

UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
class UONLINE_04_ACTION_API UCActionComponent : public UActorComponent
{
	GENERATED_BODY()
	...
    
public:
	...
    
	UFUNCTION(BlueprintPure)
		FORCEINLINE bool IsFireStormMode() { return Type == EActionType::FireStorm; }

public:
	...
	void SetFireStormMode();
};

 

 

 

 

 

 

 

CActionComponent.cpp 추가된 내용


더보기
...

void UCActionComponent::SetFireStormMode()
{
	SetMode(EActionType::FireStorm);
}

 

 

CDoAction_FireStorm.h


더보기
#pragma once

#include "CoreMinimal.h"
#include "Actions/CDoAction.h"
#include "CDoAction_FireStorm.generated.h"

UCLASS()
class UONLINE_04_ACTION_API ACDoAction_FireStorm : public ACDoAction
{
	GENERATED_BODY()

	// BP에서 세팅가능하게 처리
private:
	UPROPERTY(EditAnywhere)
		float Time = 5.0f; // 불기둥의 시간

	UPROPERTY(EditAnywhere)
		float Distance = 200.0f; // 불기둥과 플레이어 사이의 간격

	UPROPERTY(EditAnywhere)
		float Speed = 100.0f; // 불기둥이 도는 속도

	UPROPERTY(EditAnywhere)
		float HittedTime = 0.25f; // 0.25초마다 Hitted됨


protected:
	virtual void BeginPlay() override;

public:
	virtual void DoAction() override;
	virtual void Begin_DoAction() override;
	virtual void End_DoAction() override;

	virtual void Tick(float DeltaTime) override;

private:
	virtual void OnAttachmentBeginOverlap(class ACharacter* InAttacker, class AActor* InAttackCauser, class ACharacter* InOtherCharacter) override;
	virtual void OnAttachmentEndOverlap(class ACharacter* InAttacker, class AActor* InAttackCauser, class ACharacter* InOtherCharacter) override;

private:
	UFUNCTION()
		void Finish();

	UFUNCTION()
		void Hitted();

private:
	class UBoxComponent* Box;
	class UParticleSystemComponent* Attached; // 어느시간이 되면 제거하기 위해

	float Angle; // 각도

	TArray<class ACharacter*> HittedCharacters;
};

 

 

 

 

 

 

 

CDoAction_FireStorm.cpp


더보기
#include "CDoAction_FireStorm.h"
#include "Global.h"
#include "CAttachment.h"
#include "GameFramework/Character.h"
#include "Components/BoxComponent.h"
#include "Components/CStateComponent.h"
#include "Components/CStatusComponent.h"
#include "Particles/ParticleSystemComponent.h"

void ACDoAction_FireStorm::BeginPlay()
{
	Super::BeginPlay();

	for (AActor* actor : OwnerCharacter->Children)
	{
		if (actor->IsA<ACAttachment>() && actor->GetActorLabel().Contains("FireStorm"))
		{
			Box = CHelpers::GetComponent<UBoxComponent>(actor);

			break;
		}
	}
}

void ACDoAction_FireStorm::DoAction()
{
	CheckFalse(*bEquipped);

	CheckFalse(State->IsIdleMode());
	State->SetActionMode();

	OwnerCharacter->PlayAnimMontage(Datas[0].AnimMontage, Datas[0].PlayRatio, Datas[0].StartSection);

	Datas[0].bCanMove ? Status->SetMove() : Status->SetStop();
}

void ACDoAction_FireStorm::Begin_DoAction()
{
	// 불기둥의 값이 랜덤한 값으로 시작되게
	Angle = UKismetMathLibrary::RandomFloatInRange(0, 360);

	// Box콜리젼에다가 파티클 이펙트 붙여줌
	Attached = UGameplayStatics::SpawnEmitterAttached(Datas[0].Effect, Box, "");
	Attached->SetRelativeLocation(Datas[0].EffectTransform.GetLocation());
	Attached->SetRelativeScale3D(Datas[0].EffectTransform.GetScale3D());

	// Box의 Owner로 attachment가져옴
	ACAttachment* attachment = Cast<ACAttachment>(Box->GetOwner());
	
	// 시작때 꺼줬었다.
	attachment->OnCollision(); // 충돌체를 켜줌

	// Hitted 일정시간 만큼 반복해서 콜 됨
	UKismetSystemLibrary::K2_SetTimer(this, "Hitted", HittedTime, true);
}

void ACDoAction_FireStorm::End_DoAction()
{
	// 불기둥을 봐야하니까 이동허용
	Status->SetMove();

	// 일정 시간뒤에 IdleMode로 돌려준다.(불기둥이 끝나야 IdleMode)
	FTimerDynamicDelegate timerDelegate;
	timerDelegate.BindUFunction(this, "Finish");

	// 우리는 이전의 UKismetSystemLibrary::K2_SetTimer()를 썼었는데
	// 이번에는 델리게이트로 써봄
	// 1. 델리게이트 2. 몇 초뒤 실행 3. 반복여부
	UKismetSystemLibrary::K2_SetTimerDelegate(timerDelegate, Time, false);
}

void ACDoAction_FireStorm::Finish()
{
	// IdleMode 상태로 돌려줌
	State->SetIdleMode();

	// 불기둥을 날려줌
	Attached->DestroyComponent();

	ACAttachment* attachment = Cast<ACAttachment>(Box->GetOwner());
	attachment->OffCollision(); // 충돌체를 꺼줌

	// 제거 시켜줌
	UKismetSystemLibrary::K2_ClearTimer(this, "Hitted");
}


void ACDoAction_FireStorm::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

	CheckFalse(*bEquipped);
	// 액션모드일때만 실행(불기둥 등장할때는 액션모드)
	CheckFalse(State->IsActionMode());

	FVector location = OwnerCharacter->GetActorLocation();

	// 일정한 속도를 유지하기 위해 DeltaTime을 곱해준다.
	Angle += Speed * DeltaTime;
	// angle이 360과 가까운지(오차가 실수형이기에 무조건 있다.) -> 짐벌락이 발생할 수 있기에
	// 오차값이 쌓여서...
	// 축의 오차값이 누적되서, 한쪽은 도는데, 한쪽은 안돌 수 있음(짐벌락 발생)
	if (FMath::IsNearlyEqual(Angle, 360))
		Angle = 0.0f;

	// 회전 값(어느거리에서 어느 방향으로 회전할지)
	// x축에 넣었다.
	// x축 전방이다. 전방에서 Distance값만큼 떨어트리기 위해
	FVector axis = FVector(Distance, 0, 0);
	// RotateAngleAxis : 지정한 방향으로 지정한 각도만큼 회전된 위치를 리턴
	// 1. 얼마만큼 회전할건지, 2. 어느방향으로 회전할건지(회전할 축)
	// 즉, Z축(수직축)인 오른쪽으로 빙글빙글 Angle만큼 돌게 된다.
	// 빙글빙글 도는 것은 수직축 회전이다.
	FVector value = axis.RotateAngleAxis(Angle, FVector(0, 0, 1));
	// 즉, 수직축 회전을 하는데 Distance만큼 떨어져서 회전을 해서 그 위치를 구함
	
	// 플레이어 위치를 기준으로 회전
	location += value;
	
	// 박스에다 위치 세팅
	Box->SetWorldLocation(location);
}

void ACDoAction_FireStorm::OnAttachmentBeginOverlap(ACharacter* InAttacker, AActor* InAttackCauser, ACharacter* InOtherCharacter)
{
	// AddUnique : 기존에 추가 되어있는 값이 있다면 추가하지 않음으로 하나의 값만 유지됨
	HittedCharacters.AddUnique(InOtherCharacter);


}

void ACDoAction_FireStorm::OnAttachmentEndOverlap(ACharacter* InAttacker, AActor* InAttackCauser, ACharacter* InOtherCharacter)
{
	// 제거됨
	HittedCharacters.Remove(InOtherCharacter);
}


void ACDoAction_FireStorm::Hitted()
{
	// 데미지를 전달함
	FDamageEvent e;
	for (ACharacter* character : HittedCharacters)
		character->TakeDamage(Datas[0].Power, e, OwnerCharacter->GetController(), this);

}

 

 

 

결과