초보 코린이의 성장 일지

UE4 Hit Effect, Status Component, Hammer 본문

언리얼

UE4 Hit Effect, Status Component, Hammer

코오린이 2023. 5. 10. 16:47

Hit시 이펙트가 나오도록 만들어 볼 것이다.

더보기
#pragma once

#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "CWeaponStructures.generated.h"

USTRUCT()
struct FDoActionData // 액션 구조체
{
	GENERATED_BODY()

public:
	// 에디터에서 세팅은 해야하므로, EditAnywhere.
	UPROPERTY(EditAnywhere)
		class UAnimMontage* Montage;

	UPROPERTY(EditAnywhere)
		float PlayRate = 1;

	UPROPERTY(EditAnywhere)
		bool bCanMove = true;

	UPROPERTY(EditAnywhere)
		bool bFixedCamera;

	UPROPERTY(EditAnywhere)
		// UFXSystemAsset 파티클, 나이아가라 공통 부모
		class UFXSystemAsset* Effect; // 파티클, 나이아가라 사용하기 위해.

	UPROPERTY(EditAnywhere)
		// 방향 보정치
		FVector EffectLocation = FVector::ZeroVector;

	UPROPERTY(EditAnywhere)
		// 이팩트 크기 보정.
		FVector EffectScale = FVector::OneVector;

public:
	// 액션을 이곳에 넣기 위한 함수
	void DoAction(class ACharacter* InOwner);
	void PlayEffect(UWorld* InWorld, const FVector& InLocation);
	void PlayEffect(UWorld* InWorld, const FVector& InLocation, const FRotator& InRotation); 
};

USTRUCT()
struct FHitData // 액션 구조체
{
	GENERATED_BODY()

public:
	// 에디터에서 세팅은 해야하므로, EditAnywhere.
	UPROPERTY(EditAnywhere)
		class UAnimMontage* Montage;

	UPROPERTY(EditAnywhere)
		float PlayRate = 1;

	UPROPERTY(EditAnywhere)
		float Power;

	UPROPERTY(EditAnywhere)
		float Launch = 100;

	UPROPERTY(EditAnywhere)
		float StopTime;

	UPROPERTY(EditAnywhere)
		class USoundWave* Sound;

	UPROPERTY(EditAnywhere)
		// UFXSystemAsset 파티클, 나이아가라 공통 부모
		class UFXSystemAsset* Effect; // 파티클, 나이아가라 사용하기 위해.

	UPROPERTY(EditAnywhere)
		// 방향 보정치
		FVector EffectLocation = FVector::ZeroVector;

	UPROPERTY(EditAnywhere)
		// 이팩트 크기 보정.
		FVector EffectScale = FVector::OneVector;

public:
	void PlayEffect(UWorld* InWorld, const FVector& InLocation); // 위치 보정
	void PlayEffect(UWorld* InWorld, const FVector& InLocation, const FRotator& InRotation); // 회전 방향으로 보정 

};
#include "Weapons/CWeaponStructures.h"
#include "Global.h"
#include "GameFramework/Character.h"
#include "Components/CStateComponent.h"
#include "Components/CMovementComponent.h"
#include "Animation/AnimMontage.h"

void FDoActionData::PlayEffect(UWorld* InWorld, const FVector& InLocation)
{
	CheckNull(Effect);

	FTransform transform; // 보정용
	transform.SetLocation(EffectLocation);
	transform.SetScale3D(EffectScale);
	transform.AddToTranslation(InLocation);

	CHelpers::PlayEffect(InWorld, Effect, transform);
}

void FDoActionData::PlayEffect(UWorld* InWorld, const FVector& InLocation, const FRotator& InRotation)
{
	FTransform transform;
	// 파티클이 플레이될 위치 + 회전공간 이동
	transform.SetLocation(InLocation + InRotation.RotateVector(EffectLocation));
	transform.SetScale3D(EffectScale);

	CHelpers::PlayEffect(InWorld, Effect, transform);
}

///////////////////////////////////////////////////////////////////////////////

void FHitData::PlayEffect(UWorld* InWorld, const FVector& InLocation)
{
	CheckNull(Effect);

	FTransform transform; // 보정용
	transform.SetLocation(EffectLocation); 
	transform.SetScale3D(EffectScale);
	transform.AddToTranslation(InLocation);

	CHelpers::PlayEffect(InWorld, Effect, transform);
}

void FHitData::PlayEffect(UWorld* InWorld, const FVector& InLocation, const FRotator& InRotation)
{
	FTransform transform;
	// 파티클이 플레이될 위치 + 회전공간 이동
	transform.SetLocation(InLocation + InRotation.RotateVector(EffectLocation));
	transform.SetScale3D(EffectScale);

	CHelpers::PlayEffect(InWorld, Effect, transform);
}

