
Trace Channels에 Ignore 값을 갖도록 하나 추가해준다. block으로 설정하면 모든 물체가 반응하기 때문이다.

ABCapsule과 ABTrigger를 추가해준다. ABCapsule은 캡슐 컴포넌트에, ABTrigger는 트리거 되는 부분에 사용할 부분이다(Trigger는 나중에 사용).
Block을 체크하면 길을 막게하고Overlap은 길을 막게하지 않지만 이벤트가 일어나게 한다.

설정한 값들은 Config 폴더의 DefaultEngine.ini 파일에 저장되고 내용을 보면 내부에서 ABAction이라는 이름보다는 ECC_GameTraceChannel+숫자 로 이루어진 열거형을 사용하는 것을 알 수 있다.
#define CPROFILE_ABCAPSULE TEXT("ABCapsule")
#define CPROFILE_ABTRIGGER TEXT("ABTrigger")
#define CCHANNEL_ABACTION ECC_GameTraceChannel1
더 편하게 사용하기 위해서 ABCollision.h 파일을 생성해서 매크로를 설정한다.
노티파이(Notify)는애니메이션 시퀀스(Animation Sequences) 에 동기화된 반복 가능한 이벤트를 생성하는 방법을 제공합니다 - 언리얼 문서
즉, 애니메이션에서 이벤트를 발생시킬 수 있다는 뜻이다.

몽타주 내부에서 노티파이 채널에 우클릭을 하면 노티파이를 설정할 수 있다.
하지만 추가하기 전에 우리가 원하는 기능을 구현하기 위해서 Notify클래스를 하나 생성한다.

AnimNotify 클래스를 상속받는데 네이밍 규칙(AnimNotify_이름)이 존재하기 때문에 이를 따른다.
애니메이션에 설정한 Notify가 호출되면 우리가 정의한 클래스의 Notify()가 호출된다.

인터페이스 클래스를 정의해서 Character가 상속받도록 해서 해당 인터페이스를 구현하도록 한다.
<Interface.h>
class ARENABATTLE_API IABAnimationAttackInterface
{
GENERATED_BODY()
// Add interface functions to this class. This is the class that will be inherited to implement this interface.
public:
virtual void AttackHitCheck() = 0;
};
의존성이 생기는 경우에 가급적이면 인터페이스를 통해서 구현하도록 설계하면 범용적으로 노티파이 기능들을 사용할 수 있다.
animnotify와 characterbase 헤더에 추가해주고 해당 인터페이스를 characterbase에 상속하도록 한다. 그럼 플레이어는 해당 함수를 반드시 갖고있어야 한다.
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()에서 캐릭터의 AttackHitCheck()을 호출시킨다.
인터페이스를 활용하면 클래스 사이의 의존성을 줄일 수 있다.
void AABCharacterBase::AttackHitCheck()
{
FHitResult OutHitResult;
// 첫번째 인자는 InTraceTag라고 나중에 이 Collision을 어떤 태그정보로 분석할 때 식별자 정보로 사용된다.
// SCENE_QUERY_STAT은 언리얼에서 지원하는 분석 툴이 있다. 여기서 Attack이라는 태그로 우리가 수행한 작업에 대해서 조사할 수 있게 태그를 추가하는 것이다.
// 두번째 인자는 bInTraceComplex로 복잡한 형태의 충돌체(캡슐이나 구 가 아닌 복잡한 것들)도 감지할지에 대한 옵션
// 세번째 인자는 InIgnoreActor로 무시할 액터(여기서는 자기 자신만 무시하면 된다.)
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;
// 월드가 제공하기 때문에 GetWorld()로 포인터 받아옴
// 결과값을 받아올 수 있는 구조체 FHitResult를 넣어줌
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
SweepSingleByChannel()을 통해 실질적인 Hit체크를 한다.
충돌을 확인하려면 디버그 출력으로 밖에 확인을 못하는데 이때 언리얼 엔진이 제공하는 디버그 드로잉 기능을 사용해서 화면에 나타낼 수 있다.(ENABLE_DRAW_DEBUG 내부)
ABAction을 매크로로 설정했던 CCHANNEL_ABACTION을 트레이스 채널로 사용하고 지정한 크기의 구를 생성한다. 이때 Actor의 위치와 방향을 받아와서 바로 앞에 생성시키도록 한다.
Hit된다면 초록색, 그렇지 않으면 빨간색이 나오도록 설정했다.

현재로서 캐릭터에 대한 반응만 설정해서 hit 되었는지 알 수 없기 때문에 NPC를 생성해서 확인해보도록 하겠다.

C++로 CharacterBase를 상속받아 만든다. 플레이어 캐릭터와 구분을 위해 다른 메시를 사용하였다.

초록색이 잘 나오는 것을 확인할 수 있다.
여기에 캐릭터가 공격받으면 쓰러지는 모션을 추가해보자.

몽타주는 그룹별로 관리할 수 있는데 이를 슬롯이라고 한다.
슬롯을 통해서 모션들을 그룹핑을 하여 각각 따로 처리할 수 있다.
Dead 슬롯을 추가해주고 왼쪽 슬롯에 방금 생성한 DeadSlot으로 설정해준다.

Animation Blueprint 안의 몽타주 슬롯에서 특정 슬롯을 재생하도록 할 수 있다.
Output에 가깝게 설정할수록 slot의 재생 우선순위가 높아지기 때문에 Dead모션이 가장 1순위로 재생될 것이다.
void SetDead()override;
아까 생성한 공격받는 캐릭터 클래스에 SetDead()를 추가해준다.
float AABCharacterBase::TakeDamage(float Damage, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
// Instigator는 피해를 입힌 가해자, DamageCause는 가해자가 사용한 무기나 가해자가 빙의한 폰
// 이를 이용해서 내가 누구에게서 데미지를 받았는지 파악할 수 있다.
Super::TakeDamage(Damage, DamageEvent, EventInstigator, DamageCauser);
SetDead();
return Damage;
}
void AABCharacterBase::SetDead()
{
GetCharacterMovement()->SetMovementMode(EMovementMode::MOVE_None);
PlayDeadAnimation();
// Collision 기능 끄기
SetActorEnableCollision(false);
}
void AABCharacterBase::PlayDeadAnimation()
{
UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
// 모든 몽타주 재생 중지
AnimInstance->StopAllMontages(0.0f);
AnimInstance->Montage_Play(DeadMontage, 1.0f);
}
캐릭터에는 공격을 했을 때, 혹은 공격을 받았을 때 데미지를 받는 함수가 있다.(따로 구현 x)
TakeDamage라는 함수이고 Actor에서 구현되어있다. return값을 최종 받는 데미지이다.
TakeDamage()는 여기서는 실질적으로 데미지를 받는 계산을 하지는 않고 SetDead()를 호출한다.
SetDead()는 Character의 움직임을 제한시키고 Dead 애니메이션을 재생하고 Collision기능을 끄는 역할로 캐릭터의 죽은 느낌이 들도록 한다.
PlayDeadAnimation()은 모든 재생중인 몽타주를 멈추고 설정해둔 DeadMontage를 재생하도록 한다.

FTimerHandle DeadTimerHandle;
// Destroy() 라는 내용을 가진 람다 함수를 만들어서 델리게이트에 부착시켜 SetTimer에 연결
GetWorld()->GetTimerManager().SetTimer(DeadTimerHandle, FTimerDelegate::CreateLambda(
[&]()
{
Destroy();
}
), DeadEventDelayTime, false);
FTimerHandle을 통해 일정시간 뒤에 Destroy()를 호출시키는 람다와 연결된 Delegate를 호출한다.
