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;
}