1. 파티클이 생성되서 위치될 공간이 어디인지 만들어 놓는다. (사용처가 다를 수 있으므로 2개 생성)

2. Hit 구조체인 FHitData에 함수 정의, 액션이 들어가는 FDoActionData에서도 동일하게 함수 정의해준다.

3. FDoActionData에도 적용시켜줘야 공격할때 이펙트가 나오게 된다.


1. Hit되는 객체 Enemy로 와서 Effect가 나올수 있게 Hitted안에 넣어준다.

1. Hit 몽타주에서 Hit나올 이펙트를 설정해준다.

2. Location에 값을 주게되면 Hit 객체로부터 x, y, z 만큼 이동하여 이펙트가 나오게 될 것이다.

3. 3번째 Hit때 이펙트를 넣어놨기 때문에 마지막 Hit시 이펙트가 나오게된다.


1. 위에 작성했던 Hitted 부분에서 Hit된 객체가 때린 Attacker를 바라보게 만들어 준다.

2. Hit시 객체가 때린 Attacker을 바라보게 될 것이다.


Hp관리해주는 컴포넌트를 하나 생성해 줄 것이다.

1. ActorComponent로 클래스를 생성해준다.

더보기
#pragma once

#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "CStatusComponent.generated.h"


UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
class U2212_06_API UCStatusComponent : public UActorComponent
{
    GENERATED_BODY()

private:
    UPROPERTY(EditAnywhere, Category = "Health")
        float MaxHealth = 100; // 최대 Hp

public:
    // Hp 체크 return 함수
    FORCEINLINE float GetHealth() { return Health; }
    FORCEINLINE bool IsDead() { return Health <= 0.0f; }

public:
    UCStatusComponent();

protected:
    virtual void BeginPlay() override;

public:
    void Damage(float InAmount);

private:
    class ACharacter* OwnerCharacter;

private:
    float Health;
};
#include "Components/CStatusComponent.h"
#include "Global.h"
#include "GameFramework/Character.h"

UCStatusComponent::UCStatusComponent()
{

}

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

	OwnerCharacter = Cast<ACharacter>(GetOwner());

	// 시작할떄 최대 Hp 세팅
	Health = MaxHealth;
}

void UCStatusComponent::Damage(float InAmount)
{
	Health += (InAmount * -1.0f);
	Health = FMath::Clamp(Health, 0.0f, MaxHealth);
}

1. HP를 세팅해주고, Damage가 들어올시 차감해준다.


더보기
#pragma once

#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "ICharacter.generated.h"

// 인터페이스 직렬화를 위한 기본코드 이므로 수정하면 안된다.
UINTERFACE(MinimalAPI)
class UICharacter : public UInterface
{
	GENERATED_BODY()
};

class U2212_06_API IICharacter
{
	GENERATED_BODY()

public:
	// 자식에서 의무적으로 구현하게되면 순수 가상 함수로(추상) 들어가지만,
	// 아니라면 일반 가상 함수로 들어간다.
	virtual void End_BackStep() {}
	virtual void End_Hitted() {}
	virtual void End_Dead() {}


public:
	void Create_DynamicMaterial(class ACharacter* InCharacter);
	void Change_Color(class ACharacter* InCharacter, FLinearColor InColor);
};

1. 인터페이스로 와서 End_Hitted, End_Dead를 만들어 준다.


더보기
#pragma once

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

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

private:
	// 기본 Enemy에도 필요한 것, Player가 아니므로 Camera, SpringArm은 x
	UPROPERTY(VisibleAnywhere)
		class UCWeaponComponent* Weapon;

	UPROPERTY(VisibleAnywhere)
		class UCMontagesComponent* Montages;

	UPROPERTY(VisibleAnywhere)
		class UCMovementComponent* Movement;

	UPROPERTY(VisibleAnywhere)
		class UCStateComponent* State;

	UPROPERTY(VisibleAnywhere)
		class UCStatusComponent* Status;

public:
	ACEnemy();

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

private:
	void Hitted();

public:
	void End_Hitted() override;

private:
	UFUNCTION()
		void RestoreColor();

private:
	void Dead();

public:
	void End_Dead() override;

};
#include "Characters/CEnemy.h"
#include "Global.h"
#include "CAnimInstance.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "Components/SkeletalMeshComponent.h"
#include "Components/CapsuleComponent.h"
#include "Components/CWeaponComponent.h"
#include "Components/CMontagesComponent.h"
#include "Components/CMovementComponent.h"
#include "Components/CStatusComponent.h"
#include "Weapons/CWeaponStructures.h"

