본문 바로가기

Unreal Engine 4/C++

<Unreal C++> 51 - Action RPG(Enemy Hitted)

 

필요한 개념


<Enemy 피격>

// CEnemy.h

private:
	UFUNCTION()
		void OnStateTypeChanged(EStateType InPrevType, EStateType InNewType);

 

// CEnemy.cpp

// State가 바뀔때 함수 바인드
State->OnStateTypeChanged.AddDynamic(this, &ACEnemy::OnStateTypeChanged);

 


* StateComponent에 Hitted 상태를 추가함

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



<데미지 적용 & 적 뒤로 밀리는 작업(Launch)>

 

// CEnemy.cpp
void ACEnemy::Hitted()
{
	Status->SubHealth(DamageValue);
	Cast<UCUserWidget_Health>(HealthWidget->GetUserWidgetObject())->Update(Status->GetHealth(), Status->GetMaxHealth());
	DamageValue = 0.0f;
    
	// Hitted 되었을 때 뒤로 밀리는 작업
	// BP에서 SetOffsetWorld()를 했지만
	// C에서는 Launch로 한다.
	FVector start = GetActorLocation();
	FVector target = DamageInstigator->GetPawn()->GetActorLocation();
	// FindLookAtRotation()으로 적이 플레이어를 바라보는 회전방향이 만들어짐
	// 플레이어를 바라보도록 한다.
	SetActorRotation(UKismetMathLibrary::FindLookAtRotation(start, target));
	DamageInstigator = NULL;

	// start가 Target을 바라보는 방향이 나옴
	FVector direction = target - start;
	direction.Normalize();
	// 뒤로 밀리는 작업
	// 뒤로 밀릴꺼니까 반대로 뒤집음
	// 1 : 밀리는 방향 2 : XY방향으로 밀릴건지 3 : Z방향으로 밀릴건지
	// 평면상으로만 움직이게 XY방향만 킨다.
	LaunchCharacter(-direction * LaunchAmount, true, false);
}





<피격 되었을 시 색 바꾸기>

 

// CEnemy.cpp

// Hitted() 함수 내에 정의

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

// 타이머 델리게이션으로 0.1초 뒤 원래 색으로 돌아옴
UKismetSystemLibrary::K2_SetTimer(this, "RestoreColor", 0.1f, false);


void ACEnemy::RestoreColor()
{
	FLinearColor color = Action->GetCurrent()->GetEquipmentColor();

	// 현재 무기 색으로 돌려줌
	ChangeColor(color);
}




<Hitted 동작 플레이>

 

* Enemy.csv 세팅

*Enemy_csv


* 몽타주 FullBody로 세팅, Enemy.csv를 DataTable로 불러들임

 

* BP_CEnemy_Dummy의 MontageComponent에 DataTable을 세팅

 

 



* CMontageComponent에서 Hitted 세팅

void UCMontagesComponent::PlayHitted()
{
	PlayAnimMontage(EStateType::Hitted);
}

 

Tip) EStateType(CStateComponent)에 따른 애니메이션은 CMontageComponent에서 세팅해준다.



* CEnemy.cpp Hitted()에서 몽타주 플레이 시켜줌

Status->SetStop();
Montages->PlayHitted();



* 노티파이 생성(AnimNotify를 상속 받는 CAnimNotify_Hitted 클래스 생성)
피격 동작 노티파이에서는 Hit가 완료되면 CStateComponent를 가져와서 Idle 모드로 바꿔주면 된다.

// IdleMode 상태로 돌려줌
state->SetIdleMode();

 


* Hitted 노티파이를 몽타주에 적용



<연타 버그 잡기>
타격이 겹치는 거(따닥)을 막는 작업

여러번 충돌 처리가 될 수 있음을 방지

* CAttachment에 Delegate 추가(OnCollision, OffCollision때 바인딩된 함수들 Broadcast)

DECLARE_DYNAMIC_MULTICAST_DELEGATE(FAttachmentCollision);

UPROPERTY(BlueprintAssignable)
	FAttachmentCollision OnAttachmentCollision; // OnCollision 때 

UPROPERTY(BlueprintAssignable)
	FAttachmentCollision OffAttachmentCollision; // OffCollision 때



* CDoAction.h에 virtual로 추가

UFUNCTION()
	virtual void OnAttahmentCollision() {};
