Ability Task
GameplayAbility가 작업을 수행하기 위해 사용하는 일꾼 같은 개념.
즉각적으로, 또는 시간에 걸쳐서 사용할 수 있음.
기본적으로 제공되는 PlayMontageAndWait과 같은 AbilityTask도 있으며,
사용자가 직접 AbilityTask를 작성하여 사용하는 것도 가능함.
PlayMontageAndWait
```cpp
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "UObject/ObjectMacros.h"
#include "Animation/AnimInstance.h"
#include "Abilities/Tasks/AbilityTask.h"
#include "AbilityTask_PlayMontageAndWait.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FMontageWaitSimpleDelegate);
/** Ability task to simply play a montage. Many games will want to make a modified version of this task that looks for game-specific events */
UCLASS()
class GAMEPLAYABILITIES_API UAbilityTask_PlayMontageAndWait : public UAbilityTask
{
GENERATED_UCLASS_BODY()
// PlayMontageAndWait 노드의 여러 Output 핀들은 아래의 델리게이트 변수 선언을 통해 가능한 것이다.
UPROPERTY(BlueprintAssignable)
FMontageWaitSimpleDelegate OnCompleted;
UPROPERTY(BlueprintAssignable)
FMontageWaitSimpleDelegate OnBlendOut;
UPROPERTY(BlueprintAssignable)
FMontageWaitSimpleDelegate OnInterrupted;
UPROPERTY(BlueprintAssignable)
FMontageWaitSimpleDelegate OnCancelled;
UFUNCTION()
void OnMontageBlendingOut(UAnimMontage* Montage, bool bInterrupted);
UFUNCTION()
void OnMontageInterrupted();
UFUNCTION()
void OnMontageEnded(UAnimMontage* Montage, bool bInterrupted);
/**
* Start playing an animation montage on the avatar actor and wait for it to finish
* If StopWhenAbilityEnds is true, this montage will be aborted if the ability ends normally. It is always stopped when the ability is explicitly cancelled.
* On normal execution, OnBlendOut is called when the montage is blending out, and OnCompleted when it is completely done playing
* OnInterrupted is called if another montage overwrites this, and OnCancelled is called if the ability or task is cancelled
*
* @param TaskInstanceName Set to override the name of this task, for later querying
* @param MontageToPlay The montage to play on the character
* @param Rate Change to play the montage faster or slower
* @param StartSection If not empty, named montage section to start from
* @param bStopWhenAbilityEnds If true, this montage will be aborted if the ability ends normally. It is always stopped when the ability is explicitly cancelled
* @param AnimRootMotionTranslationScale Change to modify size of root motion or set to 0 to block it entirely
* @param StartTimeSeconds Starting time offset in montage, this will be overridden by StartSection if that is also set
*/
UFUNCTION(BlueprintCallable, Category="Ability|Tasks", meta = (DisplayName="PlayMontageAndWait",
HidePin = "OwningAbility", DefaultToSelf = "OwningAbility", BlueprintInternalUseOnly = "TRUE"))
static UAbilityTask_PlayMontageAndWait* CreatePlayMontageAndWaitProxy(UGameplayAbility* OwningAbility,
FName TaskInstanceName, UAnimMontage* MontageToPlay, float Rate = 1.f, FName StartSection = NAME_None, bool bStopWhenAbilityEnds = true, float AnimRootMotionTranslationScale = 1.f, float StartTimeSeconds = 0.f);
virtual void Activate() override;
/** Called when the ability is asked to cancel from an outside node. What this means depends on the individual task. By default, this does nothing other than ending the task. */
virtual void ExternalCancel() override;
virtual FString GetDebugString() const override;
protected:
virtual void OnDestroy(bool AbilityEnded) override;
/** Checks if the ability is playing a montage and stops that montage, returns true if a montage was stopped, false if not. */
bool StopPlayingMontage();
FOnMontageBlendingOutStarted BlendingOutDelegate;
FOnMontageEnded MontageEndedDelegate;
FDelegateHandle InterruptedHandle;
UPROPERTY()
TObjectPtr<UAnimMontage> MontageToPlay;
UPROPERTY()
float Rate;
UPROPERTY()
FName StartSection;
UPROPERTY()
float AnimRootMotionTranslationScale;
UPROPERTY()
float StartTimeSeconds;
UPROPERTY()
bool bStopWhenAbilityEnds;
};
```
CreatePlayMontageAndWaitProxy() 함수가 바로 GameplayAbility에서 PlayMontageAndWait 노드를 호출할 수 있게 해주는 함수이다.
Delegate를 상당히 많이 사용하는 모습을 볼 수 있다.
void UAbilityTask_PlayMontageAndWait::Activate()
{
if (Ability == nullptr)
{
return;
}
bool bPlayedMontage = false;
if (UAbilitySystemComponent* ASC = AbilitySystemComponent.Get())
{
const FGameplayAbilityActorInfo* ActorInfo = Ability->GetCurrentActorInfo();
UAnimInstance* AnimInstance = ActorInfo->GetAnimInstance();
if (AnimInstance != nullptr)
{
if (ASC->PlayMontage(Ability, Ability->GetCurrentActivationInfo(), MontageToPlay, Rate, StartSection, StartTimeSeconds) > 0.f)
{
// Playing a montage could potentially fire off a callback into game code which could kill this ability! Early out if we are pending kill.
if (ShouldBroadcastAbilityTaskDelegates() == false)
{
return;
}
InterruptedHandle = Ability->OnGameplayAbilityCancelled.AddUObject(this, &UAbilityTask_PlayMontageAndWait::OnMontageInterrupted);
BlendingOutDelegate.BindUObject(this, &UAbilityTask_PlayMontageAndWait::OnMontageBlendingOut);
AnimInstance->Montage_SetBlendingOutDelegate(BlendingOutDelegate, MontageToPlay);
MontageEndedDelegate.BindUObject(this, &UAbilityTask_PlayMontageAndWait::OnMontageEnded);
AnimInstance->Montage_SetEndDelegate(MontageEndedDelegate, MontageToPlay);
ACharacter* Character = Cast<ACharacter>(GetAvatarActor());
if (Character && (Character->GetLocalRole() == ROLE_Authority ||
(Character->GetLocalRole() == ROLE_AutonomousProxy && Ability->GetNetExecutionPolicy() == EGameplayAbilityNetExecutionPolicy::LocalPredicted)))
{
Character->SetAnimRootMotionTranslationScale(AnimRootMotionTranslationScale);
}
bPlayedMontage = true;
}
}
else
{
ABILITY_LOG(Warning, TEXT("UAbilityTask_PlayMontageAndWait call to PlayMontage failed!"));
}
}
else
{
ABILITY_LOG(Warning, TEXT("UAbilityTask_PlayMontageAndWait called on invalid AbilitySystemComponent"));
}
if (!bPlayedMontage)
{
ABILITY_LOG(Warning, TEXT("UAbilityTask_PlayMontageAndWait called in Ability %s failed to play montage %s; Task Instance Name %s."), *Ability->GetName(), *GetNameSafe(MontageToPlay),*InstanceName.ToString());
if (ShouldBroadcastAbilityTaskDelegates())
{
OnCancelled.Broadcast();
}
}
SetWaitingOnAvatar();
}
GameplayEvents - Spawn Firebolt


