EDR_Project / Character (10)

24.11.23 Character의 수정사항입니다.

공격 애니메이션 몽타주들의 종류를 늘려서 랜덤 부분에 퍼센티지 조정 하였고, 필요 없는 부분 삭제 하였습니다.

 

MyCharacter.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once
#include "Sound/SoundCue.h"
#include "Kismet/GameplayStatics.h"
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "Components/CapsuleComponent.h"
#include "EDR_Enemy_Weapon.h"
#include "MyCharacter.generated.h"


// 공격 애니메이션이 끝났는지 확인
DECLARE_MULTICAST_DELEGATE(FOnAttackEndDelegate);

DECLARE_MULTICAST_DELEGATE(FOnFightStartEndDelegate);


UCLASS()
class EDR_API AMyCharacter : public ACharacter
{
	GENERATED_BODY()

public:
	// Sets default values for this character's properties

	AMyCharacter();
	virtual void PossessedBy(AController* NewController)override;

	// 공격 히트 시 재생할 사운드
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Sound")
	TArray<TObjectPtr<class USoundCue>> HitSoundCue;

	// 공격 미스 시 재생할 사운드
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Sound")
	TArray<TObjectPtr<class USoundCue>> MissSoundCue;

	// 공격 시 재생할 사운드
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Sound")
	USoundCue* AttackSoundCue;

	// 스킬 시 재생할 사운드
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Sound")
	USoundCue* SkillSoundCue;

	// 사망 시 재생할 사운드
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Sound")
	USoundCue* DeathSoundCue;

protected:

	// 걷는 속도 관련
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement")
	float TargetSpeed = 200.f; // 목표 속도

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement")
	float Acceleration = 120.0f;  // 가속도

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement")
	float CurrentSpeed = 0.0f; // 현재 속도

	// 이동중인지 체크
	UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = "Movement")
	bool IsMoving = false;




	UPROPERTY(VisibleAnywhere, Category = Weapone)
	// 무기
	TObjectPtr<class AEDR_Enemy_Weapon> CurrentWeapon;


	// 정지 관련 함수
	void StopMovement();

	void ResumeMovement();

	// Called when the game starts or when spawned
	virtual void BeginPlay() override;
	// 받는 데미지 처리 함수
	virtual float TakeDamage(float DamageAmount, struct FDamageEvent const& DamageEvent, class AController* EventInstigator, AActor* DamageCauser) override;

	UPROPERTY(BluePrintReadWrite)
	float hp = 100.0f;

	// 보스 캐릭터의 공격 데미지 설정 변수
	UPROPERTY(BluePrintReadWrite)
	float AttackDamage;
	// 보스 캐릭터의 스킬 데미지 설정 변수
	UPROPERTY(BluePrintReadWrite)
	float SkillDamage;

	// 공격 사거리
	UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = Attack, Meta = (AllowPrivateAccess = true))
	float AttackRange;

	UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = Attack, Meta = (AllowPrivateAccess = true))
	float AttackRadius;


	// 스킬 사거리
	UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = Attack, Meta = (AllowPrivateAccess = true))
	float SkillRange;

	UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = Attack, Meta = (AllowPrivateAccess = true))
	float SkillRadius;

	// 공격 애니메이션
	UPROPERTY(EditAnywhere)
	TArray<TObjectPtr<class UAnimMontage>> AttackMontage;

	//  스킬 애니메이션
	UPROPERTY(EditAnywhere)
	TObjectPtr<class UAnimMontage> SkillMontage;

	// 사망 애니메이션
	UPROPERTY(EditAnywhere)
	TObjectPtr<class UAnimMontage> DeathMontage;

	// 전투 시작
	UPROPERTY(EditAnywhere)
	TObjectPtr<class UAnimMontage> FightStartMontage;





	// 애니메이션 재생을 위해 공격중인지 확인
	UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = Attack, Meta = (AllowPrivateAccess = true))
	bool IsAttacking;


	// 애니메이션 인스턴트 객체
	UPROPERTY()
	class UAnim_EDR_AnimInstance* EDRAnim;