UFUNCTION()
	virtual void OffAttahmentCollision() {};





* CActionData에서 AddDynamic해서 바인딩 해줌

Attachment->OnAttachmentCollision.AddDynamic(DoAction, &ACDoAction::OnAttahmentCollision);
Attachment->OffAttachmentCollision.AddDynamic(DoAction, &ACDoAction::OffAttahmentCollision);




* CDoAction_Melee에서는 해당 함수를 오버라이드 해서 작성

// Collision이 충돌되었을 때
void ACDoAction_Melee::OnAttachmentBeginOverlap(class ACharacter* InAttacker, class AActor* InAttackCauser, class ACharacter* InOtherCharacter)
{
	...
	// 목록에 있다면 했던것을 또 하지 않음
	for (const ACharacter* other : HittedCharacters)
	{
		if (InOtherCharacter == other)
			return;
	}
	HittedCharacters.Add(InOtherCharacter);
	...
}



void ACDoAction_Melee::OnAttahmentCollision()
{
}

// Collision을 껏을 때
void ACDoAction_Melee::OffAttahmentCollision()
{
	// 충돌된 캐릭터들을 비워버림
	HittedCharacters.Empty();
}

 

 

 

 

 

 

CActionComponent.cpp 수정된 내용


더보기
void UCActionComponent::SetUnarmedMode()
{
	// 이미 무기가 장착되어 있다면 해제하고
	if (!!Datas[(int32)Type])
	{
		ACEquipment* equipment = Datas[(int32)Type]->GetEquipment();
		if (!!equipment)
			equipment->Unequip();
	}


	// 이제 UnamedMode도 장비가 있는 것으로 처리된다.
	ACEquipment* equipment = Datas[(int32)EActionType::Unarmed]->GetEquipment();
	CheckNull(equipment);
	
	// 색바꾸기 위해(Unarmed도 장비가 있는 것으로 바꿈)
	equipment->Equip();

	ChangeType(EActionType::Unarmed);

}

 

 

CEnemy.h 추가된 내용


더보기
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "Characters/ICharacter.h"
#include "Components/CStateComponent.h"
#include "CEnemy.generated.h"

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

private:
	// Hitted 되었을 때 밀리는 정도
	UPROPERTY(EditAnywhere, Category = "Hitted")
		float LaunchAmount = 100.0f;
	
private:
	UFUNCTION()
		void OnStateTypeChanged(EStateType InPrevType, EStateType InNewType);
	
    
	// 색 복원하기(타이머 델리게이션으로)
	UFUNCTION()
		void RestoreColor();

private:
	void Hitted();

private:
	// 공격한 놈의 컨트롤러
	class AController* DamageInstigator;
	// 데미지 양
	float DamageValue;
};

 

 

 

 

 

 

 

CEnemy.cpp 추가된 내용


더보기
...

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


float ACEnemy::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
	DamageInstigator = EventInstigator;
	// 데미지를 깍어야 하거나 증가하거나 이런경우는 TakeDamage()리턴된 값으로 처리하는 것이 맞다.
	// 부모에서 적용된 것으로 쓰겠다.
	DamageValue = Super::TakeDamage(DamageAmount, DamageEvent, EventInstigator, DamageCauser);

	State->SetHittedMode();

	return Status->GetHealth();
}

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

	Status->SetStop();
	Montages->PlayHitted();

	// Hitted 되었을 때 뒤로 밀리는 작업
	// BP에서 SetOffsetWorld()를 했지만
	// C에서는 Launch로 한다.
	FVector start = GetActorLocation();
	FVector target = DamageInstigator->GetPawn()->GetActorLocation();
	// FindLookAtRotation()으로 적이 플레이어를 바라보는 회전방향이 만들어짐
	// 플레이어를 바라보도록 한다.
	SetActorRotation(UKismetMathLibrary::FindLookAtRotation(start, target));
	DamageInstigator = NULL;

	// start가 Target을 바라보는 방향이 나옴
	FVector direction = target - start;
	direction.Normalize();
	// 뒤로 밀리는 작업
	// 뒤로 밀릴꺼니까 반대로 뒤집음
	// 1 : 밀리는 방향 2 : XY방향으로 밀릴건지 3 : Z방향으로 밀릴건지
	// 평면상으로만 움직이게 XY방향만 킨다.
	LaunchCharacter(-direction * LaunchAmount, true, false);

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

	// 타이머 델리게이션으로 0.1초 뒤 원래 색으로 돌아옴
	UKismetSystemLibrary::K2_SetTimer(this, "RestoreColor", 0.1f, false);
}

