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

AbilityTask와 TargetActor를 활용한 물리 판정 - GAS

by daisy0461 2025. 8. 22.

물리 판정을 위해 제작한 GA와 AT 그리고 TA를 순서대로 살펴보자.

 

GA의 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);

	//AbilityTask를 생성함.
	UABAT_Trace* AttackTraceTask = UABAT_Trace::CreateTask(this, AABTA_Trace::StaticClass());
	//AbilityTask가 끝났을 때 OnTraceResultCallback 수행.
	AttackTraceTask->OnComplete.AddDynamic(this, &ThisClass::OnTraceResultCallback);
	//Task를 실행시킨다.
	AttackTraceTask->ReadyForActivation();
}

void UABGA_AttackHitCheck::OnTraceResultCallback(const FGameplayAbilityTargetDataHandle& TargetDataHandle)
{
	//FGameplayAbilityTargetDataHandle는 내부적으로 TArray<TSharedPtr<FGameplayAbilityTargetData>> 컨테이너를 들고 있다.
	//TagetDataHasHitResult는 TargetDataHandle에서 0번째 요소를 뽑아 해당 요소에 HitResult가 존재하는지 확인하는 함수이다.
	if (UAbilitySystemBlueprintLibrary::TargetDataHasHitResult(TargetDataHandle, 0))
	{
		//TargetDataHandle의 0번째 요소에서 HitResult를 Get하여 저장.
		FHitResult HitResult = UAbilitySystemBlueprintLibrary::GetHitResultFromTargetData(TargetDataHandle, 0);
		//어떤 Target이 맞았는지 출력
		GAS_LOG(LogABGAS, Log, TEXT("Target %s Detected"), *HitResult.GetActor()->GetName());
	}

	bool bReplicatedEndAbility = true;
	bool bWasCancelled = false;

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

여기서는 ABAT_Trace를 생성하고 OnComplete구독을 하며 Task를 실행시킨다.

 

그럼 이제 ABAT_Trace Task를 보자.

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


#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< AABTA_Trace> TargetActorClass)
{
	//UABAT_Trace Task를 생성하여 Return한다.
	UABAT_Trace* NewTask = NewAbilityTask< UABAT_Trace>(OwningAbility);
	NewTask->TargetActorClass = TargetActorClass;
	return NewTask;
}

void UABAT_Trace::Activate()
{
	Super::Activate();

	SpawnAndInitializeTargetActor();
	FinalizeTargetActor();

	SetWaitingOnAvatar();
}

void UABAT_Trace::OnDestroy(bool AbilityEnded)
{
	if (SpawnedTargetActor)
	{
		SpawnedTargetActor->Destroy();
	}

	Super::OnDestroy(AbilityEnded);
}

void UABAT_Trace::SpawnAndInitializeTargetActor()
{
	//SpawnedTargetActor를 생성할 때 SpawnActorDeferred로 초기화할 시간을 주는 상태로 생성합니다.
	SpawnedTargetActor = Cast<AABTA_Trace>(Ability->GetWorld()->SpawnActorDeferred<AGameplayAbilityTargetActor>(TargetActorClass, FTransform::Identity, nullptr, nullptr, ESpawnActorCollisionHandlingMethod::AlwaysSpawn));
	if (SpawnedTargetActor)
	{
		//SpawnedTargetActor의 변수값과 Delegate에 함수를 Bind합니다.
		SpawnedTargetActor->SetShowDebug(true);
		//아래 Delegate는 ConfirmTargeting()할 때 호출됩니다.
		SpawnedTargetActor->TargetDataReadyDelegate.AddUObject(this, &UABAT_Trace::OnTargetDataReadyCallback);
	}
}

void UABAT_Trace::FinalizeTargetActor()
{
	//ASC는 CreateTask의 NewAbilityTask과정에서 해당 Task가 어떤 Ability와 연결될지 알려주는데 
	//이때 ASC도 같이 세팅하여 ASC를 Task에서 들고올 수 있다.
	UAbilitySystemComponent* ASC = AbilitySystemComponent.Get();
	if (ASC)
	{
		//ASC에서 아바타 Actor의 Transform을 가져온다.
		//그리고 아래 FinishSpawning에 해당 값을 넣어 액터를 해당 위치에 배치시킨다.
		const FTransform SpawnTransform = ASC->GetAvatarActor()->GetTransform();
		//여기서 Deferred로 지연된 생성이 완료됩니다.
		SpawnedTargetActor->FinishSpawning(SpawnTransform);

		//ASC에는 자신이 스폰한 TargetActor를 추적하는 배열인 SpawnedTargetActors가 있다.
		//Push를 한다는 의미는 ASC가 해당 TargetActor를 관리 리스트에 등록한다는 의미이다.
		ASC->SpawnedTargetActors.Push(SpawnedTargetActor);
		//TargetActor가 어떤 Ability를 위한 Targeting인지 파라미터를 통해 알 수 있으면 Targeting을 시작하는 함수이다.
		SpawnedTargetActor->StartTargeting(Ability);
		//Trace기반 TargetActor는 즉시 결과를 낼 수 있어서 바로 결과를 낸다.
		//혹시 TargetActor를 모아야하는 다른 Targeting의 경우에 바로 호출하지 않고 TargetActorHandleData를 모은 뒤에 호출한다.
		SpawnedTargetActor->ConfirmTargeting();
	}
}

void UABAT_Trace::OnTargetDataReadyCallback(const FGameplayAbilityTargetDataHandle& DataHandle)
{
	if (ShouldBroadcastAbilityTaskDelegates())
	{
		OnComplete.Broadcast(DataHandle);
	}

	EndTask();
}

해당 GA가 Activate되면

SpawnAndInitializeTargetActor에서 TargetActor를 지연 생성(SpawnActorDeferred)를 하고 값 초기화 및 Delegate Bind한다.

그리고 FinalizeTargetActor에서 TargetActor를 생성 완료하고 ASC가 해당 TargetActor를 관리 리스트에 등록한다.

그리고 Targeting을 시작하고 아래 코드를 보면 알겠지만 해당 Targeting은 Trace라서 아래에 ConfirmTargeting()을 한다.

 

그리고 ABTA_Trace이다.

#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는 AGameplayAbilityTargetActor에 선언되어있다.
	SourceActor = Ability->GetCurrentActorInfo()->AvatarActor.Get();
}

