본문 바로가기

Unreal Engine 4/C++

<Unreal C++> 61 - Action RPG (Dead)

 

 

필요한 개념


* 우선 CStateComponent에 Dead 상태 추가

UENUM(BlueprintType)
enum class EStateType : uint8
{
	Idle, Roll, Backstep, Equip, Action, Hitted, Dead, Max
};



UFUNCTION(BlueprintPure)
	FORCEINLINE bool IsDeadMode() { return Type == EStateType::Dead; }




void UCStateComponent::SetDeadMode()
{
	ChangeType(EStateType::Dead);
}




* CEnemy.cpp의  안에 추가

 

// Hitted() 함수안에 추가

if (Status->GetHealth() <= 0.0f)
{
	State->SetDeadMode();

	return;
}



void ACEnemy::OnStateTypeChanged(EStateType InPrevType, EStateType InNewType)
{
	switch (InNewType)
	{
		case EStateType::Hitted: Hitted(); break;
		case EStateType::Dead: Dead(); break;
	}
}



void ACEnemy::Dead()
{
	// 재동작 방지
	CheckFalse(State->IsDeadMode());

	// 몽타주 플레이
	Montages->PlayDead();
}



* CMontagesComponent.cpp에 추가

 

void UCMontagesComponent::PlayDead()
{
	PlayAnimMontage(EStateType::Dead);
}


Tip) CMontageComponent는 EStateType(CStateComponent가 가지고 있는 상태)에 따라 몽타주를 추가 할 수 있음


* DeadMontage 생성

 

 


Tip) 만약 몽타주의 버그가 있다면 (* 노티파이가 호출이 안되는 버그)
애니메이션 복제본을 만들어, 해당 프레임까지 커서를 이동 한 후 우 클릭 -> 시퀸스 편집(프레임 0부터 프레임 ?까지 제거) 클릭하면 애니메이션이 짤라짐(즉, 0 ~ ?까지 프레임이 사라짐)
그 후 몽타주를 만들자(몽타주에서 조정가능하지만, 애니메이션에서 짤라버리는게 좋음)
이렇게 해놓고 몽타주 만들고, 노티파이 세팅하면 그런 버그가 사라짐

* 애니메이션이 계속 엎어진 상태로 유지하도록, 끝 프레임을 늘려준다.
애니메이션에 들어가서, 우클릭 -> 끝 부분에 덧붙임(우리는 120프레임 추가했음)

너무 길어서 우클릭 해서 (131 ~ 190까지 제거 한다.)
마우스는 131프레임에서 우클릭 해야함

애니메이션 결과를 보면 계속 엎어져있는 것 확인가능함

이 애니메이션으로 몽타주 생성하고, 못 움직이게 FullBody로 세팅


* 사망이나 피격(Hitted) 등은 캐릭터 모두가 가지게 될 것이므로 인터페이스 ICharacter에 함수 추가함

// 아직 플레이어는 추가를 안해서 순수가상함수로 처리하지 않는다.(노티파이에 의해서 콜 될 거여서, BeginDead, EndDead가 필요)
virtual void Begin_Dead() {};
virtual void End_Dead() {};



나중에 Hitted()도 이렇게 통합해서 구현하면 된다.(공격하다가 맞으면(Hitted())되면 공격 취소하고 Hitted하고 다시 공격해야함)
그런 처리 때 Hitted()도 인터페이스에 함수로 구현함

 

 

* 노티파이를 만듬(CAnimNotifyState_Dead 클래스 생성)
사망은 AnimNotifyState로 만들어 사망 시작시 충돌체를 모두 꺼버리고 종료시 사망 처리를 완료하도록 함
사망 완료시 해당 캐릭터 삭제

 

CAnimNotifyState_Dead.cpp에 추가

character->Begin_Dead(); // NotifyBegin() 때
character->End_Dead(); // NotifyEnd() 때