SendGameplayEventToActor를 통해 GameplayTag를 특정 액터에게 호출한다.
(GameplayTag에 해당하는 GameplayEvent가 특정 액터에게 발생했다고 알림)
GameplayEvent의 WaitForGameplayEvent 노드의 EventReceived 부분에 투사체를 발사하는 로직을 호출해주면 된다.
(AuraProjectileSpell 클래스에서 ActivateAbility()에서 실행되던 투사체 발사를 별도의 함수를 생성하여 분리해주면 쉽게 처리 가능)
```cpp
void UAuraProjectileSpell::SpawnProjectile()
{
const bool bIsServer = GetAvatarActorFromActorInfo()->HasAuthority();
if (!bIsServer) return;
ICombatInterface* CombatInterface = Cast<ICombatInterface>(GetAvatarActorFromActorInfo());
if (CombatInterface)
{
const FVector SocketLocation = CombatInterface->GetCombatSocketLocation();
FTransform SpawnTransform;
SpawnTransform.SetLocation(SocketLocation);
// TODO: Set Projectile Rotation
AAuraProjectile* Projectile = GetWorld()->SpawnActorDeferred<AAuraProjectile>(
ProjectileClass,
SpawnTransform,
GetOwningActorFromActorInfo(),
Cast<APawn>(GetOwningActorFromActorInfo()),
ESpawnActorCollisionHandlingMethod::AlwaysSpawn);
// TODO: Set Projectile Properties (Give Projectile a GameplayEffectSped for causing Damage)
Projectile->FinishSpawning(SpawnTransform);
}
}
```
Custom Ability Task - Spawn Firebolt from right Location
커스텀 AbilityTask를 생성하여 GameplayAbility에서 사용해본다.
현재는
#include "CoreMinimal.h"
#include "Abilities/Tasks/AbilityTask.h"
#include "TargetDataUnderCursor.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FCursorTargetDataDelegate, const FVector&, Data);
UCLASS()
class AURA_API UTargetDataUnderCursor : public UAbilityTask
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintCallable, Category = "Ability|Tasks", meta = (DisplayName = "Target Data Under Cursor", HidePin = "OwningAbility", DefaultToSelf = "OwningAbility", BlueprintInternalUseOnly = "true"))
static UTargetDataUnderCursor* CreateTargetDataUnderCursor(UGameplayAbility* OwningAbility);
UPROPERTY(BlueprintAssignable)
FCursorTargetDataDelegate OnCursorLocation;
private:
UFUNCTION()
virtual void Activate() override;
};
AbilityTask_PlayMontageAndWait 등 기존에 존재하던 AbilityTask 클래스를 참고하면 된다.
다른 AbilityTask가 그렇듯이, 스스로의 포인터를 반환하는 static 함수를 선언한다.
매개변수로는 UGameplayAbility*가 필요하다. 이 AbilityTask가 어떤 Ability의 작업을 처리하고 있는지를 알아야 하기 때문이다.
함수의 UFUNCTION() 매크로의 지정자가 중요한 편이다.
AbilityTask의 Activate()를 오버라이드 함수로 선언한다.
AbilityTask 커스텀 가능한 실질적인 기능은 해당 함수에 작성하게 된다.
델리게이트를 선언하고, 해당 델리게이트를 UPROPERTY(BlueprintAssignable)로 지정해준다.
이 AbilityTask의 목적은 투사체를 발사할 위치를 얻는 것이기 때문에, 여기에서는 FVector 1개의 매개변수를 지니는 델리게이트를 선언했다.

