2-6강 캐릭터 공격 판정

Ryan Ham·2024년 7월 1일
0

이득우 Unreal

목록 보기
9/23
post-thumbnail

오늘 배울것

  1. 공격판정 구현을 위한 물리 트레이스 채널 및 프로필 설정
  2. 디버그 드로잉 기능을 활용한 충돌 디버깅
  3. 데미지 프레임웍을 사용한 데미지 전달
  4. Delegate와 Lambda 함수의 사용

Unreal World가 제공하는 3가지 충돌 판정 서비스

LineTrace, Sweep, Overlap 3가지 방법으로 World 내 배치된 충돌체와 충돌하는지 파악하고, 충돌한 Actor 정보를 얻을 수 있다.

  • LineTrace는 지정한 방향으로 선을 투사, 이와 충돌되는 물체 있는지 확인.
  • Sweep은 선이 아니고 어떤 도형을 지정한 방향으로 투사. LineTrace와 유사하다.
  • Overlap은 투사 X. 지정한 영역에 큰 범위의 도형을 설정해서 해당 volume 영역과 물체가 충돌되는지를 검사.

Trace channel과 Collision profile 생성

Trace Channel은 총 Ignore, Block, Overlap이 가능한데, 우리는 일단 Ignore로 설정한다.

이번 예제에서는 총 2개의 Collision Profile을 생성할 것. Capsule Component용 하나랑 Trigger용 하나. Character Mesh에도 collision setting을 할 수 있지만 이는 RagDoll 같은 기능을 구현할 때 사용하는 것이기 때문에 지금은 패스.

Capsule Component의 오브젝트 타입은 Pawn, Trigger의 오브젝트 타입은 WorldStatic으로 설정한다.

Collision Setting에서는 2가지 부분을 봐야 하는데 하나는 Trace Channel이고 다른 하나는 Collision Profile이다. Collision Profile은 각각의 오브젝트가 어떤 Collision 규칙을 따를 것인지 설정하는 부분이다. 예를 들어서 A라는 물체는 B라는 물체와는 물리 충돌(블록)을 하지만 C라는 물체와는 자연스럽게 통과(무시 혹은 오버랩)을 하고 싶은 상황이 있기 때문이다. Collision Profile에는 다른 오브젝트 타입과의 상호작용 뿐만 아니라, 다양한 Trace Channel들에 대해서도 어떻게 상호작용 할지 설정하는 부분이 있다.

Trace Channel은 Line Trace로 collision을 검사하는데, 캐릭터의 시야 검사, 총알 발사 경로 검사 같이 직선 형태로 충돌 검사가 유리할때 많이 사용된다. Collision Profile 편집창에서 기본적인 Trace 타입에 카메라가 block으로 설정되어 있는데, 이는 카메라가 캐릭터를 비출때 해당 물체와 카메라 사이에 물체가 있게 된다면 카메라의 LineTrace를 통해서 이를 판단하고 카메라를 그 물체보다 더 앞으로 땡겨서 성공적으로 캐릭터를 비춘다.

이와 같이 Collision Setting을 작성하면, 이 결과들은 Config->DefaultEngine.ini에 다 기록이 된다.


AnimNotify

"AttackHitCheck"라는 AnimNotify 클래스를 하나 만들어보자. AnimNotify 같은 경우는 클래스 이름을 정하면 자동으로 접두사에 "AnimNotify"가 붙는다. 따라서 최종 이름은 AnimNotify_AttackHitCheck가 된다.

이렇게 Custom Notify를 만들었으면 이를 Animation Montage가 추가해보자. 각 Section마다 캐릭터의 공격하는 순간들을 보면서 알맞은 타이밍에 AttackHitCheck Notify를 배치한다(총 4개).

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

	if (MeshComp)
	{
		IRyanAnimationAttackInterface* AttackPawn = Cast<IRyanAnimationAttackInterface>(MeshComp->GetOwner());
		if (AttackPawn)
		{
			AttackPawn->AttackHitCheck();
		}
	}
}

