다양한 기록

[UE Game Framework] #6 Character Attack Hit Check 본문

언리얼 엔진/Unreal Game Framework

[UE Game Framework] #6 Character Attack Hit Check

라구넹 2025. 1. 5. 02:01

캐릭터 액션의 충돌 판정

- 월드가 제공하는 충돌 판정 서비스를 사용

- 월드는 크게 세 가지의 충돌 판정 서비스를 제공함

    - LineTrace : 지정한 방향으로 선을 투사

    - Sweep : 지정한 방향으로 도형을 투사

    - Overlap : 지정한 영역에 큰 범위의 도형을 설정해서 해당 볼륨 영역과 물체가 충돌하는지 검사 

- 월드 내 배치된 충돌체와 충돌하는지 파악하고 충돌한 액터 정보를 얻을 수 있음

 

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

- 액션 판정을 위한 트레이스 채널의 생성 : ABAction, 기본 반응은 무시

- 캐릭터 캡슐용 프로필 : ABAction 트레이스 채널에 반응, 오브젝트 타입은 Pawn

- 스켈레탈 메시용 프로필 : 랙돌 구현을 위해 주로 활용됨

- 기믹 트리거용 프로필 : 폰 캡슐에만 반응하도록 설정, 오브젝트 타입은 WorldStatic(안움직임)

블록은 모든 물체가 반응함

 

지정한 캡슐 컴포넌트만 반응하게 할 것이라 변경 필요

여기서 이제 필요한 거 추가

 

Trace 타입은 이그노어, 블록만 신경쓰면 되고

Object 타입은 이그노어, 오버랩, 블록 세가지를 신경 써야 함

오버랩은 길은 안막는데 이벤트가 발생, 블록은 길막기

ex. 파괴한 조각들이 길을 막으면 안되니 Ignore

 

캡슐용 프로필이니 쿼리용으로, 폰타입으로 설정하고

ABAction에 블록되도록 함

 

트리거도 만들어주는데, 폰만 겹치는 반응 걸어주면 됨

 

이런 정보들은 DefaultEngine.ini에 다 저장됨

 

열어보면 확인 가능

 

중요한 정보

DefaultChannelResponses라는 정보의 ABAction이라는 이름이

엔진에서 지정한 내부 열거형에서 ECC_GameTraceChannel1라는 값을 사용

코드 내에서는 저 값을 써주는게 더 편리

그 위엔 추가한 프로필들 확인 가능


몽타주의 애니메이션에서 이벤트를 발생시켜서 공격 판정을 구현할 것

여기서 공격 판정을 주고 싶음 -> 애니메이션 노티파이 활용

 

선택하고 만들어줌 (이름 AnimNotify_AttackHitCheck)

이제 만들어주기 가능

각 섹션마다 설정해줌

 

이제 UAnimNotify의 함수를 오버라이드해서 구현해야 함

위 Notify는 언리얼 5부터 안씀

 

Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, const FAnimNotifyEventReference& EventReference)

스켈레탈 메쉬 컴포넌트, 애니메이션 정보가 인자

그리고 추가적인 레퍼런스 정보들 ..

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


#include "Animation/AnimNotify_AttackHitCheck.h"
#include "Character/ABCharacterBase.h"

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

	}
}

오너가 우리가 선언한 캐릭터인지 체크해야 함 -> 다른 헤더를 인클루드 해야 함

=> 의존성 증가

노티파이는 공용으로 쓰는 일이 많은데, 이러면 별로 안좋음

=> 인터페이스 활용

 

만들어주기

어쩔 수 없는 의존성 발생 시 가급적이면 인터페이스를 통해 해결

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


#include "Animation/AnimNotify_AttackHitCheck.h"
#include "Interface/ABAnimationAttackInterface.h"

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

	}
}

이렇게 바꿔줄 수 있음

 

인터페이스에서 함수 하나 추가

 

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


#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 )
	{
		IABAnimationAttackInterface* AttackPawn = Cast<IABAnimationAttackInterface>(MeshComp->GetOwner());

		if ( AttackPawn )
		{
			AttackPawn->AttackHitCheck();
		}
	}
}

결과적으로는 이렇게 바뀜

이제 AttackHitCheck 구현하면 됨

 

공격 판정의 구현

월트 트레이싱 함수의 선택

- 세가지 카테고리로 원하는 함수 이름을 얻을 수 있음

- 카테고리 1 : 처리 방법
    - LineTrace

    - Sweep

    - Overlap

- 카테고리 2 : 대상

    - Test : 무언가 감지되었는지를 테스트

    - Single or AnyTest : 감지된 단일 물체 정보를 반환 (LineTrace, Sweep -> Single, Overlap -> AnyTest)

    - Multi : 감지된 모든 물체 정보를 배열로 반환

- 카테고리 3: 처리 설정

    - ByChannel : (트레이스) 채널 정보를 사용해 감지

    - ByObjectType : 물체에 지정된 물리 타입 정보를 사용해 감지

    - ByProfile : (물리에 설정된)프로필 정보를 사용해 감지

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

 