public:

	// 전투 시작 애니메이션 확인
	UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = Attack, Meta = (AllowPrivateAccess = true))
	bool IsFightStarting = false;

	// Called every frame
	virtual void Tick(float DeltaTime) override;

	UPROPERTY(BluePrintReadWrite)
	bool Death = false;

	// Called to bind functionality to input
	virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;



	// hp 반환
	UFUNCTION(BlueprintPure, category = "Player")
	float GetHp() { return hp; }


	// hp 업데이트
	UFUNCTION(BlueprintCallable, category = "Player")
	virtual void UpdateHP(float NewHP);


	// 사망 애니메이션 재생
	UFUNCTION(BlueprintCallable, category = "Player")
	virtual void IsDeath();


	// 전투 시작 애니메이션 끝났을 때 호출할 델리게이트
	FOnFightStartEndDelegate OnFightStartEnd;

	// 새로운 함수 추가
	UFUNCTION()
	void OnFightStartMontageEnded(UAnimMontage* Montage, bool bInterrupted);

	// 전투 시작시 애니메이션 재생하는 함수
	void FightStart();


	// 공격 함수
	virtual void Attack();

	// 공격 애니메이션 끝났는지 체크
	FOnAttackEndDelegate OnAttackEnd;



	// 공격 히트 체크
	UFUNCTION()
	void AttackCheck();
	UFUNCTION()
	void OnAttackMontageEnded(UAnimMontage* Montage, bool bInterrupted);


	UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = Attack, Meta = (AllowPrivateAccess = true))
	bool bCanAttackSmallMove; //공격미세이동여부. 
	UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = Attack, Meta = (AllowPrivateAccess = true))FVector ExpectedAttackLocation; //공격미세이동목표값.


private:

	FTimerHandle StopMovementTimerHandle;
	int32 RandomValue;
};
반응형

MyCharacter.cpp

// Fill out your copyright notice in the Description page of Project Settings.


#include "MyCharacter.h"
#include "Engine/World.h"
#include "Anim_EDR_AnimInstance.h"
#include "Components/CapsuleComponent.h"
#include "DrawDebugHelpers.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "Engine/DamageEvents.h"

// Sets default values
AMyCharacter::AMyCharacter()
{
	// Set this character to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;
	IsAttacking = false;
	IsFightStarting = false;
	// 캡슐컴포넌트가 MyCharacter프리셋을 사용하도록 함
	GetCapsuleComponent()->SetCollisionProfileName(TEXT("MyCharacter"));


}

void AMyCharacter::PossessedBy(AController* NewController)
{
	Super::PossessedBy(NewController);

	// 캐릭터 회전 부드럽게
	bUseControllerRotationYaw = false;
	GetCharacterMovement()->bUseControllerDesiredRotation = false;
	GetCharacterMovement()->bOrientRotationToMovement = true;
	GetCharacterMovement()->RotationRate = FRotator(0.0f, 480.f, 0.0f);
}

// Called when the game starts or when spawned
void AMyCharacter::BeginPlay()
{
	Super::BeginPlay();

}

// Called every frame
void AMyCharacter::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);
	//// 이동 중인지 확인
	if (GetVelocity().Size() > 0)
	{
		IsMoving = true;
	}
	else
	{
		IsMoving = false;
	}
	// 걷다가 멈추면 현재 속도 다시 초기화
	if (!IsMoving)
	{
		CurrentSpeed = 0.0f;
	}


	// 현재 속도 증가
	if (CurrentSpeed < TargetSpeed)
	{
		CurrentSpeed += Acceleration * DeltaTime;
		if (CurrentSpeed > TargetSpeed)
		{
			CurrentSpeed = TargetSpeed; // 최대 속도 제한
		}
	}




	// 이동 속도 업데이트
	GetCharacterMovement()->MaxWalkSpeed = CurrentSpeed;
}

// Called to bind functionality to input
void AMyCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);

}



// 캐릭터 정지 관련 함수
void AMyCharacter::StopMovement()
{
	// 이동을 멈추기 위해 MaxWalkSpeed를 0으로 설정
	GetCharacterMovement()->MaxWalkSpeed = 0.0f;

	// 2초 후에 이동을 재개
	GetWorld()->GetTimerManager().SetTimer(StopMovementTimerHandle, this, &AMyCharacter::ResumeMovement, 2.0f, false);
}

void AMyCharacter::ResumeMovement()
{
	// 원래의 이동 속도로 복원 (예: 200으로 설정)
	GetCharacterMovement()->MaxWalkSpeed = 200.0f; // 원하는 속도로 설정
}





// 사망 애니메이션



