UE5
[UE5] GAS를 활용한 캐릭터 공격
검정색필통
2025. 1. 3. 14:17
캐릭터 콤보 공격 구현을 위한 기획
- 공격 시작 후 유효 시간 내에 추가 공격 입력을 넣으면, 다음 공격 모션을 발동
- 콤보 공격에 대한 정보는 ABComboActionData에서 불러들임
- AT를 발동하고 입력 점검 타이머도 함께 발동
- 입력 점검 타이머가 발동되면 다음 공격 입력이 있는지 검사
- 다음 공격 입력이 있으면 다음 공격 모션을 발동하고 다시 입력 점검 타이머를 발동
// Fill out your copyright notice in the Description page of Project Settings.
#include "GA/ABGA_Attack.h"
#include "Character/ABCharacterBase.h"
#include "Abilities/Tasks/AbilityTask_PlayMontageAndWait.h"
#include "ArenaBattleGAS.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "Character/ABComboActionData.h"
UABGA_Attack::UABGA_Attack()
{
InstancingPolicy = EGameplayAbilityInstancingPolicy::InstancedPerActor;
}
// UAbilityTask_PlayMontageAndWait를 통해 몽타주를 재생하고, 첫 번째 콤보 섹션을 시작
// StartComboTimer를 호출하여 콤보 입력 가능 시간을 설정
void UABGA_Attack::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData)
{
Super::ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData);
AABCharacterBase* ABCharacter = CastChecked<AABCharacterBase>(ActorInfo->AvatarActor.Get());
CurrentComboData = ABCharacter->GetComboActionData();
ABCharacter->GetCharacterMovement()->SetMovementMode(EMovementMode::MOVE_None);
UAbilityTask_PlayMontageAndWait* PlayAttackTask = UAbilityTask_PlayMontageAndWait::CreatePlayMontageAndWaitProxy(this, TEXT("PlayAttack"), ABCharacter->GetComboActionMontage(), 1.0f, GetNextSection());
PlayAttackTask->OnCompleted.AddDynamic(this, &UABGA_Attack::OnCompletedCallback);
PlayAttackTask->OnInterrupted.AddDynamic(this, & UABGA_Attack::OnInterruptedCallback);
PlayAttackTask->ReadyForActivation();
StartComboTimer();
}
// 플레이어 입력이 발생하면 HasNextComboInput 플래그가 설정
void UABGA_Attack::InputPressed(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo)
{
if (!ComboTimerHandle.IsValid())
{
HasNextComboInput = false;
}
else
{
HasNextComboInput = true;
}
}
void UABGA_Attack::CancelAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, bool bReplicateCancelAbility)
{
Super::CancelAbility(Handle, ActorInfo, ActivationInfo, bReplicateCancelAbility);
}
void UABGA_Attack::EndAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, bool bReplicateEndAbility, bool bWasCancelled)
{
Super::EndAbility(Handle, ActorInfo, ActivationInfo, bReplicateEndAbility, bWasCancelled);
AABCharacterBase* ABCharacter = CastChecked<AABCharacterBase>(ActorInfo->AvatarActor.Get());
ABCharacter->GetCharacterMovement()->SetMovementMode(EMovementMode::MOVE_Walking);
CurrentComboData = nullptr;
CurrentCombo = 0;
HasNextComboInput = false;
}
void UABGA_Attack::OnCompletedCallback()
{
bool bReplicatedEndAbility = true;
bool bWasCanceled = false;
EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, bReplicatedEndAbility, bWasCanceled);
}
void UABGA_Attack::OnInterruptedCallback()
{
bool bReplicatedEndAbility = true;
bool bWasCanceled = true;
EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, bReplicatedEndAbility, bWasCanceled);
}
// 다음 섹션 이름 계산
FName UABGA_Attack::GetNextSection()
{
CurrentCombo = FMath::Clamp(CurrentCombo + 1, 1, CurrentComboData->MaxComboCount);
FName NextSection = *FString::Printf(TEXT("%s%d"), *CurrentComboData->MontageSectionNamePrefix, CurrentCombo);
return NextSection;
}
// 타이머 설정 및 콤보 입력 대기
void UABGA_Attack::StartComboTimer()
{
int32 ComboIndex = CurrentCombo - 1;
ensure(CurrentComboData->EffectiveFrameCount.IsValidIndex(ComboIndex));
// 현재 콤보 단계에서 다음 입력을 받을 수 있는 프레임 수 / 애니메이션의 초당 프레임수(FPS)
float ComboEffectiveTime = CurrentComboData->EffectiveFrameCount[ComboIndex] / CurrentComboData->FrameRate;
if (ComboEffectiveTime > 0.0f)
{
GetWorld()->GetTimerManager().SetTimer(ComboTimerHandle, this, &UABGA_Attack::CheckComboInput, ComboEffectiveTime, false);
}
}
// 타이머가 만료되면 CheckComboInput에서 입력 플래그를 확인
// 입력이 있다면 다음 섹션으로 이동(MontageJumpToSection)하고, 새로운 타이머를 설정하여 다음 콤보 입력을 기다림
// 입력이 없으면 onComplete 델리게이트가 호출되며 콜백함수 실행되고 종료
void UABGA_Attack::CheckComboInput()
{
ComboTimerHandle.Invalidate();
if (HasNextComboInput)
{
MontageJumpToSection(GetNextSection());
StartComboTimer();
HasNextComboInput = false;
}
}
어빌리티 태스크(AT)의 제작 규칙
- AT는 UAbilityTask 클래스를 상속받아 제작
- AT 인스턴스를 생성해 반환하는 static 함수를 선언해 구현
- AT가 종료되면 GA에 알려줄 델리게이트를 선언
- 시작과 종료 처리를 위해 Activate와 OnDestroy 함수를 재정의(Override)해 구현
- 일정 시간이 지난 후 AT를 종료하고자 한다면, 활성화시 SetWaitingOnAvatar 함수를 호출해 Waiting 상태로 설정
- 만일 Tick을 활성화하고 싶다면 bTickingTask 값을 true로 설정
- AT가 종료되면 델리게이트를 브로드캐스팅
// ABAT_JumpAndWaitForLanding.cpp
#include "GA/AT/ABAT_JumpAndWaitForLanding.h"
#include "GameFramework/Character.h"
UABAT_JumpAndWaitForLanding::UABAT_JumpAndWaitForLanding()
{
}
// UABAT_JumpAndWaitForLanding 작업 생성 및 반환
UABAT_JumpAndWaitForLanding* UABAT_JumpAndWaitForLanding::CreateTask(UGameplayAbility* OwningAbility)
{
UABAT_JumpAndWaitForLanding* NewTask = NewAbilityTask<UABAT_JumpAndWaitForLanding>(OwningAbility);
return NewTask;
}
// 작업 실행시 호출, 점프 동작과 착지 이벤트 바인딩 설정
void UABAT_JumpAndWaitForLanding::Activate()
{
Super::Activate();
ACharacter* Character = CastChecked<ACharacter>(GetAvatarActor());
Character->LandedDelegate.AddDynamic(this, &UABAT_JumpAndWaitForLanding::OnLandedCallback);
Character->Jump();
// 작업이 아바타의 동작에 의존하고 있음을 명시
SetWaitingOnAvatar();
}
// 작업이 파괴되거나 능력이 종료될 때 호출
void UABAT_JumpAndWaitForLanding::OnDestroy(bool AbilityEnded)
{
ACharacter* Character = CastChecked<ACharacter>(GetAvatarActor());
Character->LandedDelegate.RemoveDynamic(this, &UABAT_JumpAndWaitForLanding::OnLandedCallback);
Super::OnDestroy(AbilityEnded);
}
// 캐릭터가 착지했을 때 호출
void UABAT_JumpAndWaitForLanding::OnLandedCallback(const FHitResult& Hit)
{
// 이 작업이 여전히 활성 상태인지 확인
if (ShouldBroadcastAbilityTaskDelegates())
{
// OnComplete 델리게이트 호출
OnComplete.Broadcast();
}
}
// ABGA_Jump.cpp
#include "GA/ABGA_Jump.h"
#include "GameFramework/Character.h"
#include "GA/AT/ABAT_JumpAndWaitForLanding.h"
UABGA_Jump::UABGA_Jump()
{
InstancingPolicy = EGameplayAbilityInstancingPolicy::InstancedPerActor;
}
// 특정 능력이 실행 가능한지 여부를 판단하는 데 사용 (점프 가능한 상태인지 조건 검사)
bool UABGA_Jump::CanActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayTagContainer* SourceTags, const FGameplayTagContainer* TargetTags, OUT FGameplayTagContainer* OptionalRelevantTags) const
{
// 부모 클래스의 CanActivateAbility를 호출하여, 기본조건(쿨다운, 비용 등)이 충족되는지 확인
bool bResult = Super::CanActivateAbility(Handle, ActorInfo, SourceTags, TargetTags, OptionalRelevantTags);
if (!bResult)
{
return false;
}
// 캐릭터가 점프 가능한지 내부 상태(공중 여부, 착지 여부 등)를 기반으로 판단
const ACharacter* Character = Cast<ACharacter>(ActorInfo->AvatarActor.Get());
return (Character && Character->CanJump());
}
void UABGA_Jump::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData)
{
Super::ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData);
// OnComplete 델리게이트를 구독하고, 착지에 성공해서 호출시 OnLandedCallback 함수 호출
UABAT_JumpAndWaitForLanding* JumpAndWaitingForLandingTask = UABAT_JumpAndWaitForLanding::CreateTask(this);
JumpAndWaitingForLandingTask->OnComplete.AddDynamic(this, &UABGA_Jump::OnLandedCallback);
JumpAndWaitingForLandingTask->ReadyForActivation();
}
// 점프키를 떼면 점프가 종료
void UABGA_Jump::InputReleased(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo)
{
ACharacter* Character = CastChecked<ACharacter>(ActorInfo->AvatarActor.Get());
Character->StopJumping();
}
void UABGA_Jump::OnLandedCallback()
{
// true: Ability 종료가 서버와 클라이언트 간 동기화 됨, false: 로컬에서만 Ability를 종료
bool bReplicatedEndAbility = true;
// true: 능력이 취소된 경우, false: 능력이 정상적으로 종료된 경우
bool bWasCanceled = false;
// 활성화된 능력 종료
EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, bReplicatedEndAbility, bWasCanceled);
}
공격 판정을 위한 신규 기능 기획
- 애니메이션 몽타주의 노티파이를 활용해 원하는 타이밍에 공격을 판정하는 기능 추가
- 애니메이션 노티파이가 발동되면 판정을 위한 GA를 트리거해 발동
- 새로운 GA가 발동되면 공격 판정을 위한 AT를 실행
- GAS에서 제공하는 타겟액터를 활용해 물리 공격 판정을 수행
- 판정 결과를 시각적으로 확인할 수 있도록 드로우 디버그 기능 제공
// AnimNotify_GASAttackHitCheck.cpp
#include "Animation/AnimNotify_GASAttackHitCheck.h"
#include "AbilitySystemBlueprintLibrary.h"
// UAnimNotify를 상속 받아 구현된 애니메이션 알림 클래스
// 애니메이션의 특정 프레임에서 호출되어 GAS와 연동하는 기능 제공
UAnimNotify_GASAttackHitCheck::UAnimNotify_GASAttackHitCheck()
{
}
FString UAnimNotify_GASAttackHitCheck::GetNotifyName_Implementation() const
{
return TEXT("GASAttackHitCheck");
}
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);
}
}
}
// ABGA_AttackHitCheck.cpp
#include "GA/ABGA_AttackHitCheck.h"
#include "ArenaBattleGAS.h"
#include "AbilitySystemBlueprintLibrary.h"
#include "GA/AT/ABAT_Trace.h"
#include "GA/TA/ABTA_Trace.h"
UABGA_AttackHitCheck::UABGA_AttackHitCheck()
{
InstancingPolicy = EGameplayAbilityInstancingPolicy::InstancedPerActor;
}
void UABGA_AttackHitCheck::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData)
{
Super::ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData);
// 히트 체크를 수행하는 AT 생성
UABAT_Trace* AttackTraceTask = UABAT_Trace::CreateTask(this, AABTA_Trace::StaticClass());
AttackTraceTask->OnComplete.AddDynamic(this, &UABGA_AttackHitCheck::OnTraceResultCallback);
AttackTraceTask->ReadyForActivation();
}
void UABGA_AttackHitCheck::OnTraceResultCallback(const FGameplayAbilityTargetDataHandle& TargetDataHandle)
{
// UAbilitySystemBlueprintLibrary: 타겟 데이터에서 히트 결과를 추출하고 처리하기 위한 헬퍼 함수 제공
// TargetDataHasResult : 주어진 타겟 데이터에 히트 결과가 포함되어 있는지 확인
if (UAbilitySystemBlueprintLibrary::TargetDataHasHitResult(TargetDataHandle, 0))
{
// GetHitResultFromTargetData : 타겟 데이터에서 히트 결과 추출
FHitResult HitResult = UAbilitySystemBlueprintLibrary::GetHitResultFromTargetData(TargetDataHandle, 0);
ABGAS_LOG(LogABGAS, Log, TEXT("Target %s Detected"), *(HitResult.GetActor()->GetName()));
}
bool bReplicatedEndAbility = true;
bool bWasCanceled = false;
EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, bReplicatedEndAbility, bWasCanceled);
}
// UABAT_Trace.cpp
#include "GA/AT/ABAT_Trace.h"
#include "GA/TA/ABTA_Trace.h"
#include "AbilitySystemComponent.h"
UABAT_Trace::UABAT_Trace()
{
}
UABAT_Trace* UABAT_Trace::CreateTask(UGameplayAbility* OwningAbility, TSubclassOf<class AABTA_Trace> TargetActorClass)
{
UABAT_Trace* NewTask = NewAbilityTask<UABAT_Trace>(OwningAbility);
NewTask->TargetActorClass = TargetActorClass;
return NewTask;
}
void UABAT_Trace::Activate()
{
Super::Activate();
// 타겟 액터(AABTA_Trace)를 스폰하고 초기 설정을 수행
SpawnAndInitializeTargetActor();
// 타겟 액터의 스폰 작업을 완료하고, 타겟팅을 시작
FinalizeTargetActor();
// 이 작업이 아바타의 동작에 의존하고 있음을 명시
SetWaitingOnAvatar();
}
void UABAT_Trace::OnDestroy(bool AbilityEnded)
{
if (SpawnedTargetActor)
{
SpawnedTargetActor->Destroy();
}
Super::OnDestroy(AbilityEnded);
}
// 타겟 엑터(AABTA_Trace)를 스폰하고 초기 설정을 수행
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 SpawnTarnsform = ASC->GetAvatarActor()->GetTransform();
// 타겟 액터의 스폰 작업을 완료하고, 트랜스폼(위치/회전)을 설정
SpawnedTargetActor->FinishSpawning(SpawnTarnsform);
// ASC에 새로 생성된 타겟 액터를 추가
ASC->SpawnedTargetActors.Push(SpawnedTargetActor);
// 타겟팅 작업을 시작하고, 즉시 완료
SpawnedTargetActor->StartTargeting(Ability);
SpawnedTargetActor->ConfirmTargeting();
}
}
void UABAT_Trace::OnTargetDataReadyCallback(const FGameplayAbilityTargetDataHandle& DataHandle)
{
if (ShouldBroadcastAbilityTaskDelegates())
{
OnComplete.Broadcast(DataHandle);
}
EndTask();
}
게임플레이 어빌리티 타겟 액터
- 게임플레이 어빌리티에서 대상에 대한 판정(주로 물리 판정)을 구현할 때 사용하는 특수한 액터. 줄여서 TA
- AGameplayAbilityTargetActor 클래스를 상속받아서 구현
- 왜 타겟 액터(TA)가 필요한가?
- 타겟을 설정하는 다양한 방법이 있음
- Trace를 사용해 즉각적으로 타겟을 검출하는 방법
- 사용자의 최종 확인을 한번 더 거치는 방법이 있음 ( ex. 원거리 범위 공격 )
- 공격 범위 확인을 위한 추가 시각화 ( 시각화를 수행하는 액터를 월드레티클(WorldReticle)이라고 함 )
- 주요 함수
- StartTargeting : 타겟팅을 시작
- ConfirmTargetingAndContinue : 타겟팅을 확정하고 이후 남은 프로세스를 진행
- ConfirmTargeting : 태스크 진해 없이 타겟팅만 확정
- CancelTargeting : 타겟팅을 취소
게임플레이 어빌리티 타겟 데이터
- 타겟 액터에서 판정한 결과를 담은 데이터
- 다음의 속성을 가짐
- Trace 히트 결과 (HitResult)
- 판정된 다수의 액터 포인터
- 시작 지점
- 끝 지점
- 타겟 데이터를 여러 개 묶어 전송하는 것이 일반적인데 이를 타겟 데이터 핸들이라고 함
// ABTA_Trace.cpp
#include "GA/TA/ABTA_Trace.h"
#include "Abilities//GameplayAbility.h"
#include "GameFramework/Character.h"
#include "Components/CapsuleComponent.h"
#include "Physics/ABCollision.h"
#include "DrawDebugHelpers.h"
AABTA_Trace::AABTA_Trace()
{
}
// 타겟팅 시작 시 호출되며, 공격의 시작점(소스 액터)을 설정
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.0f;
const float AttackRadius = 50.0f;
FCollisionQueryParams Params(SCENE_QUERY_STAT(UABTA_Trace), false, Character);
const FVector Forward = Character->GetActorForwardVector();
const FVector Start = Character->GetActorLocation() + Forward * Character->GetCapsuleComponent()->GetScaledCapsuleRadius();
const FVector End = Start + Forward * AttackRange;
bool HitDetected = GetWorld()->SweepSingleByChannel(OutHitResult, Start, End, FQuat::Identity, CCHANNEL_ABACTION, FCollisionShape::MakeSphere(AttackRadius), Params);
FGameplayAbilityTargetDataHandle DataHandle;
if (HitDetected)
{
FGameplayAbilityTargetData_SingleTargetHit* TargetData = new FGameplayAbilityTargetData_SingleTargetHit(OutHitResult);
DataHandle.Add(TargetData);
}
#if ENABLE_DRAW_DEBUG
if (bShowDebug)
{
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(Forward).ToQuat(), DrawColor, false, 5.0f);
}
#endif
return DataHandle;
}