* 참고로 GetOwner()는 게임상황이 아니면 안나오는 경우가 있어서 항상 CheckNull()을 해줘야 함

CheckNull(MeshComp);
CheckNull(MeshComp->GetOwner());
IICharacter* character = Cast<IICharacter>(MeshComp->GetOwner());
CheckNull(character);



* 몽타주에 Dead 노티파이 스테이트 추가
시작부터 어느정도 계속 엎어져 있는 것 쯤까지 해줌 



* PlayDead()를 할려면 Excel Data를 수정함(DataTable 수정)
2번 Dead 추가한 뒤 DataTable 리임포트

 



<Enemy Dead() 처리시>
* 캐릭터의 캡슐 충돌체와 함께, Enemy 무기의 모든 충돌체도 꺼줘야 함 -> 다시 맞으면 안됨

CActionComponent가 Datas로 해서 Attachment를 가지고 있고, Attachment가 충돌체를 모두 가지고 있다.

 

* CActionComponent에 무기 충돌체를 모두 끄는 함수를 만든다.

// 모든 Collision을 꺼달라
void OffAllCollision();

 

void UCActionComponent::OffAllCollision()
{
	// 모든 충돌체를 가져와 끄면 된다.
	for (UCActionData * data : Datas)
	{
		// data가 Null이라면
		if (!!data == false)
			continue;

		// 즉, GetAttachment()가 Null이라면
		if (!!data->GetAttachment() == false)
			continue;

		// 충돌체 꺼라는 명령 내림
		data->GetAttachment()->OffCollision();
	}
}


CEnemy.cpp

void ACEnemy::Begin_Dead()
{
	// 충돌체를 꺼줌(다시 맞으면 안됨, 무기도 꺼야해고, 자기의 캡슐도 꺼야함)
	// 즉, 캐릭터의 캡슐 충돌체와 함께, Enemy 무기의 모든 충돌체도 꺼줘야 한다.
	// 충돌체 전부 꺼줌
	Action->OffAllCollision();

	// 자신의 캐릭터 캡슐 컴포넌트(Collision을 꺼줌)
	GetCapsuleComponent()->SetCollisionEnabled(ECollisionEnabled::NoCollision);
}

 

void ACEnemy::End_Dead()
{
	// 자기 자신을 제거
	Destroy();
}




<2강 Dead - 2>

* 결과를 보면 타켓팅된 애를 죽이면 타켓팅 해제가 안되고 있음 타켓팅 해제에 대한 처리가 없으므로 타켓팅을 옯겨줘야만 다시 타겟팅이 일어남


TargetComponent.cpp TickComponent() 함수 부분 추가

// 타겟이 사망했는지를 체크해야 하니까
UCStateComponent* state = CHelpers::GetComponent<UCStateComponent>(Target);
// 타겟팅이 해제될 수 있는 조건 2개
// 1. 일정거리를 벗어났을때
// 2. DeadMode 
// GetDistanceTo() : 거리를 구함

bool b = false;
// b가 false니까 뒤에가 true면 true가 됨
b |= state->IsDeadMode();
b |= Target->GetDistanceTo(OwnerCharacter) >= TraceRadius;
// &로 하게 된다면 b를 true로 주고 하면 된다.

 

// 옆으로 계속 늘어지는 거 보다 좋음
if (b == true)
{
	EndTargeting();

	return;
}




 

 

 

CStateComponent.h 추가된 내용


더보기
...

UENUM(BlueprintType)
enum class EStateType : uint8
{
	// Action : 공격 상태
	Idle, Roll, Backstep, Equip, Action, Hitted, Dead, Max
};

...

UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class UONLINE_04_ACTION_API UCStateComponent : public UActorComponent
{
	GENERATED_BODY()

public:
	...
	UFUNCTION(BlueprintPure)
		FORCEINLINE bool IsDeadMode() { return Type == EStateType::Dead; }

public:
	...
	void SetDeadMode();

};

 

 

 

 

 

 

 