void AMyCharacter::IsDeath()
{
	// 이미 죽어있으면 애니메이션 실행 안함
	if (Death)
	{
		GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Red, TEXT("Death true"));
		return;
	}

	// 애니메이션 몽타주 실행
	PlayAnimMontage(DeathMontage, 1.0f);
	Death = true;
	// 스킬 사운드 재생
	if (DeathSoundCue != nullptr)
	{
		UGameplayStatics::SpawnSoundAttached(
			DeathSoundCue,
			GetRootComponent(),
			NAME_None,
			FVector::ZeroVector,
			EAttachLocation::KeepRelativeOffset,
			false,
			1.0f,  // Volume multiplier
			0.7f   // Pitch multiplier, 0.5로 설정하면 재생 속도가 절반으로 느려짐
		);
	}
}
void AMyCharacter::UpdateHP(float NewHP)
{
	hp += NewHP;

	GEngine->AddOnScreenDebugMessage(-1, 3.f, FColor::Red, FString::Printf(TEXT("UpdateHP() - %s HP : %f"), *GetName(), hp));
}




// 전투 시작

void AMyCharacter::FightStart()
{
	EDRAnim = Cast<UAnim_EDR_AnimInstance>(GetMesh()->GetAnimInstance());
	if (FightStartMontage)
	{
		PlayAnimMontage(FightStartMontage, 1.0f);
		GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Yellow, TEXT("FightStartMontage Played"));
	}

	if (EDRAnim != nullptr)
	{
		EDRAnim->OnMontageEnded.RemoveAll(this);  // 기존 이벤트 제거
		EDRAnim->OnMontageEnded.AddDynamic(this, &AMyCharacter::OnFightStartMontageEnded);
	}
}

// 전투 시작 애니메이션 종료 되면 호출
void AMyCharacter::OnFightStartMontageEnded(UAnimMontage* Montage, bool bInterrupted)
{
	// FightStart 애니메이션이 끝났을 때 공격 시작
	GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Blue, FString::Printf(TEXT("bInterrupted: %s"), bInterrupted ? TEXT("True") : TEXT("False")));
	GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Red, TEXT("Fight Start Animation Ended"));

	// 애니메이션 종료 확인
	IsFightStarting = true;
	//StopMovement();
	OnFightStartEnd.Broadcast();


}




// 공격 관련

void AMyCharacter::Attack()
{
	bCanAttackSmallMove = true; //미세전진 가능
	ExpectedAttackLocation = GetActorLocation() + GetActorForwardVector() * 1000.0f;//전진시킬 목표위치 지정.
	// 공격, 스킬 확률
	RandomValue = FMath::RandRange(0, 100);
	if (Death)
	{
		GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Red, TEXT("is Death true"));
		return;
	}

	if (IsAttacking)
	{
		GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Red, TEXT("is attacking true"));
		return;
	}

	EDRAnim = Cast<UAnim_EDR_AnimInstance>(GetMesh()->GetAnimInstance());

	if (nullptr == EDRAnim)
	{
		return;
	}

	// 20%확률로 스킬 발동
	if (RandomValue <= 20)
	{
		GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Red, TEXT("Skill!!!!!!!!!!!!!!!!!"));
		PlayAnimMontage(SkillMontage, 1.3f);

		// 스킬 사운드 재생
		if (SkillSoundCue != nullptr)
		{
			UGameplayStatics::SpawnSoundAttached(
				SkillSoundCue,
				GetRootComponent(),
				NAME_None,
				FVector::ZeroVector,
				EAttachLocation::KeepRelativeOffset,
				false,
				1.0f,  // Volume multiplier
				0.5f   // Pitch multiplier
			);
		}
	}
	else
	{
		int aRandom = FMath::RandRange(0, 100);
		GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Red, TEXT("Attack!@!@!@!@!@"));
		if (aRandom < 20 && aRandom >= 0)
		{
			if (AttackMontage.IsValidIndex(0))
			{
				GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Red, TEXT("asdfasjkdfhslkfjhaslkjh"));
				PlayAnimMontage(AttackMontage[0], 1.0f);
			}
		}
		else if (aRandom < 40 && aRandom >= 20)
		{
			if (AttackMontage.IsValidIndex(1))
			{
				GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Red, TEXT("asdfasjkdfhslkfjhaslkjh"));
				PlayAnimMontage(AttackMontage[1], 1.0f);
			}
		}
		else if (aRandom < 60 && aRandom >= 40)
		{
			if (AttackMontage.IsValidIndex(2))
			{
				GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Red, TEXT("asdfasjkdfhslkfjhaslkjh"));
				PlayAnimMontage(AttackMontage[2], 1.0f);
			}
		}
		else if (aRandom < 80 && aRandom >= 60)
		{
			if (AttackMontage.IsValidIndex(3))
			{
				GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Red, TEXT("asdfasjkdfhslkfjhaslkjh"));
				PlayAnimMontage(AttackMontage[3], 1.0f);
			}
		}
		else
		{
			if (AttackMontage.IsValidIndex(4))
			{
				GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Red, TEXT("asdfasjkdfhslkfjhaslkjh"));
				PlayAnimMontage(AttackMontage[4], 1.0f);
			}
		}

		// 공격 사운드 재생
		if (AttackSoundCue != nullptr)
		{
			UGameplayStatics::SpawnSoundAttached(
				AttackSoundCue,
				GetRootComponent(),
				NAME_None,
				FVector::ZeroVector,
				EAttachLocation::KeepRelativeOffset,
				false,
				1.0f,  // Volume multiplier
				0.5f   // Pitch multiplier
			);
		}
	}

	IsAttacking = true;

	// 공격 판정 이벤트 중복 등록 방지: 기존에 바인딩된 이벤트가 있으면 제거
	EDRAnim->OnAttackHitCheck.RemoveAll(this);

	// 공격 판정 이벤트 바인딩
	EDRAnim->OnAttackHitCheck.AddUObject(this, &AMyCharacter::AttackCheck);

	// 애니메이션 종료 시 공격 끝 처리
	EDRAnim->OnMontageEnded.RemoveAll(this);  // 중복 바인딩 방지
	EDRAnim->OnMontageEnded.AddDynamic(this, &AMyCharacter::OnAttackMontageEnded);
}