ACEnemy::ACEnemy()
{
	// 아래부분은 Player과 동일하게 가지고 있다.
	// 생성
	CHelpers::CreateActorComponent<UCWeaponComponent>(this, &Weapon, "Weapon");
	CHelpers::CreateActorComponent<UCMontagesComponent>(this, &Montages, "Montages");
	CHelpers::CreateActorComponent<UCMovementComponent>(this, &Movement, "Movement");
	CHelpers::CreateActorComponent<UCStateComponent>(this, &State, "State");
	CHelpers::CreateActorComponent<UCStatusComponent>(this, &Status, "Status");
}

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

void ACEnemy::Hitted()
{
	// Apply Damage
	{
		Status->Damage(Damage.Power);
		Damage.Power = 0;

	}
	
	//TODO: 데미지 처리
	//TODO: 사망 처리

		if (Status->IsDead() == false)
		{
			// 때린 객체 바라보게 만들기.
			FVector start = GetActorLocation();
			FVector target = Damage.Character->GetActorLocation();
			FVector direction = target - start;
			direction.Normalize();

			LaunchCharacter(-direction * data->Launch, false, false); // 밀리는 Launch값
			SetActorRotation(UKismetMathLibrary::FindLookAtRotation(start, target)); // 바라보게 만들기

		}
	} 

	if (Status->IsDead())
	{
		State->SetDeadMode();

		return;
	}

	Damage.Character = nullptr;
	Damage.Causer = nullptr;
	Damage.Event = nullptr;
}

void ACEnemy::End_Hitted()
{
	State->SetIdleMode();
}

void ACEnemy::Dead()
{
	GetCapsuleComponent()->SetCollisionEnabled(ECollisionEnabled::NoCollision);

	Montages->PlayDeadMode();
}

void ACEnemy::End_Dead()
{
	Destroy();
}

1. Enemy로 넘어와서 Status를 만들어준다.

2. End_Hitted, End_Dead를 인터페이스에서 받아온다.

3. 예외처리 중 하나는 Dead 아닐때는 때린 객체 바라보고, Dead라면 못바라보도록 처리해준다.


더보기
#pragma once

#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "Components/CStateComponent.h"
#include "Engine/DataTable.h"
#include "CMontagesComponent.generated.h"

UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class U2212_06_API UCMontagesComponent : public UActorComponent
{
	GENERATED_BODY()
    
public:
	void PlayBackStepMode(); // 빽스탭
	void PlayDeadMode();

};
#include "Components/CMontagesComponent.h"
#include "Global.h"
#include "GameFramework/Character.h"

void UCMontagesComponent::PlayBackStepMode()
{
	PlayAnimMontage(EStateType::BackStep);
}

void UCMontagesComponent::PlayDeadMode()
{
	PlayAnimMontage(EStateType::Dead);

}

1. 몽타주 컴포넌트로와서 DeadMode 정의해준다.

2. 이제 몽타주도 나오게 될 것이다.


1. Hit되는 몽타주와 Dead시 나오는 몽타주 둘다 노티파이 스테이트로 바꿔주고, Type 알맞게 변경해준다.


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

FString UCAnimNotify_EndState::GetNotifyName_Implementation() const
{
	return "EndState";
}

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

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

	switch (StateType)
	{
		case EStateType::BackStep: character->End_BackStep(); break;
		case EStateType::Hitted: character->End_Hitted(); break;
		case EStateType::Dead: character->End_Dead(); break;

	}
}

1. EndState 노티파이로와서 끝을 낼 수 있게끔 switch안에 Hitted, Dead를 추가해준다.


1. Player에 데이터 테이블을 복사해서, Enemy에 넣어주고 몽타주를 Dead로 변경해준다.

2. 에디터로 돌아와서 타입에 맞게 설정하고 임포트 해준다.

 

1. 몽타주 컴포넌트를 클릭하여, 임포트한 데이터 테이블을 넣어준다.


이제 무기를 추가해 볼 것이다. 이미 컴포넌트와 구조가 짜여져 있기 때문에 부분 추가만 해주면 완성된다.

1. Sword를 그대로 복사해서 Hammer로 변경해준다.

1. 지금 가지고 있는 Hammer이 스태틱 메시로 되어있는데, 이걸 간편하게 사용하기 위해 Skeleton으로 변경해서 사용 할 것이다.

1. 애셋 액션 -> 익스포트를 누른다 -> 편한곳에 파일을 저장해준다.

1. 처음에 불러들인 FPX 버전으로 나타난다.

2. 익스포트를 또 눌러준다.

 

1. 원하는 폴도에서 임포트를 눌러준다.

2. 저장해놓은 파일을 클릭해준다.

1. Skeleton Mesh를 체크해주고 모두 임포트 해준다.

1. 파일안에 7개가 생성되는데 그 중 필요없는 몇가지를 삭제해준다.

2. 이제 Hammer Skeleton Mesh가 생성됐다.

1. 하얀색으로 되어있는 Hammer에 머터리얼을 교체해준다.

1. 나머지 설정들을 해주고, Capsule도 추가하여 알맞게 값을 설정해준다.