이를 통해 블루프린트에서 CreateTargetDataUnderCursor()를 호출 시, 스스로의 인스턴스가 반환되며, 기본 Output 핀 뿐만 아니라 델리게이트로 선언한 변수에 따라 추가로 Output 핀이 존재하게 된다.
#include "AbilitySystem/AbilityTasks/TargetDataUnderCursor.h"
#include "AbilitySystemComponent.h"
UTargetDataUnderCursor* UTargetDataUnderCursor::CreateTargetDataUnderCursor(UGameplayAbility* OwningAbility)
{
UTargetDataUnderCursor* MyObj = NewAbilityTask<UTargetDataUnderCursor>(OwningAbility);
return MyObj;
}
void UTargetDataUnderCursor::Activate()
{
if (Ability == nullptr) return;
FHitResult HitResult;
APlayerController* PC = Ability->GetCurrentActorInfo()->PlayerController.Get();
bool bIsHit = PC->GetHitResultUnderCursor(ECC_Visibility, false, HitResult);
if (bIsHit)
{
FVector HitLocation = HitResult.Location;
OnCursorLocation.Broadcast(HitLocation);
}
}
Target Data
GAS는 서버/클라이언트로부터 TargetData를 전송하는 기능을 내장하고 있음.
클라이언트 측에서 AbilityTask의 Activate()를 호출 → 서버에서도 얼마 간의 시간 이후 Activate()가 호출될 것임. (네트워크 전송으로 인한 지연 시간 발생)

TargetData는 이러한 문제점을 해소하기 위해 GAS가 제공하는 기능임.
FGameplayAbilityTargetData를 상속하는 여러 형태의 자료형이며, ServerSetReplicatedTargetData()등의 함수를 통해 TargetData를 서버측에 전송하고,
이를 TargetSet(FAbilityTargetDataSetDelegate) 델리게이트로 Broadcast()함.
따라서, 서버 측에서 TargetSet에 바인딩 된 콜백이 있다면, TargetData를 수신 가능함.
AbilityTargetDataMap 맵으로 AbilitySpec과 TargetData를 매핑함.

혹시나 TargetData의 Broadcast()가 먼저 이루어져도, 안전장치의 개념으로 CallReplicatedTargetDataDelegateIfSet()를 사용함.