캐릭터 공격 판정의 구현

- 캐릭터의 위치에서 시선 방향으로 물체가 있는지 감지

- 작은 구체를 제작하고 시선 방향으로 특정 거리까지만 투사

- 하나의 물체만 감지

- 트레이스 채널을 사용해 감지

=> SweepSingleByChannel

 

트레이스 채널을 써야 하는데, 간단한 헤더 파일에서 매크로 만들어 쓰면 편함

(Source/Physics/ABCollision.h) .. 

파일 탐색기에서 만들어서 그런가 재생성해도 빨간줄이 뜨긴 하는데,

일단 빌드는 됨

프로필 두개랑 트레이스 채널

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)
	{

	}
}

Params

첫번째 인자: InTraceTag, 콜리전 분석 시 식별자

두번째 인자: bInTraceComplex : 복잡한 형태의 충돌체도 감지할지에 대한 옵션

세번째 인자: 무시할 액터들, 자기 자신만 무시하면 됨

 

물리 충돌 테스트

- 디버그 드로잉 함수를 사용해 물리 충돌을 시각적으로 테스트

- 90도로 회전시킨 캡슐을 그리기

    - Origin

    - HalfHeight

    - Radius

 

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)
	{

	}

#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
}

** false랑 5.0f는 계속 유지할지 말지와, 유지 안할 거면 얼마나 유지할지

캐릭터랑만 충돌 처리해놔서 뭘 해도 빨간색만 나옴

=> NPC 생성해서 테스트

// 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 "Animation/AnimMontage.h"
#include "ABComboActionData.h"
#include "Physics/ABCollision.h"

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

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

	// Movement
	GetCharacterMovement()->bOrientRotationToMovement = true;
	GetCharacterMovement()->RotationRate = FRotator(0.0f, 500.0f, 0.0f);
	GetCharacterMovement()->JumpZVelocity = 700.0f;
	GetCharacterMovement()->AirControl = 0.35f;
	GetCharacterMovement()->MaxWalkSpeed = 500.0f;
	GetCharacterMovement()->MinAnalogWalkSpeed = 20.0f;
	GetCharacterMovement()->BrakingDecelerationWalking = 2000.0f;

	// 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/Animation/ABP_ABCharacter.ABP_ABCharacter_C"));
	if (AnimInstanceClassRef.Class)
	{
		GetMesh()->SetAnimInstanceClass(AnimInstanceClassRef.Class);
	}

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

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

	static ConstructorHelpers::FObjectFinder<UAnimMontage> ComboActionMontageRef(TEXT("/Script/Engine.AnimMontage'/Game/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;
	}
}

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 ( !ComboTimerHande.IsValid() )	// 타이밍을 놓쳤거나, 더 이상 진행 못하거나 
	{
		HasNextComboCommand = false;
	}
	else
	{
		HasNextComboCommand = true;
	}
}

void AABCharacterBase::ComboActionBegin()
{
	// Combo Started
	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);

	ComboTimerHande.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(
			ComboTimerHande, this, &AABCharacterBase::ComboCheck, ComboEffectiveTime, false);
	}
}

void AABCharacterBase::ComboCheck()
{
	ComboTimerHande.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)
	{

	}

#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
}

베이스에서 몽타주랑 데이터 애셋 기본값 주고

캡슐 컴포넌트랑 메쉬 프로필 이름 설정하고 실행

NPC는 인식해서 초록색으로 나옴

다음은 죽는 기능

(* 여기서부터 코드가 좀 달라진게 있음, 폴더 잘못 만든게 있어서 수정함)

 

데드 몽타주 생성

애니메이션 블렌드 안되고 바로 실행되도록 오토 블렌드 해제

몽타주는 그룹별로 관리할 수 있는데 이게 슬롯임 (그룹이 상위, 슬롯이 하위)

기본은 디폴트 슬롯

데드슬롯 추가

데드슬롯으로 설정

애니메이션 블루프린트도 변경

이제 공격을 받으면 바로 죽도록 할 것임

 

언리얼 엔진 액터 설정에 TakeDamage 가 존재

리턴값은 최종으로 액터가 받은 대미지의 양을 의미

인스티게이터는 피해를 입힌 가해자

커저는 가해자가 사용한 무기, 아니면 빙의한 폰, 액터 정보

 

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);
}

공격을 받으면 움직임 멈추고, 충돌 판정 전부 제거

그리고 실행 중이던 몽타주 있으면 전부 정지시키고 데드 몽타주 실행

한 대라도 맞으면 바로 SetDead 실행됨

 

대미지 입히는 건 이렇게 하면 됨

 

죽음

그런데 NPC는 일정 시간 지나면 사라지는게 나음

NPC에만 추가 구현

 

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

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

타이머 쓰면 되는데, SetTimer에서 멤버 함수 만들어서 부착하긴 번거로우니

람다 함수 사용

 

사라지는 것 확인