void ACEnemy::RestoreColor()
{
	FLinearColor color = Action->GetCurrent()->GetEquipmentColor();

	// 현재 무기 색으로 돌려줌
	ChangeColor(color);
}

 

 

CStateComponent.h 추가된 내용


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

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

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

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

 

 

 

 

 

 

 

CStateComponent.cpp 추가된 내용


더보기
...

void UCStateComponent::SetHittedMode()
{
	ChangeType(EStateType::Hitted);
}

 

 

 

 

 

 

 

CMontagesComponent.h 추가된 내용


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

	...

public:
	void PlayHitted();
};

 

 

 

 

 

 

 

CMontagesComponent.cpp 추가된 내용


더보기
...

void UCMontagesComponent::PlayHitted()
{
	PlayAnimMontage(EStateType::Hitted);
}

 

 

 

CAnimNotify_Hitted.h


더보기
#pragma once

#include "CoreMinimal.h"
#include "Animation/AnimNotifies/AnimNotify.h"
#include "CAnimNotify_Hitted.generated.h"

UCLASS()
class UONLINE_04_ACTION_API UCAnimNotify_Hitted : public UAnimNotify
{
	GENERATED_BODY()
	
public:
	FString GetNotifyName_Implementation() const override;

	virtual void Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation) override;

};

 

 

 

 

 

 

 

CAnimNotify_Hitted.cpp


더보기
#include "CAnimNotify_Hitted.h"
#include "Global.h"
#include "GameFramework/Character.h"
#include "Components/CStateComponent.h"

FString UCAnimNotify_Hitted::GetNotifyName_Implementation() const
{
	return "Hitted";
}

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

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

	UCStateComponent* state = CHelpers::GetComponent<UCStateComponent>(character);
	CheckNull(state);

	// IdleMode 상태로 돌려줌
	state->SetIdleMode();
}

 

CAttachment.h 수정된 내용


더보기
...

DECLARE_DYNAMIC_MULTICAST_DELEGATE(FAttachmentCollision);


UCLASS()
class UONLINE_04_ACTION_API ACAttachment : public AActor
{
	GENERATED_BODY()

	...
    
public:
	// 노티파이때 충돌체가 켜지고, 꺼진다.
	void OnCollision();
	void OffCollision();

public:
	UPROPERTY(BlueprintAssignable)
		FAttachmentCollision OnAttachmentCollision;

	UPROPERTY(BlueprintAssignable)
		FAttachmentCollision OffAttachmentCollision;
};

 

 

 

 

 

 

 

CAttachment.cpp 수정된 내용


더보기
...

void ACAttachment::OnCollision()
{
	// 켜줌
	for (UShapeComponent* component : ShapeComponents)
		component->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);

	if (OnAttachmentCollision.IsBound())
		OnAttachmentCollision.Broadcast();
}

void ACAttachment::OffCollision()
{
	// 꺼줌
	for (UShapeComponent* component : ShapeComponents)
		component->SetCollisionEnabled(ECollisionEnabled::NoCollision);

	if (OffAttachmentCollision.IsBound())
		OffAttachmentCollision.Broadcast();
}

 

CDoAction.h 수정된 내용


더보기
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Actions/CActionData.h"
#include "CDoAction.generated.h"

UCLASS()
class UONLINE_04_ACTION_API ACDoAction : public AActor
{
	GENERATED_BODY()
    
    ...

public:
	UFUNCTION()
		virtual void OnAttahmentCollision() {};
	UFUNCTION()
		virtual void OffAttahmentCollision() {};

};

 

 

 

 

 

 

 

CDoAction.cpp 수정된 내용


더보기
...

void ACDoAction_Melee::OnAttachmentBeginOverlap(class ACharacter* InAttacker, class AActor* InAttackCauser, class ACharacter* InOtherCharacter)
{
	Super::OnAttachmentBeginOverlap(InAttacker, InAttackCauser, InOtherCharacter);
	CheckNull(InOtherCharacter);

	for (const ACharacter* other : HittedCharacters)
	{
		if (InOtherCharacter == other)
			return;
	}
	HittedCharacters.Add(InOtherCharacter);
	

	// 데미지 추가 정보를 넘길때
	FDamageEvent e;

	// 1. 데미지, 2. 데미지 이벤트, 3 : Instigator(컨트롤러), 4 : DamageCursor(데미지 야기하는 친구)
	InOtherCharacter->TakeDamage(Datas[Index].Power, e, OwnerCharacter->GetController(), this);
}