// 공격 애니메이션 종료 처리 함수
void AMyCharacter::OnAttackMontageEnded(UAnimMontage* Montage, bool bInterrupted)
{
	if (!IsAttacking)
	{
		return;
	}

	IsAttacking = false;

	// 공격 종료 시 이벤트 해제
	if (EDRAnim != nullptr)
	{
		EDRAnim->OnAttackHitCheck.RemoveAll(this);  // 중복 호출 방지
	}

	GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Red, TEXT("Attack Animation Ended"));
	OnAttackEnd.Broadcast();
}





//  공격 판정


void AMyCharacter::AttackCheck()
{
	// 스킬 판정
	if (RandomValue <= 30)
	{
		GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Green, TEXT("SkillCheck~~~~~~~~~~~~~~~~~~"));
		FHitResult HitResult;
		FCollisionQueryParams Params(NAME_None, false, this);

		// 구체의 중심을 캐릭터 발 앞에 설정
		FVector StartLocation = GetActorLocation() + FVector(0.0f, 0.0f, -GetCapsuleComponent()->GetScaledCapsuleHalfHeight());
		FVector ForwardLocation = StartLocation + GetActorForwardVector() * SkillRange;

		bool bResult = GetWorld()->SweepSingleByChannel(
			HitResult, // 물리적 충돌이 탐지된 경우 관련 정보를 담을 구조체
			StartLocation, // 탐색 시작 위치
			ForwardLocation,
			FQuat::Identity,
			ECollisionChannel::ECC_GameTraceChannel2, // 물리 충돌 감지에 사용할 트레이스 채널
			FCollisionShape::MakeSphere(SkillRadius), // 탐색 사용할  도형
			Params);

		// 디버그 출력 정보를 담은 변수
#if ENABLE_DRAW_DEBUG
		FVector TraceVec = GetActorForwardVector() * SkillRange;
		FVector Center = StartLocation + TraceVec * 0.5f;  // 구체의 중심을 설정
		FQuat CapsuleRot = FRotationMatrix::MakeFromZ(TraceVec).ToQuat();
		FColor DrawColor = bResult ? FColor::Green : FColor::Red;
		float DebugLifeTime = 1.0f;

		// 디버그 출력
		DrawDebugSphere(GetWorld(),
			ForwardLocation,
			SkillRadius,
			12, // 세그먼트 수, 더 많을수록 부드러워짐
			DrawColor,
			false,
			DebugLifeTime);

#endif
		// 액터 감지시
		if (bResult)
		{
			if (HitResult.GetActor() != nullptr)
			{
				// 히트 사운드 재생
				if (HitSoundCue[1] != nullptr)
				{
					UGameplayStatics::SpawnSoundAttached(
						HitSoundCue[1],
						GetRootComponent(),
						NAME_None,
						FVector::ZeroVector,
						EAttachLocation::KeepRelativeOffset,
						false,
						2.5f,  // Volume multiplier
						0.7f   // Pitch multiplier, 0.5로 설정하면 재생 속도가 절반으로 느려짐
					);
				}
				// 데미지 정보 전달
				FDamageEvent DamageEvent;
				HitResult.GetActor()->TakeDamage(SkillDamage, DamageEvent, GetController(), this);
			}
		}
	}
	// 공격판정
	else
	{
		GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Green, TEXT("AttackCheck~~~~~~~~~~~~~~~~~~"));
		FHitResult HitResult;
		FCollisionQueryParams Params(NAME_None, false, this);

		// 구체의 중심을 캐릭터 발 앞에 설정
		FVector StartLocation = GetActorLocation() + FVector(0.0f, 0.0f, -GetCapsuleComponent()->GetScaledCapsuleHalfHeight());
		FVector ForwardLocation = StartLocation + GetActorForwardVector() * AttackRange;

		bool bResult = GetWorld()->SweepSingleByChannel(
			HitResult, // 물리적 충돌이 탐지된 경우 관련 정보를 담을 구조체
			StartLocation, // 탐색 시작 위치
			ForwardLocation,
			FQuat::Identity,
			ECollisionChannel::ECC_GameTraceChannel2, // 물리 충돌 감지에 사용할 트레이스 채널
			FCollisionShape::MakeSphere(AttackRadius), // 탐색 사용할  도형
			Params);

		// 디버그 출력 정보를 담은 변수
