요약
- 공격판정 구현을 위한 물리 트레이스 채널 및 프로필 설정
- 디버그 드로잉 기능을 활용한 충돌 디버깅
- 데미지 프레임웍을 이용한 데미지 전달
- Delegate와 Lambda함수의 사용
Unreal World가 제공하는 3가지 충돌 서비스
LineTrace, Sweep, Overlap 3가지 방법으로 World 내 배치된 충동체와 충돌하는지 파악하고 충돌한 Actor 정보를 얻어올 수 있다.
- LineTrace는 지정한 방향으로 "선"을 투사해 이와 충돌되는 물체가 있는지 확인한다.
- Sweep은 선이 아닌 어떠한 "도형"을 지정한 방향으로 투사해 물체가 있는지 확인한다.
- Overlap은 투사는 아니지만 지정한 영역에 큰 범위의 도형을 설정해 해당 Volume 영역과 물체가 충돌하는지 검사한다.
공격판정 구현을 위한 물리 트레이스 채널 및 프로필 설정
트레이스(Trace)는 위에서 기술한 물체와의 충돌을 판정하는 메소드들을 제공한다.
그리고 트레이스 채널(Trace Channel)은 물체끼리 충돌이 가능한지, 이벤트를 발생할건지 등을 정하는 프로필이다.
캐릭터끼리 서로 공격판정을 낼 것이기 때문에 다른 물체와 충돌이 나지 않게 하려고 한다.
그런 경우 New Trace Channel을 만들어서 모든 물체에 대해 충돌을 일단 무시하기 위해 Ignore로 생성한다. 이름은 ABAction이라고 한다.
그리고 두개의 Collision Profile을 만들어서 하나는 Capsule Component용과 Trigger 용으로 하나를 만든다.
이 Profile을 통해 각각의 오브젝트가 어떤 Collision 규칙을 따를지 설정할 수 있다. 가령 A라는 물체는 B라는 물체와는 Block하지만 C라는 물체와는 Ignore하거나 Overlap하고 싶은 경우가 있을 때, 커스텀 가능한 것이다.
이렇게 설정하면 결과물들은 Config 폴더의 DefaultEngine.ini 에 기록이 된다. 여기서 사용되는 GCC Channel 번호를 이용하는 것이다.
몽타주 수정
이전 포스팅에서 만든 몽타주에 Notify를 추가하자.
Notify는 자동적으로 애니메이션이 실행될 때 목표 프레임에 도달 했을 경우 발생하는 이벤트이다.
AnimNotify를 상속받은 우리만의 C++을 만들어서 Notify 해보자.
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();
}
}
}
Notify의 인자로는 MeshComp가 들어오는데, MeshComp->GetOwner를 통해 Notify를 실행한 주체를 알 수 있다. 최종적으로 이 주체의 AttackHitCheck()라는 멤버 함수를 호출한다. 이는 의존성을 떨어뜨리기 위해 구성된 Interface이고 이를 CharacterBase가 상속받아 구현해준다. 여기에 우리가 하고자 하는 동작을 넣어주면 된다.
Trace 함수 선택 방법
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 값을 더해 준비해 준다.
#if endif 문을 통해 시작적으로 결과를 확인할 수 있다.
NPC 캐릭터 만들기
NPC는 CharacterBase를 상속받고 죽는 함수만 override해서 작성하면 된다.
ARyanNPCCharacter::ARyanNPCCharacter()
{
}
void ARyanNPCCharacter::SetDead()
{
Super::SetDead();
FTimerHandle DeadTimerHandle;
GetWorld()->GetTimerManager().SetTimer(DeadTimerHandle, FTimerDelegate::CreateLambda(
[&]()
{
Destroy();
}
), DeadEventDelayTime, false);
}
NPC 캐릭터가 죽으면 타이머를 설정하고 없에는 코드이다. Delegate와 Lambda를 사용해서 작성할 수 있다. SetTimer는 함수가 여러개이기 때문에 Delegate 버전은 위와 같다.
데미지 구현
언리얼 엔진에서 미리 만들어 놓은, Actor 레밸에서 구현된 TakeDamage라는 함수가 있다. 이를 override해서 사용해보자.
return은 최종적으로 해당 Actor가 받은 데미지 양을 의미한다. Instigator는 나에게 데미지를 입힌 주체를 의미하고, Causer는 가해자가 빙의한 폰 혹은 들고 있는 무기 정보이다.
만약 이 TakeDamage라는 함수를 커스터마이징 한다면, 방어력을 구현해 들어온 데미지를 반감시켜서 리턴하도록 만들어 볼 수도 있다.
// 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);
}
그런데 이렇게 직접적으로 TakeDamage를 호출해도 상관 없지만 언리얼 엔진에는 ApplyDamage라는 함수가 존재한다.
이를 호출하면 매개변수로 지정된 Actor의 TakeDamage 함수를 자동으로 호출하게 된다.
'Unreal Engine 5 > 강의' 카테고리의 다른 글
[UE5] 스폰 - 무한 맵 제작 (0) | 2024.12.10 |
---|---|
[UE5] 아이템 - 여러 종류 아이템 획득하기 (0) | 2024.12.09 |
[UE5] 애니메이션 - 캐릭터 애니메이션과 몽타주를 이용한 연속 공격 (0) | 2024.12.03 |
[UE5] 캐릭터 움직임 - 캐릭터 컨트롤에 대해 (0) | 2024.11.29 |
[UE5] 언리얼 게임 제작 기초 - C++ 과 캐릭터 입력 시스템 (0) | 2024.11.28 |