1. 시작하면 숨겨주고, Equip가 호출되면 무기를 보이게하고 손으로 장착해준다.

1. Hammer에 Combo 타수만큼 몽타주를 넣어주고, Hit 몽타주도 알맞은 Combo 숫자만큼 몽타주와 값들을 넣어준다.

1. Player로와서 Weapon에 Hammer를 등록해준다.

 

Combo_1
Combo_2
Combo_3
Combo_4

1. Hammer 몽타주에 노티파이를 알맞게 배치해준다.


이제 Hammer 이벤트 연결을해 줄 것이다.

1. 무기 Hammer을 추가해준다.

1. C++에 한 줄 추가를 하는 것만으로 Hammer 장착이 끝이난다. (구조가 이만큼 중요하다)


1. Sword 스테이트 머신을 복사해주고, Hammer로 변경해준다.

2. Hammer 스테이트 머신으로 들어가서, BS_Hammer를 올려주고 연결해준다.

3. 캐시 포즈를 추가해주고 블랜드 포즈에 연결해준다.


이제 Player가 공격을해서 Target을 조금이라도 맞췄다면, 맞춘 대상을 바라보도록 만들어 줄 것이다.

더보기
#pragma once

#include "CoreMinimal.h"
#include "Weapons/CDoAction.h"
#include "CDoAction_Combo.generated.h"

UCLASS()
class U2212_06_API UCDoAction_Combo : public UCDoAction
{
	GENERATED_BODY()

public:
// 직렬화 표시를 안해줘도 상속된 부분을 가져왔기 때문에 직렬화가 되어있다.
	void OnAttachmentBeginOverlap(class ACharacter* InAttacker, AActor* InAttackCuaser, class ACharacter* InOther) override;
	void OnAttachmentEndCollision() override;

private:
	TArray<class ACharacter*> Hitted;
};
#include "Weapons/DoActions/CDoAction_Combo.h"
#include "Global.h"
#include "GameFramework/Character.h"
#include "Components/CStateComponent.h"

void UCDoAction_Combo::OnAttachmentBeginOverlap(ACharacter* InAttacker, AActor* InAttackCauser, ACharacter* InOther)
{
	Super::OnAttachmentBeginOverlap(InAttacker, InAttackCauser, InOther);
	CheckNull(InOther);

	////CLog::Log(InOther->GetName());

	//FActionDamageEvent e;
	//e.HitData = &HitDatas[0];

	for (ACharacter* hitted : Hitted)
		CheckTrue(hitted == InOther);

	Hitted.AddUnique(InOther); // 추가

	CheckTrue(HitDatas.Num() - 1 < Index) // 인덱스보다 작으면 사이즈가 안맞으므로 하면 안된다.

	//InOther->TakeDamage(e.HitData->Power, e, FDamageEvent(), InAttacker->GetController(), InAttackCauser);
	HitDatas[Index].SendDamage(InAttacker, InAttackCauser, InOther);
}

void UCDoAction_Combo::OnAttachmentEndCollision()
{
	Super::OnAttachmentEndCollision();

	float angle = -2.0f; // 내적 최소값 
	ACharacter* candidate = nullptr;

	for (ACharacter* hitted : Hitted)
	{
		// 바라보게 하기
		FVector direction = hitted->GetActorLocation() - OwnerCharacter->GetActorLocation();
		direction = direction.GetSafeNormal2D();

		// 전방백터
		FVector forward = FQuat(OwnerCharacter->GetActorRotation()).GetForwardVector();

		// 내적
		float dot = FVector::DotProduct(direction, forward);
		if (dot >= angle) // 최대값 구하기, 최대값은 1에 가까운값
		{
			angle = dot;
			candidate = hitted;
		}

	}
		if (!!candidate)
		{
			// 회전 시키기
			FRotator rotator = UKismetMathLibrary::FindLookAtRotation(OwnerCharacter->GetActorLocation(), candidate->GetActorLocation());
			FRotator target = FRotator(0, rotator.Yaw, 0);

			AController* controller = OwnerCharacter->GetController<AController>();
			controller->SetControlRotation(target);

		}

		Hitted.Empty(); // Hitted 제거
}

1. EndCollision을 가져와서 override해준다.

2. Hit시 캐릭이 맞은 타겟을 바라보게 처리해준다.

 

https://www.youtube.com/watch?v=7f-WOWXZrrA 

 

'언리얼' 카테고리의 다른 글

UE4 Weapon Plugin  (0) 2023.05.26
UE4 Fist, Camera Shake  (0) 2023.05.11
UE4 HitData, Effect, Sound, HitStop  (0) 2023.05.09
UE4 AnimNotify, ComboState, TakeDamage  (0) 2023.05.08
UE4 Equip, DoAction  (2) 2023.05.04
Comments