CStateComponent.cpp 추가된 내용


더보기
...

void UCStateComponent::SetDeadMode()
{
	ChangeType(EStateType::Dead);
}

 

 

CEnemy.h


더보기
UCLASS()
class UONLINE_04_ACTION_API ACEnemy : public ACharacter, public IICharacter
{
	GENERATED_BODY()

...

private:
	void Hitted();
	void Dead();


public:
	virtual void Begin_Dead() override;
	virtual void End_Dead() override;
};

 

 

 

 

 

 

 

CEnemy.cpp 수정된 내용


더보기
...

void ACEnemy::OnStateTypeChanged(EStateType InPrevType, EStateType InNewType)
{
	switch (InNewType)
	{
		case EStateType::Hitted: Hitted();	break;
		case EStateType::Dead: Dead();	break;
	}
}


void ACEnemy::Hitted()
{
	Status->SubHealth(DamageValue);
	Cast<UCUserWidget_Health>(HealthWidget->GetUserWidgetObject())->Update(Status->GetHealth(), Status->GetMaxHealth());
	DamageValue = 0.0f;

	Status->SetStop();


	if (Status->GetHealth() <= 0.0f)
	{
		State->SetDeadMode();

		return;
	}


	Montages->PlayHitted();

	FVector start = GetActorLocation();
	FVector target = DamageInstigator->GetPawn()->GetActorLocation();
	SetActorRotation(UKismetMathLibrary::FindLookAtRotation(start, target));
	DamageInstigator = NULL;

	FVector direction = target - start;
	direction.Normalize();
	LaunchCharacter(-direction * LaunchAmount, true, false);

	ChangeColor(FLinearColor(1, 0, 0, 1));

	UKismetSystemLibrary::K2_SetTimer(this, "RestoreColor", 0.1f, false);
}

void ACEnemy::Dead()
{
	// 재동작 방지
	CheckFalse(State->IsDeadMode());

	Montages->PlayDead();
}

void ACEnemy::Begin_Dead()
{
	// 충돌체를 꺼줌(다시 맞으면 안됨, 무기도 꺼야해고, 자기의 캡슐도 꺼야함)
	// 즉, 캐릭터의 캡슐 충돌체와 함께, Enemy 무기의 모든 충돌체도 꺼줘야 한다.
	// 충돌체 전부 꺼줌
	Action->OffAllCollision();

	// 자신의 캐릭터 캡슐 컴포넌트(Collision을 꺼줌)
	GetCapsuleComponent()->SetCollisionEnabled(ECollisionEnabled::NoCollision);
}


void ACEnemy::End_Dead()
{
	// 자기 자신을 제거
	Destroy();
}

 

 

CMontagesComponent.h 추가된 내용


더보기
...

UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class UONLINE_04_ACTION_API UCMontagesComponent : public UActorComponent
{
	GENERATED_BODY()

...

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

 

 

 

 

 

 

 

CMontagesComponent.cpp 추가된 내용


더보기
...

void UCMontagesComponent::PlayDead()
{
	PlayAnimMontage(EStateType::Dead);
}

 

 

ICharacter.h 추가된 내용


더보기
...

class UONLINE_04_ACTION_API IICharacter
{
	GENERATED_BODY()

public:
	...
    
	// 아직 플레이어는 추가를 안해서 순수가상함수로 처리하지 않는다.(노티파이에 의해서 콜 될 거여서, BeginDead, EndDead가 필요)
	virtual void Begin_Dead() {};
	virtual void End_Dead() {};
};

 

 

 

 

CAnimNotifyState_Dead.h


더보기
#pragma once

#include "CoreMinimal.h"
#include "Animation/AnimNotifies/AnimNotifyState.h"
#include "CAnimNotifyState_Dead.generated.h"

UCLASS()
class UONLINE_04_ACTION_API UCAnimNotifyState_Dead : public UAnimNotifyState
{
	GENERATED_BODY()

public:
	FString GetNotifyName_Implementation() const override;

	virtual void NotifyBegin(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, float TotalDuration) override;
	virtual void NotifyEnd(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation) override;
};

 

 

 

 

 

 

 

CAnimNotifyState_Dead.cpp


더보기
#include "CAnimNotifyState_Dead.h"
#include "Global.h"
#include "Characters/ICharacter.h"

FString UCAnimNotifyState_Dead::GetNotifyName_Implementation() const
{
	return "Dead";
}

void UCAnimNotifyState_Dead::NotifyBegin(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, float TotalDuration)
{
	Super::NotifyBegin(MeshComp, Animation, TotalDuration);
	CheckNull(MeshComp);
	CheckNull(MeshComp->GetOwner());

	IICharacter* character = Cast<IICharacter>(MeshComp->GetOwner());
	CheckNull(character);

	character->Begin_Dead();
}

void UCAnimNotifyState_Dead::NotifyEnd(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation)
{
	Super::NotifyEnd(MeshComp, Animation);
	CheckNull(MeshComp);
	CheckNull(MeshComp->GetOwner());

	IICharacter* character = Cast<IICharacter>(MeshComp->GetOwner());
	CheckNull(character);

	character->End_Dead();
}

 

 

CActionComponent.h 추가된 내용


더보기
...

UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
class UONLINE_04_ACTION_API UCActionComponent : public UActorComponent
{
	GENERATED_BODY()
    
    ...
    
	// 무기가 가진 모든 충돌체를 꺼달라
	void OffAllCollision();
};

 

 

 

 

 

 

 

CActionComponent.cpp 추가된 내용


더보기
...

void UCActionComponent::OffAllCollision()
{
	// 모든 충돌체를 가져와 끄면 된다.
	for (UCActionData * data : Datas)
	{
		// data가 Null이라면
		if (!!data == false)
			continue;

		// 즉, GetAttachment()가 Null이라면
		if (!!data->GetAttachment() == false)
			continue;

		// 충돌체 꺼라는 명령 내림
		data->GetAttachment()->OffCollision();
	}
}

 

 

 

 

 

CActionComponent.cpp 수정된 내용


더보기
...

void UCTargetComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
	Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
	CheckNull(Target);

	// 타겟이 사망했는지를 체크해야 하니까
	UCStateComponent* state = CHelpers::GetComponent<UCStateComponent>(Target);

	// 타겟팅이 해제될 수 있는 조건 2개
	// 1. 일정거리를 벗어났을때
	// 2. DeadMode 
	// GetDistanceTo() : 거리를 구함

	bool b = false;
	// b가 false니까 뒤에가 true면 true가 됨
	b |= state->IsDeadMode();
	b |= Target->GetDistanceTo(OwnerCharacter) >= TraceRadius;
	// &로 하게 된다면 b를 true로 주고 하면 된다.

	// 옆으로 계속 늘어지는 거 보다 좋음
	if (b == true)
	{
		EndTargeting();

		return;
	}


	// 즉, 바라보는 방향의 회전값으로 세팅되도록 만듬(left, right target 버그 해결)

	FVector start = OwnerCharacter->GetActorLocation();
	FVector target = Target->GetActorLocation();

	// start에서 target을 바라보는 회전값이 만들어짐
	FRotator rotator = UKismetMathLibrary::FindLookAtRotation(start, target);
	FRotator current = OwnerCharacter->GetControlRotation();

	// RInterpTo : 시작 방향에서 종료 방향까지 시간에 비례한 속도로 자연스럽게 보간해주는 함수
	rotator = UKismetMathLibrary::RInterpTo(current, rotator, DeltaTime, interopSpeed);

	// SetControlRotation()은 private이어서 Contoller를 통해 가져와야함
	OwnerCharacter->GetController()->SetControlRotation(rotator);
}

 

 

 

결과