futurelabunseen / B-JeonganLee

UNSEEN 2nd Term Learning and Project Repo.
5 stars 0 forks source link

6강: 캐릭터 공격 판정 #48

Closed fkdl0048 closed 6 months ago

fkdl0048 commented 6 months ago

6강: 캐릭터 공격 판정

충돌 채널의 설정

캐릭터 액션의 충돌 판정

Unity의 Collider, Raycast와 유사한 기능을 제공한다.

트레이스 채널과 충돌 프로필 생성

실습

#pragma once

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

UCLASS()
class ARENABATTLE_API UAnimNotify_AttackHitCheck : public UAnimNotify
{
 GENERATED_BODY()

protected:
 virtual void Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, const FAnimNotifyEventReference& EventReference) override;
};
#include "Animation/AnimNotify_AttackHitCheck.h"
#include "Interface/ABAnimationAttackInterface.h"

void UAnimNotify_AttackHitCheck::Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, const FAnimNotifyEventReference& EventReference)
{
 Super::Notify(MeshComp, Animation, EventReference);

 if (MeshComp)
 {
  // 언리얼의 특징? 얽힌 상속 구조를 사용하여 Notify될 때 MeshComp의 Owner를 찾아서 IABAnimationAttackInterface를 구현한 객체를 찾아서 AttackHitCheck를 호출한다.
  IABAnimationAttackInterface* AttackPawn = Cast<IABAnimationAttackInterface>(MeshComp->GetOwner());

  if (AttackPawn)
  {
   AttackPawn->AttackHitCheck();
  }
 }
}
#pragma once

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

UINTERFACE(MinimalAPI)
class UABAnimationAttackInterface : public UInterface
{
 GENERATED_BODY()
};

class ARENABATTLE_API IABAnimationAttackInterface
{
 GENERATED_BODY()

public:
 virtual void AttackHitCheck() = 0;
};

공격 판정의 구현

월드 트레이싱 함수의 선택

크게 세 가지 카테고리로 원하는 함수 이름을 얻을 수 있다.

{처리방법}{대상}{처리설정}

캐릭터 공격 판정의 구현

{Sweep}{Single}{ByChannel}

물리 충돌 테스트

디버그 드로잉을 이용한 물리 충돌을 시각적으로 테스트한다.

실습

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

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "Interface/ABAnimationAttackInterface.h"
#include "ABCharacterBase.generated.h"

UENUM()
enum class ECharacterControlType : uint8
{
 Shoulder,
 Quater
};

UCLASS()
class ARENABATTLE_API AABCharacterBase : public ACharacter, public IABAnimationAttackInterface
{
 GENERATED_BODY()

public:
 // Sets default values for this character's properties
 AABCharacterBase();

protected:
 virtual void SetCharacterControlData(const class UABCharacterControlData* CharacterControlData);

 UPROPERTY(EditAnywhere, Category = CharacterControl, Meta = (AllowPrivateAccess = "true"))
 TMap<ECharacterControlType, class UABCharacterControlData*> CharacterControlManager;

 // Combo Action Section
protected:

 // 둘다 블루프린트에서 설정할 수 있도록 열어둔 변수
 // 위는 몽타주, 아래는 데이터 에셋이다.
 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Animation)
 TObjectPtr<class UAnimMontage> ComboActionMontage;

 UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Attack, Meta = (AllowPrivateAccess = "true"))
 TObjectPtr<class UABComboActionData> ComboActionData;

 // 키에 바인딩 될 메서드
 void ProcessComboCommand();

 // 시작 그리고 종료에 각각 호출되는 메서드
 void ComboActionBegin();
 void ComboActionEnd(class UAnimMontage* TargetMontage, bool IsProperlyEnded);

 // 타이머 발동 메소드와 입력이 들어왔는지 체크하는 함수
 void SetComboCheckTimer();
 void ComboCheck();

 // 콤보 Flag 변수
 int32 CurrentCombo = 0;
 // 언리얼 엔진 월드에서 제공하는 특정 타이머 기능을 사용할 수 있는 구조체
 FTimerHandle ComboTimerHandle; 
 bool HasNextComboCommand = false;

 // AttackHit Section
protected:
 virtual void AttackHitCheck() override;
 virtual float TakeDamage(float DamageAmount, struct FDamageEvent const& DamageEvent, class AController* EventInstigator, AActor* DamageCauser) override;

 // Dead Section
protected:
 UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Stat, Meta = (AllowPrivateAccess = "true"))
 TObjectPtr<class UAnimMontage> DeadMontage;

 virtual void SetDead();
 void PlayDeadAnimation();

 float DeadEventDelayTime = 5.0f;
};
// Fill out your copyright notice in the Description page of Project Settings.

#include "Character/ABCharacterBase.h"
#include "Components/CapsuleComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "ABCharacterControlData.h"
#include "ABComboActionData.h"
#include "Engine/DamageEvents.h"
#include "Physics/ABCollision.h"