Custom Notify를 사용하기 위해서 Notify라는 함수를 꼭 override해서 사용해주어야 한다. Notify의 인자로는 MeshComp가 들어오는데, MeshComp->GetOwner를 통해 Notify를 실행한 주체를 알 수 있다. 최종적으로 이 주체의 AttackHitCheck()라는 멤버 함수를 부르게 되는데 이 함수의 정체는 밑에서 알아보자. 지금은 Animation Montage의 특정한 순간에 자동으로 Notify가 실행된다는 사실만 일단 기억하자!


Interface란?

Interface는 다양한 클래스들이 공통으로 구현해야 하는 메서드들을 정의하는 일종의 계약이다. Interface는 다른 클래스와 독립적으로 정의되며, 이를 구현하는 클래스는 Interface에서 정의된 메서드를 반드시 구현해야 한다. 이를 통해 서로 다른 클래스들이 동일한 방식으로 상호작용할 수 있도록 보장하며, 코드의 유연성과 재사용성을 높이는 데 기여한다.

언리얼 엔진에서 Interface는 UInterface 클래스를 상속받아 정의되며, 클래스 간의 결합도를 낮추고 다형성을 활용하는 중요한 도구이다.

Interface의 핵심적 기능 2개

  • 순수가상함수를 통해 Interface를 상속하는 클래스들이 무조건 구현해야 할 함수를 강제할 수 있다.
  • 헤더에 특정 파일을 추가하는 것이 아닌 Interface를 추가함으로서 의존성 분리를 할 수 있다(훨씬 확장적인 프로그램 설계 가능).

World Tracing 함수의 선택

Category 1 : 처리 방법

  • LineTrace, Sweep, OverLap

Category 2 : 대상

  • Test : 무언가 감지되었는지를 테스트
  • Single 또는 AnyTest : 감지된 단일 물체 정보를 반환
  • Multi : 감지된 모든 물체 정보를 배열로 반환

Category 3 : 처리 설정

  • ByChannel : 채널 정보를 사용해 감지
  • ByObjectType : 물체에 지정된 물리 타입 정보를 사용해 감지
  • ByProfile : 프로필 정보를 사용해 감지

이 3가지 Catrgory를 차례로 붙여 원하는 함수 이름을 얻을 수 있다.
최종적인 함수 이름(처리 방법) + (대상) + (처리 설정)으로 결정된다.

우리는 목적상 SweepSingleByChannel을 사용할 것.


AttackHitCheck 함수에서 충돌 감지 부분

void ARyanCharacterBase::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;

	// 우리는 구체를 만들어서 이를 sweep할 것이다. Start지점과 End지점을 만들어서 이를
	// "SweepSingleByChannel"에 인자로 넘겨주자. 
	const FVector Start = GetActorLocation() + GetActorForwardVector() * GetCapsuleComponent()->GetScaledCapsuleRadius();
	const FVector End = Start + GetActorForwardVector() * AttackRange;

	bool HitDetected = GetWorld()->SweepSingleByChannel(OutHitResult, Start, End, FQuat::Identity, ECC_GameTraceChannel1, FCollisionShape::MakeSphere(AttackRadius), Params);

	// HitDetected가 감지 -> sweep 했는데 무언가가 맞음.
	if (HitDetected)
	{
		FDamageEvent DamageEvent;
		OutHitResult.GetActor()->TakeDamage(AttackDamage, DamageEvent, GetController(), this);
	}

// Collision이 일어나는 Debug Sphere 그리기
#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

}

우리가 SweepSingleByChannel로 collision이 일어났는지 안 일어났는지를 조사하고 싶다면 우선 해당 과정에 필요한 재료들을 준비하여야 한다. 우리는 Sweep을 구로 할 것이기 때문에 FCollisionShape::MakeSphere를 인자로 넣어주고 구의 반지름 또한 설정해 준다. Sweep이 일어나는 start 지점은 캐릭터의 현재 위치에 캐릭터가 속해 있는 capsule component 값을 더해서 구해주고, Sweep의 end지점은 start 지점에 AttackRange 값을 더해 준비해 준다.


AttackHitCheck 함수에서의 디버그 드로잉 기능

