본문 바로가기
Unreal 게임 개발/Unreal 강의 개인 정리

스킬 구현 (Skill GA & Anim & TargetActor 감지) - GAS

by daisy0461 2025. 8. 28.

GAS를 활용하여 범위 공격 스킬을 구현해보자.

#include "GA/ABGA_Skill.h"
#include "Character/ABGASCharacterPlayer.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "Abilities/Tasks/AbilityTask_PlayMontageAndWait.h"

UABGA_Skill::UABGA_Skill()
{
	
}

void UABGA_Skill::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData)
{
	Super::ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData);

	AABGASCharacterPlayer* TargetCharacter = Cast<AABGASCharacterPlayer>(ActorInfo->AvatarActor.Get());
	if (!TargetCharacter) return;

	ActiveSkillActionMontage = TargetCharacter->GetSkillActionMontage();
	if (!ActiveSkillActionMontage) return;

	TargetCharacter->GetCharacterMovement()->SetMovementMode(EMovementMode::MOVE_None);

	UAbilityTask_PlayMontageAndWait* PlayMontageTask = UAbilityTask_PlayMontageAndWait::CreatePlayMontageAndWaitProxy(this, TEXT("SkillMontage"), ActiveSkillActionMontage, 1.0f);
	PlayMontageTask->OnCompleted.AddDynamic(this, &UABGA_Skill::OnCompleteCallback);
	PlayMontageTask->OnInterrupted.AddDynamic(this, &UABGA_Skill::OnInterruptedCallback);

	PlayMontageTask->ReadyForActivation();
}

void UABGA_Skill::EndAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, bool bReplicateEndAbility, bool bWasCancelled)
{
	AABGASCharacterPlayer* TargetCharacter = Cast<AABGASCharacterPlayer>(ActorInfo->AvatarActor.Get());
	if (TargetCharacter)
	{
		TargetCharacter->GetCharacterMovement()->SetMovementMode(EMovementMode::MOVE_Walking);
	}
	
	Super::EndAbility(Handle, ActorInfo, ActivationInfo, bReplicateEndAbility, bWasCancelled);
}

void UABGA_Skill::OnCompleteCallback()
{
	bool bReplicatedEndAbility = true;
	bool bWasCancelled = false;

	EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, bReplicatedEndAbility, bWasCancelled);
}

void UABGA_Skill::OnInterruptedCallback()
{
	bool bReplicatedEndAbility = true;
	bool bWasCancelled = true;

	EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, bReplicatedEndAbility, bWasCancelled);
}

위 코드는 길지만 간단하다.

이 Ability가 실행되고 Montage를 재생한 후 Montage가 끝나면 Ability가 끝난다는 것이다.

그럼 이 Ability를 Character가 받아야한다. 해당 Ability는 무기를 집을 때 사용 가능하다고 가정하자.

void AABGASCharacterPlayer::EquipWeapon(const FGameplayEventData* EventData)
{
	if (Weapon)
	{
		Weapon->SetSkeletalMesh(WeaponMesh);

		FGameplayAbilitySpec NewSkillSpec(SkillAbilityClass);
		NewSkillSpec.InputID = 3;

		//해당 어빌리티가 없으면 GiveAbility를 한다.
		if (ASC->FindAbilitySpecFromClass(SkillAbilityClass))
		{
			ASC->GiveAbility(NewSkillSpec);
		}
	}
}

void AABGASCharacterPlayer::UnequipWeapon(const FGameplayEventData* EventData)
{
	if (Weapon)
	{
		Weapon->SetSkeletalMesh(nullptr);

		FGameplayAbilitySpec* SkillAbilitySpec = ASC->FindAbilitySpecFromClass(SkillAbilityClass);
		if (SkillAbilitySpec)
		{
			//어빌리티 제거
			ASC->ClearAbility(SkillAbilitySpec->Handle);
		}
	}
}

void AABGASCharacterPlayer::GASInputPressed(int32 InputId)
{
	FGameplayAbilitySpec* Spec = ASC->FindAbilitySpecFromInputID(InputId);
	if (Spec)
	{
		Spec->InputPressed = true;
		if (Spec->IsActive())		//Ability가 이미 발동이 되었는지 확인한다.
		{
			//다시 눌러졌음을 알리기만한다.
			ASC->AbilitySpecInputPressed(*Spec);
		}
		else
		{
			//어빌리티를 실행하고 핸들을 사용해서 제어를 해야하기 때문에 Spec에 있는 Handle 값을 넣어준다.
			ASC->TryActivateAbility(Spec->Handle);
		}
	}
}

이것 또한 지금까지 따라왔으면 간단하다.