// Sets default values
AABCharacterBase::AABCharacterBase()
{
 // Pawn
 bUseControllerRotationPitch = false;
 bUseControllerRotationYaw = false;
 bUseControllerRotationRoll = false;

 // Capsule
 GetCapsuleComponent()->InitCapsuleSize(42.f, 96.0f);
 GetCapsuleComponent()->SetCollisionProfileName(CPROFILE_ABCAPSULE);

 // Movement
 GetCharacterMovement()->bOrientRotationToMovement = true;
 GetCharacterMovement()->RotationRate = FRotator(0.0f, 500.0f, 0.0f);
 GetCharacterMovement()->JumpZVelocity = 700.f;
 GetCharacterMovement()->AirControl = 0.35f;
 GetCharacterMovement()->MaxWalkSpeed = 500.f;
 GetCharacterMovement()->MinAnalogWalkSpeed = 20.f;
 GetCharacterMovement()->BrakingDecelerationWalking = 2000.f;

 // Mesh
 GetMesh()->SetRelativeLocationAndRotation(FVector(0.0f, 0.0f, -100.0f), FRotator(0.0f, -90.0f, 0.0f));
 GetMesh()->SetAnimationMode(EAnimationMode::AnimationBlueprint);
 GetMesh()->SetCollisionProfileName(TEXT("NoCollision"));

 static ConstructorHelpers::FObjectFinder<USkeletalMesh> CharacterMeshRef(TEXT("/Script/Engine.SkeletalMesh'/Game/InfinityBladeWarriors/Character/CompleteCharacters/SK_CharM_Cardboard.SK_CharM_Cardboard'"));
 if (CharacterMeshRef.Object)
 {
  GetMesh()->SetSkeletalMesh(CharacterMeshRef.Object);
 }

 static ConstructorHelpers::FClassFinder<UAnimInstance> AnimInstanceClassRef(TEXT("/Game/ArenaBattle/Animation/ABP_ABCharacter.ABP_ABCharacter_C"));
 if (AnimInstanceClassRef.Class)
 {
  GetMesh()->SetAnimInstanceClass(AnimInstanceClassRef.Class);
 }

 static ConstructorHelpers::FObjectFinder<UABCharacterControlData> ShoulderDataRef(TEXT("/Script/ArenaBattle.ABCharacterControlData'/Game/ArenaBattle/CharacterControl/ABC_Shoulder.ABC_Shoulder'"));
 if (ShoulderDataRef.Object)
 {
  CharacterControlManager.Add(ECharacterControlType::Shoulder, ShoulderDataRef.Object);
 }

 static ConstructorHelpers::FObjectFinder<UABCharacterControlData> QuaterDataRef(TEXT("/Script/ArenaBattle.ABCharacterControlData'/Game/ArenaBattle/CharacterControl/ABC_Quater.ABC_Quater'"));
 if (QuaterDataRef.Object)
 {
  CharacterControlManager.Add(ECharacterControlType::Quater, QuaterDataRef.Object);
 }

 static ConstructorHelpers::FObjectFinder<UAnimMontage> ComboActionMontageRef(TEXT("/Script/Engine.AnimMontage'/Game/ArenaBattle/Animation/AM_ComboAttack.AM_ComboAttack'"));
 if (ComboActionMontageRef.Object)
 {
  ComboActionMontage = ComboActionMontageRef.Object;
 }

 static ConstructorHelpers::FObjectFinder<UABComboActionData> ComboActionDataRef(TEXT("/Script/ArenaBattle.ABComboActionData'/Game/ArenaBattle/CharacterAction/ABA_ComboAttack.ABA_ComboAttack'"));
 if (ComboActionDataRef.Object)
 {
  ComboActionData = ComboActionDataRef.Object;
 }

 static ConstructorHelpers::FObjectFinder<UAnimMontage> DeadMontageRef(TEXT("/Script/Engine.AnimMontage'/Game/ArenaBattle/Animation/AM_Dead.AM_Dead'"));
 if (DeadMontageRef.Object)
 {
  DeadMontage = DeadMontageRef.Object;
 }
}

void AABCharacterBase::SetCharacterControlData(const UABCharacterControlData* CharacterControlData)
{
 // Pawn
 bUseControllerRotationYaw = CharacterControlData->bUseControllerRotationYaw;

 // CharacterMovement
 GetCharacterMovement()->bOrientRotationToMovement = CharacterControlData->bOrientRotationToMovement;
 GetCharacterMovement()->bUseControllerDesiredRotation = CharacterControlData->bUseControllerDesiredRotation;
 GetCharacterMovement()->RotationRate = CharacterControlData->RotationRate;
}

void AABCharacterBase::ProcessComboCommand()
{
 if (CurrentCombo == 0)
 {
  ComboActionBegin();
  return;
 }

 if (!ComboTimerHandle.IsValid())
 {
  HasNextComboCommand = false;
 }
 else
 {
  HasNextComboCommand = true;
 }
}