언리얼 엔진에서 제공하는 DebugDrawing 기능을 활용해서 충돌이 일어났는지를 시각적으로 봐 보자. SweepSingleByChannel의 결과는 FHitResult라는 구조체에 들어오게 되는데, 만약 이 값이 있다면 DebugSphere의 색깔을 초록색, 없다면 빨간색으로 칠한다.

void ARyanCharacterBase::AttackHitCheck()
{
...
#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
}

sweep을 하기 위해서 구체를 시작시점서부터 끝지점까지 한번 훑는 것이기 때문에 capsule component를 90도 회전해서 sweep 영역의 모양을 잡아 준다. 이 회전시키는 RotationMatrix에 대해서는 추후에 더 깊이 알아보자.


NPC 캐릭터 만들기

TraceChannel에서는 Character에 대해서 반응하도록 만들었기 때문에
CharacterBase를 상속해서 NPC Character 클래스를 만들어보자.

ARyanNPCCharacter::ARyanNPCCharacter()
{
}

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

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

}

NPC 캐릭터가 죽으면 타이머를 설정하고 없에는 코드이다. Delegate와 Lambda를 사용해서 tight하게 코드를 작성.


죽는 Animation Montage

Combo Montage와 별개로 죽는 Montage를 따로 만들자. 이 Montage에서는 때리면 바로 죽는 모션을 재생하기 위해 "자동 블랜드 아웃 활성화" 기능을 체크 해제한다.

Montage에서는 Group이 상위 개념이고 Slot이 하위 개념이다. Combo에 관한 Montage는 'DefaultSlot'라 작명을 하고, 죽는 모션에 대한 Montage는 'DeadSlot'라 이름을 붙여서 위 그림과 같이 AnimGraph에 배치한다. DeadSlot이 DefaultSlot보다 뒤에 있으므로 더 우선 순위를 가진다.


데미지 구현

언리얼 엔진에서 미리 만들어 놓은, Actor 레밸에서 구현된 TakeDamage라는 함수가 있다. 이를 override해서 사용해보자.
return은 최종적으로 해당 Actor가 받은 데미지 양을 의미한다. Instigator는 나에게 데미지를 입힌 주체를 의미하고, Causer는 가해자가 빙의한 폰 혹은 들고 있는 무기 정보이다.

만약 이 TakeDamage라는 함수를 커스터마이징 한다면, 방어력을 구현해 들어온 데미지를 반감시켜서 리턴하도록 만들어 볼 수도 있다.

	if (HitDetected)
	{
		FDamageEvent DamageEvent;
		OutHitResult.GetActor()->TakeDamage(AttackDamage, DamageEvent, GetController(), this);
	}

다시 Recap 하자면, 앞서 SweepSingleByChannel을 통해 Sweep으로 인한 Collision이 발생했으면 HitResult에 충돌 결과가 담기게 된다. 그러면 이때, 위의 코드처럼 이 TakeDamage 함수를 실행시킨다.

TakeDamage와 죽는 Montage 실행 코드

// CharacterBase.cpp
// Actor 레벨에서 이미 구현된 TakeDamage라는 함수를 override해서 로직추가

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

	SetDead();

	return DamageAmount;
}

void ARyanCharacterBase::SetDead()
{
	GetCharacterMovement()->SetMovementMode(EMovementMode::MOVE_None);
	PlayDeadAnimation();
    // 죽었으면 캐릭터의 모든 collision 기능을 꺼준다.
	SetActorEnableCollision(false);
}

void ARyanCharacterBase::PlayDeadAnimation()
{
	UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
	AnimInstance->StopAllMontages(0.0f);
	AnimInstance->Montage_Play(DeadMontage, 1.0f);
}

죽은 NPC Destory

캐릭터와 달리 NPC는 죽으면 일정 시간 후에 게임에서 사라지게 만드는 것이 바람직하다. NPC.cpp에 이러한 로직을 구현해보자.

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

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

언리얼의 타이머 시스템을 적극 활용해서 DeadEventDelayTime이 지난 후에 NPC 캐릭터를 Destory한다.


최종 화면

profile
🏦KAIST EE | 🏦SNU AI(빅데이터 핀테크 전문가 과정) | 📙CryptoHipsters 저자

0개의 댓글