공격 판정
이전 포스트에서 구현한 콤보 공격에서 공격 판정을 추가로 구현할 것이다.
여태까지의 공격 기능에 흐름은 다음과 같다.
- 플레이어의 입력에 의해 캐릭터의 EnhancedInput이 감지되어 해당 함수를 실행
- 사전에 부여한 Ability 중 InputId에 맞는 Ability를 실행
- Ability에서 애니메이션을 동작시키기 위해 AT(Ability Task)를 생성하여 실행
- AT의 완료 Callback을 받아 마무리
위의 과정에서 애니메이션을 동작하는 부분이 있다.
이는 AnimMontage를 이용하여 애니메이션을 동작한다.
GAS에서의 공격판정은 일반적인 UE프로젝트와 다르지 않게 시작된다.
AnimMontage에 Notify를 등록하여 원하는 동작에서 공격을 판정하게 할 수 있다.
공격판정을 위한 전체적인 흐름은 다음과 같다.
- AnimNotify가 실행되면 소유한 ASC(AbilitySystemComponent)에 공격판정을 위해 GA(GameplayAbility) 실행을 요청한다.
- 실행된 공격 판정 GA는 공격판정 결과를 받아올 AT를 만들어 실행한다.
- AT는 TargetActor라는 판정 결과를 데이터로 관리하는 액터를 생성한다.
- TargetActor에 데이터가 준비되었다면 반환한다.
- 반환된 데이터를 읽어 원하는 동작을 수행한다.
구현
우선, AnimNotify를 상속받아 GA를 실행할 수 있게 만들어야 한다.
AnimNotify에서 구현해야 하는 함수는 두 가지이다.
virtual FString GetNotifyName_Implementation() const override;
virtual void Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, const FAnimNotifyEventReference& EventReference) override;
GetNotiftName_Implementation은 에디터에서 AnimNotify를 식별할 이름을 반환하는 함수이다.
Notify는 실질적으로 Notify가 실행되었을 때, 동작할 코드를 넣어주면 된다.
지금은 Owner의 ASC를 통해 GA를 실행하면 된다.
이 과정에서 Event를 통해 특정 태그를 TriggerTag로 가지는 GA를 실행시키는 함수가 있다.
#include "AbilitySystemBlueprintLibrary.h"
void UAnimNotify_GASAttackHitCheck::Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, const FAnimNotifyEventReference& EventReference)
{
Super::Notify(MeshComp, Animation, EventReference);
if (MeshComp)
{
AActor* OwnerActor = MeshComp->GetOwner();
if (OwnerActor)
{
FGameplayEventData PayloadData;
UAbilitySystemBlueprintLibrary::SendGameplayEventToActor(OwnerActor, TriggerGameplayTag, PayloadData);
}
}
}
마지막인자는 PayloadData인데 추가적인 데이터를 넘길 수 있다.
Event를 통해 TriggerGameplayTag를 소유한 GA를 실행하기 때문에 에디터에서 이를 설정해 주어야 한다.
GA의 Ability Triggers를 보면 Trigger Tag를 설정할 수 있다.
AnimMontage에 Notify를 설정하고 Notify의 Trigger Gamplay Tag를 동일하게 설정하면 SendGameplayEventToActor를 통해 원하는 GA를 실행시킬 수 있다.
공격 판정을 하는 GA는 AT를 생성하여 실행하면 된다.
그리고 판정이 완료되면 완료된 결과를 통해 원하는 동작을 하면된다.
AT를 이용하는 이유는 판정이 빠른 시간 안에 완료된다 하여도 판정 결과가 나오기 전에는 다른 로직을 실행할 수 없기 때문에 비동기로 처리하여 판정 결과를 받아왔을 때 동작하게 하는 것이 더욱 효율적이다.
void UABGA_AttackHitCheck::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData)
{
Super::ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData);
UABAT_Trace* AttackTraceTask = UABAT_Trace::CreateTask(this, AABTA_Trace::StaticClass());
AttackTraceTask->OnComplete.AddDynamic(this, &UABGA_AttackHitCheck::OnTraceResultCallback);
AttackTraceTask->ReadyForActivation();
}
AT에서는 앞에서 언급한 TA(TargetActor)를 생성하여 데이터를 핸들링한다.
이때, TA는 Deffered로 지연 생성한다.
TA의 변수 설정이나 완료 Delegate에 Binding 하기 위해서이다.
void UABAT_Trace::Activate()
{
Super::Activate();
SpawnAndInitializeTargetActor();
FinalizeTargetActor();
SetWaitingOnAvatar();
}
void UABAT_Trace::SpawnAndInitializeTargetActor()
{
SpawnedTargetActor = Cast<AABTA_Trace>(Ability->GetWorld()->SpawnActorDeferred<AGameplayAbilityTargetActor>(TargetActorClass, FTransform::Identity, nullptr, nullptr, ESpawnActorCollisionHandlingMethod::AlwaysSpawn));
if (SpawnedTargetActor)
{
SpawnedTargetActor->SetShowDebug(true);
SpawnedTargetActor->TargetDataReadyDelegate.AddUObject(this, &UABAT_Trace::OnTargetDataReadyCallback);
}
}
void UABAT_Trace::FinalizeTargetActor()
{
UAbilitySystemComponent* ASC = AbilitySystemComponent.Get();
if (ASC)
{
const FTransform SpawnTransform = ASC->GetAvatarActor()->GetTransform();
SpawnedTargetActor->FinishSpawning(SpawnTransform);
ASC->SpawnedTargetActors.Push(SpawnedTargetActor);
SpawnedTargetActor->StartTargeting(Ability);
SpawnedTargetActor->ConfirmTargeting();
}
}
TA가 생성되면 ASC에 생성된 TA를 추가하여 관리할 수 있게 해 준다.
이후, StartTargeting을 통해 Targeting을 위한 초기 작업을 시작하고 ConfirmTargeting을 통해 확정한다.
TA에서 데이터를 만들어 준비가 끝났다면 Delegate를 이용하여 결과를 돌려주면 된다.
void AABTA_Trace::StartTargeting(UGameplayAbility* Ability)
{
Super::StartTargeting(Ability);
SourceActor = Ability->GetCurrentActorInfo()->AvatarActor.Get();
}
void AABTA_Trace::ConfirmTargetingAndContinue()
{
if (SourceActor)
{
FGameplayAbilityTargetDataHandle DataHandle = MakeTargetData();
TargetDataReadyDelegate.Broadcast(DataHandle);
}
}
FGameplayAbilityTargetDataHandle AABTA_Trace::MakeTargetData() const
{
ACharacter* Character = CastChecked<ACharacter>(SourceActor);
FHitResult OutHitResult;
const float AttackRange = 100.f;
const float AttackRadius = 50.f;
FCollisionQueryParams Params(SCENE_QUERY_STAT(UABAT_Trace), false, Character);
const FVector Forward = Character->GetActorForwardVector();
const FVector Start = Character->GetActorLocation() + Forward * Character->GetCapsuleComponent()->GetScaledCapsuleRadius();
const FVector End = Start + Forward * AttackRange;
bool bHitDetected = GetWorld()->SweepSingleByChannel(OutHitResult, Start, End, FQuat::Identity, CCHANNEL_ABACTION, FCollisionShape::MakeSphere(AttackRadius), Params);
FGameplayAbilityTargetDataHandle DataHandle;
if (bHitDetected)
{
FGameplayAbilityTargetData_SingleTargetHit* TargetData = new FGameplayAbilityTargetData_SingleTargetHit(OutHitResult);
DataHandle.Add(TargetData);
}
#if ENABLE_DRAW_DEBUG
if (bShowDebug)
{
FVector CapsuleOrigin = Start + (End - Start) * .5;
float CapsuleHalfHeight = AttackRange * .5;
FColor DrawColor = bHitDetected ? FColor::Green : FColor::Red;
DrawDebugCapsule(GetWorld(), CapsuleOrigin, CapsuleHalfHeight, AttackRadius, FRotationMatrix::MakeFromZ(Forward).ToQuat(), DrawColor, false, 5.f);
}
#endif
return DataHandle;
}
실질적인 동작은 TA에서 한다고 생각하면 된다.
FGameplayAbilityTargetDataHandle이란 여러 개의 FGameplayAbilityTargetData들을 관리하는 구조체이다.
FGameplayAbilityTargetData_SingleTargetHit을 이용하면 HitResult를 쉽게 관리할 수 있다.
TA에서 돌려줄 데이터를 준비하여 TargetDataReadyDelegate를 Broadcast 하면 실행된 순서의 역순으로 callback이 실행된다.
TA를 실행한 AT에서 먼저 callback을 받는다.
void UABAT_Trace::OnTargetDataReadyCallback(const FGameplayAbilityTargetDataHandle& DataHnadle)
{
if (ShouldBroadcastAbilityTaskDelegates())
{
OnComplete.Broadcast(DataHnadle);
}
EndTask();
}
AT에서는 특별한 동작 없이 AT를 실행한 GA에 데이터를 전달해 주면 된다.
AT를 실행한 GA는 callback을 받아 원하는 동작을 하면 된다.
void UABGA_AttackHitCheck::OnTraceResultCallback(const FGameplayAbilityTargetDataHandle& TargetDataHandle)
{
if (UAbilitySystemBlueprintLibrary::TargetDataHasHitResult(TargetDataHandle, 0))
{
FHitResult HitResult = UAbilitySystemBlueprintLibrary::GetHitResultFromTargetData(TargetDataHandle, 0);
//Do Something
}
bool bReplicateEndAbility = true;
bool bWasCancelled = false;
EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, bReplicateEndAbility, bWasCancelled);
}
Target Actor
Target Actor(TA)란 게임플레이 어빌리티에서 대상에 대한 판정(주로 물리 판정)을 구현할 때 사용되는 특수한 액터이다.
플레이어가 판정을 원하는 부분에 대한 시각적 효과를 주거나 범위에 대한 확정을 하는 등의 구현이 가능하게 된다.
주요 함수로는 다음이 있다.
- StartTargeting: 타겟팅을 시작, SourceActor 같은 주요 변수들을 초기화하는데 적합
- ComfirmTargetingAndContinue: 타겟팅을 확정하고 남은 프로세스 진행
- ConfirmTargeting: 타겟팅만 확정
- CancelTargeting: 타겟팅 취소
TA는 TargetData를 생성하여 결과를 전달하는 역할을 주로 한다.
TargetData란 다음과 같은 데이터를 담는 구조체이다.
- Trace Hit 결과(HitResult)
- 판정된 액터의 포인터(여러 개)
- 시작 지점
- 끝 지점
이러한 TargetData를 여러 개 모아 하나로 관리는 구조체가 GameplayAbilityTargetDataHandle이라고 한다.
주로 이 Handle을 결과로 돌려준다.