void AABCharacterBase::ComboActionBegin()
{
 // Combo Start
 CurrentCombo = 1;

 // Movement Setting
 // 캐릭터의 움직임을 제한한다.
 GetCharacterMovement()->SetMovementMode(EMovementMode::MOVE_None);

 // Animation Setting
 const float AttackSpeedRate = 1.0f;
 UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
 AnimInstance->Montage_Play(ComboActionMontage, AttackSpeedRate);

 FOnMontageEnded EndDelegate;
 EndDelegate.BindUObject(this, &AABCharacterBase::ComboActionEnd);
 AnimInstance->Montage_SetEndDelegate(EndDelegate, ComboActionMontage);

 ComboTimerHandle.Invalidate();
 SetComboCheckTimer();
}

void AABCharacterBase::ComboActionEnd(UAnimMontage* TargetMontage, bool IsProperlyEnded)
{
 ensure(CurrentCombo != 0);
 CurrentCombo = 0;
 // 콤보가 끝났으므로 움직임을 다시 허용한다.
 GetCharacterMovement()->SetMovementMode(EMovementMode::MOVE_Walking);
}

void AABCharacterBase::SetComboCheckTimer()
{
 int32 ComboIndex = CurrentCombo - 1;
 ensure(ComboActionData->EffectiveFrameCount.IsValidIndex(ComboIndex));

 const float AttackSpeedRate = 1.0f;
 float ComboEffectiveTime = (ComboActionData->EffectiveFrameCount[ComboIndex] / ComboActionData->FrameRate) / AttackSpeedRate;
 if (ComboEffectiveTime > 0.0f)
 {
  GetWorld()->GetTimerManager().SetTimer(ComboTimerHandle, this, &AABCharacterBase::ComboCheck, ComboEffectiveTime, false);
 }
}

void AABCharacterBase::ComboCheck()
{
 ComboTimerHandle.Invalidate();
 if (HasNextComboCommand)
 {
  UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();

  CurrentCombo = FMath::Clamp(CurrentCombo + 1, 1, ComboActionData->MaxComboCount);
  FName NextSection = *FString::Printf(TEXT("%s%d"), *ComboActionData->MontageSectionNamePrefix, CurrentCombo);
  AnimInstance->Montage_JumpToSection(NextSection, ComboActionMontage);
  SetComboCheckTimer();
  HasNextComboCommand = false;
 }
}

void AABCharacterBase::AttackHitCheck()
{
 FHitResult OutHitResult;
 FCollisionQueryParams Params(SCENE_QUERY_STAT(Attack), false, this);

 const float AttackRange = 40.0f;
 const float AttackRadius = 50.0f;
 const float AttackDamage = 30.0f;
 const FVector Start = GetActorLocation() + GetActorForwardVector() * GetCapsuleComponent()->GetScaledCapsuleRadius();
 const FVector End = Start + GetActorForwardVector() * AttackRange;

 bool HitDetected = GetWorld()->SweepSingleByChannel(OutHitResult, Start, End, FQuat::Identity, CCHANNEL_ABACTION, FCollisionShape::MakeSphere(AttackRadius), Params);
 if (HitDetected)
 {
  FDamageEvent DamageEvent;
  OutHitResult.GetActor()->TakeDamage(AttackDamage, DamageEvent, GetController(), this);
 }

#if ENABLE_DRAW_DEBUG

 FVector CapsuleOrigin = Start + (End - Start) * 0.5f;
 float CapsuleHalfHeight = AttackRange * 0.5f;
 FColor DrawColor = HitDetected ? FColor::Green : FColor::Red;

 DrawDebugCapsule(GetWorld(), CapsuleOrigin, CapsuleHalfHeight, AttackRadius, FRotationMatrix::MakeFromZ(GetActorForwardVector()).ToQuat(), DrawColor, false, 5.0f);
#endif
}

float AABCharacterBase::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
 Super::TakeDamage(DamageAmount, DamageEvent, EventInstigator, DamageCauser);

 SetDead();

 return DamageAmount;
}

void AABCharacterBase::SetDead()
{
 GetCharacterMovement()->SetMovementMode(EMovementMode::MOVE_None);
 PlayDeadAnimation();
 SetActorEnableCollision(false);
}

void AABCharacterBase::PlayDeadAnimation()
{
 UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
 AnimInstance->StopAllMontages(0.0f);
 AnimInstance->Montage_Play(DeadMontage, 1.0f);
}
// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Character/ABCharacterBase.h"
#include "ABCharacterNonPlayer.generated.h"

/**
 * 
 */
UCLASS()
class ARENABATTLE_API AABCharacterNonPlayer : public AABCharacterBase
{
 GENERATED_BODY()

public:
 AABCharacterNonPlayer();

protected:
 void SetDead() override;
};
#include "ABCharacterNonPlayer.h"

AABCharacterNonPlayer::AABCharacterNonPlayer()
{
}

void AABCharacterNonPlayer::SetDead()
{
 Super::SetDead();

 FTimerHandle DeadTimerHandle;
 GetWorld()->GetTimerManager().SetTimer(DeadTimerHandle, FTimerDelegate::CreateLambda(
  [&]()
  {
   Destroy();
  }
 ), DeadEventDelayTime, false);
}

정리