void ACDoAction_Melee::OnAttachmentEndOverlap(class ACharacter* InAttacker, class AActor* InAttackCauser, class ACharacter* InOtherCharacter)
{
}

void ACDoAction_Melee::OnAttahmentCollision()
{
}

void ACDoAction_Melee::OffAttahmentCollision()
{
	HittedCharacters.Empty();
}

void ACDoAction_Melee::RestoreDilation()
{
	UGameplayStatics::SetGlobalTimeDilation(GetWorld(), 1.0f);
}

 

 

CDoAction_Melee.h 수정된 내용


더보기
#pragma once

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

UCLASS()
class UONLINE_04_ACTION_API ACDoAction_Melee : public ACDoAction
{
	GENERATED_BODY()
	...
    
public:
	virtual void OnAttahmentCollision() override;
	virtual void OffAttahmentCollision() override;
    
private:
	TArray<ACharacter*> HittedCharacters;
};

 

 

 

 

 

 

 

CDoAction_Melee.cpp 수정된 내용


더보기
...

void ACDoAction_Melee::OnAttachmentBeginOverlap(class ACharacter* InAttacker, class AActor* InAttackCauser, class ACharacter* InOtherCharacter)
{
	Super::OnAttachmentBeginOverlap(InAttacker, InAttackCauser, InOtherCharacter);
	CheckNull(InOtherCharacter);

	for (const ACharacter* other : HittedCharacters)
	{
		if (InOtherCharacter == other)
			return;
	}
	HittedCharacters.Add(InOtherCharacter);
	

	// 데미지 추가 정보를 넘길때
	FDamageEvent e;

	// 1. 데미지, 2. 데미지 이벤트, 3 : Instigator(컨트롤러), 4 : DamageCursor(데미지 야기하는 친구)
	InOtherCharacter->TakeDamage(Datas[Index].Power, e, OwnerCharacter->GetController(), this);
}


void ACDoAction_Melee::OnAttachmentEndOverlap(class ACharacter* InAttacker, class AActor* InAttackCauser, class ACharacter* InOtherCharacter)
{
}

void ACDoAction_Melee::OnAttahmentCollision()
{
}

void ACDoAction_Melee::OffAttahmentCollision()
{
	HittedCharacters.Empty();
}

 

 

CActionData.cpp 수정된 내용


더보기
#include "CActionData.h"
#include "Global.h"
#include "CAttachment.h"
#include "CEquipment.h"
#include "CDoAction.h"
#include "GameFramework/Character.h"
#include "Components/SkeletalMeshComponent.h"

...

void UCActionData::BeginPlay(class ACharacter* InOwnerCharacter)
{
	...
    
	if(!!DoActionClass)
	{
		DoAction = InOwnerCharacter->GetWorld()->SpawnActorDeferred<ACDoAction>(DoActionClass, transform, InOwnerCharacter);
		DoAction->AttachToComponent(InOwnerCharacter->GetMesh(), FAttachmentTransformRules(EAttachmentRule::KeepRelative, true));
		DoAction->SetActorLabel(GetLableName(InOwnerCharacter, "DoAction"));
		DoAction->SetDatas(DoActionDatas);
		UGameplayStatics::FinishSpawningActor(DoAction, transform);

		if (!!Equipment)
		{
			DoAction->SetEquipped(Equipment->GetEquipped());
		}

		if (!!Attachment)
		{
			// 충돌 함수 연결해줌
			Attachment->OnAttachmentBeginOverlap.AddDynamic(DoAction, &ACDoAction::OnAttachmentBeginOverlap);
			Attachment->OnAttachmentEndOverlap.AddDynamic(DoAction, &ACDoAction::OnAttachmentEndOverlap);

			Attachment->OnAttachmentCollision.AddDynamic(DoAction, &ACDoAction::OnAttahmentCollision);
			Attachment->OffAttachmentCollision.AddDynamic(DoAction, &ACDoAction::OffAttahmentCollision);
		}
	}
}

 

 

결과