넣어둔 SkillAbilityClass를 찾아 없다면 GiveAbility를 하고 무기를 해제할 때 해당 Ability도 해제시켜준다.
그리고 해당 ID에 맞는 값이 눌려졌을 때 TryActivateAbility를 한다.

 

그렇다면 해당 Ability가 발동하면 현재까진 AnimMontage가 재생이 될 것이다.

AnimMontage에 이전에 제작했던 AnimNotify_GASAttackHitCheck를 추가하면

캐릭터에게 GiveAbility가 되어있는 Ability 중 Tag에 맞는 Ability가 발동할 것이다.

Tag는 다음과 같이 설정했다.

 

이 Skill Ability도 이전에 제작했던 ABGA_AttackHitCheck를 실행할 것이다.

 

여기서 문제가 생긴다.

이전에 작성했던 ABGA_AttackHitCheck Trace를 통해서 1명의 Target만 뽑았다.

하지만 범위 공격은 1명의 Target만 뽑는 동작이 아니다.

그렇다면 Trace를 여러명 진행해야한다.

//UABAT_Trace* AttackTraceTask = UABAT_Trace::CreateTask(this, AABTA_Trace::StaticClass());
UABAT_Trace* AttackTraceTask = UABAT_Trace::CreateTask(this, TargetActorClass);

일단 이전에 CreateTask를 StaticClass()로 하던 것을 TSubclassOf로 Blueprint에서 받은 Class를 사용한다.

 

그리고 이 AABTA_Trace를 상속받은 클래스를 하나 만든다.

// Fill out your copyright notice in the Description page of Project Settings.


#include "GA/TA/ABTA_SphereMultiTrace.h"
#include "ArenaBattleGAS.h"
#include "AbilitySystemBlueprintLibrary.h"
#include "Abilities/GameplayAbility.h"
#include "GameFramework/Character.h"
#include "Engine/OverlapResult.h"
#include "Physics/ABCollision.h"
#include "DrawDebugHelpers.h"

AABTA_SphereMultiTrace::AABTA_SphereMultiTrace()
{
}

FGameplayAbilityTargetDataHandle AABTA_SphereMultiTrace::MakeTargetData() const
{
	ACharacter* Character = CastChecked<ACharacter>(SourceActor);

	UAbilitySystemComponent* ASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(SourceActor);
	if (!ASC)
	{
		GAS_LOG(LogABGAS, Error, TEXT("ASC Not Found"));
		return FGameplayAbilityTargetDataHandle();
	}

	//겹침 정보를 TArray로 받는다.
	TArray<FOverlapResult> Overlaps;
	const float SkillRadius = 600.0f;
	FVector Origin = Character->GetActorLocation();
	FCollisionQueryParams Params(SCENE_QUERY_STAT(AABTA_SphereMultiTrace), false, Character);
	//겹친 정보를 Overlaps에 넣는다.
	GetWorld()->OverlapMultiByChannel(Overlaps, Origin, FQuat::Identity, CCHANNEL_ABACTION, FCollisionShape::MakeSphere(SkillRadius), Params);

	//아래의 FGameplayAbilityTargetData_ActorArray에 TWeakObjectPtr이 들어가기에 따로 배열을 만들어서 넣어준다.
	TArray <TWeakObjectPtr<AActor>> HitActors;
	for (const FOverlapResult& Overlap : Overlaps)
	{
		AActor* HitActor = Overlap.OverlapObjectHandle.FetchActor<AActor>();
		if (HitActor && !HitActors.Contains(HitActor))
		{
			HitActors.Add(HitActor);
		}
	}

	//다수의 Actor를 보관하는 타입이다.
	FGameplayAbilityTargetData_ActorArray* ActorsDatas = new FGameplayAbilityTargetData_ActorArray();
	//SetActors에 WeakPtr이 들어간다.
	ActorsDatas->SetActors(HitActors);

#if ENABLE_DRAW_DEBUG
	if (bShowDebug)
	{
		FColor DrawColor = HitActors.Num() > 0 ? FColor::Green : FColor::Red;
		DrawDebugSphere(GetWorld(), Origin, SkillRadius, 16, DrawColor, false, 5.f);
	}
#endif

	return FGameplayAbilityTargetDataHandle(ActorsDatas);
}

다음 코드와 같이 여러 Actor를 감지할 수 있는 Trace를 제작하면 된다.

특이사항은 FGammeplayAbilityTargetData_ActorArray형의 SetActors가 TWeakObjectPtr이 들어간다는 점이다.

그렇기에 위 코드와 같이 TArray로 받은 값을 다시 TWeakObjectPtr<AActor>형 배열에 넣는 것을 확인할 수 있다.