#if ENABLE_DRAW_DEBUG
		FVector TraceVec = GetActorForwardVector() * AttackRange;
		FVector Center = StartLocation + TraceVec * 0.5f;  // 구체의 중심을 설정
		FQuat CapsuleRot = FRotationMatrix::MakeFromZ(TraceVec).ToQuat();
		FColor DrawColor = bResult ? FColor::Green : FColor::Red;
		float DebugLifeTime = 1.0f;

		// 디버그 출력
		DrawDebugSphere(GetWorld(),
			ForwardLocation,
			AttackRadius,
			12, // 세그먼트 수, 더 많을수록 부드러워짐
			DrawColor,
			false,
			DebugLifeTime);

#endif
		// 액터 감지시
		if (bResult)
		{
			if (HitResult.GetActor() != nullptr)
			{
				// 히트 사운드 재생
				if (HitSoundCue[0] != nullptr)
				{
					UGameplayStatics::SpawnSoundAttached(
						HitSoundCue[0],
						GetRootComponent(),
						NAME_None,
						FVector::ZeroVector,
						EAttachLocation::KeepRelativeOffset,
						false,
						2.5f,  // Volume multiplier
						0.7f   // Pitch multiplier, 0.5로 설정하면 재생 속도가 절반으로 느려짐
					);
				}
				// 데미지 정보 전달
				FDamageEvent DamageEvent;
				HitResult.GetActor()->TakeDamage(AttackDamage, DamageEvent, GetController(), this);
			}
		}
		else
		{
			if (MissSoundCue[0] != nullptr)
			{
				UGameplayStatics::SpawnSoundAttached(
					MissSoundCue[0],
					GetRootComponent(),
					NAME_None,
					FVector::ZeroVector,
					EAttachLocation::KeepRelativeOffset,
					false,
					2.5f,  // Volume multiplier
					0.7f   // Pitch multiplier, 0.5로 설정하면 재생 속도가 절반으로 느려짐
				);
			}
		}
	}


}




// 데미지 받는 함수
float AMyCharacter::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
	if (Death)
	{
		return Super::TakeDamage(DamageAmount, DamageEvent, EventInstigator, DamageCauser);
	}


	// 입은 데미지 만큼 hp 차감
	UpdateHP(-DamageAmount);


	// hp가 0이 되었을경우 사망 함수 호출
	if (this->hp <= 0)
	{
		this->hp = 0;
		IsDeath();
	}

	return Super::TakeDamage(DamageAmount, DamageEvent, EventInstigator, DamageCauser);
}

 

전투 시작 애니메이션이 아직 부자연스러워서 추후 수정해 나갈 예정입니다.