void AABTA_Trace::ConfirmTargetingAndContinue()
{
	if (SourceActor)
	{
		//Trace 기반이기에 여기서 DataHandle을 만들고 반환한다.
		FGameplayAbilityTargetDataHandle DataHandle = MakeTargetData();
		//TargetDataReadyDelegate는 AGameplayAbilityTargetActor에 선언되어 있으며 FGameplayAbilityTargetDataHandle대한 인자를 받는다.
		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;

	// Sweep 충돌 검사 파라미터 (Trace를 수행한 주체를 무시하도록 설정)
	//SCENE_QUERY_STAT은 해당 트레이스의 이름이다.
	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;

	// 구체 형태로 Sweep Trace 수행
	// Start ~ End 구간을 AttackRadius만큼의 Sphere로 쓸어보면서 CCHANNEL_ABACTION을 가진 Actor 충돌 탐색
	//가장 먼저 탐지된 한개 충돌 결과만 반환
	bool HitDetected = GetWorld()->SweepSingleByChannel(OutHitResult, Start, End, FQuat::Identity, CCHANNEL_ABACTION, FCollisionShape::MakeSphere(AttackRadius), Params);

	FGameplayAbilityTargetDataHandle DataHandle;
	//맞은게 있다면
	if (HitDetected)
	{
		// HitResult를 담은 TargetData 생성한다.
		//OutHitResult는 SweepSingleByChannel상 하나만 채워지기 때문에 하나의 TargetData가 만들어진다.
		FGameplayAbilityTargetData_SingleTargetHit* TargetData = new FGameplayAbilityTargetData_SingleTargetHit(OutHitResult);
		// Handle에 추가
		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;
}

여기서는 하나의 TargetActor를 Sphere Trace로 받아서 Handle에 저장한 후 Delegate로 Broadcast한다.

Trace로 하는 동작이기에 바로 결과를 도출할 수 있어서 ConfirmTargeting에 의해 호출되는 ConfirmTargetAndContinue에서 Data를 수집하였지만 기다려야하는 경우에는 다른 곳에서 작성해야한다.

 

이제 다음 동작은 Broadcast되면 AT에서 해당 신호를 받아
SpawnedTargetActor->TargetDataReadyDelegate.AddUObject(this, &UABAT_Trace::OnTargetDataReadyCallback);
에 의해 OnTargetDataReadyCallback을 실행한다.

OnTargetDataReadyCallback함수 내부의 OnComplete.Broadcast(DataHandle);를 통해
GA에 DataHandle을 넘기게 되고 어떤 Actor가 맞았는지 GA에서 출력하게 된다.