Gameplay Ability System Course Project - Development Blog

Hello again everyone! Welcome to another entry in this on-going blog/development thread for my GAS Course Project. For more context of this project, please see the very first post in this thread, and as usual, you can find my socials here:

Twitch : Twitch
Discord : UE5 - GAS Course Project - JevinsCherries
Tiktok : Devin Sherry (@jevinscherriesgamedev) | TikTok
Youtube : Youtube
Twitter : https://twitter.com/jevincherries
Instagram : Jevins Cherries Gamedev (@jevinscherriesgamedev) • Instagram photos and videos

Today we are going to talk about a topic that I had just recently learned about in the context of using GAS and Wait Target Data in multiplayer, which is how to properly replicate local/client target data to the server when executing an ability locally. I know that on the last entry of this ongoing blog/thread I said we would talk about Attributes/Attribute Sets & Initialization, but I felt to cover this instead since I have discussed Wait Target Data as my first entry of this thread.


To begin, let’s reiterate over the original problem we needed to solve with abilities using Wait Target Data in a multiplayer context. When using any sort of targeting, more specifically Target Data with GAS, there is the question of what needs to be done locally on the client and what needs to be replicated back to the server in order for the ability to execute correctly. For my GAS Course project, I need the following only to happen on the local client/player side:

  1. The targeting reticle showing the radius of the ability’s targeting effect.
  2. The red outline to appear on the valid targets within that radius.
  3. The damage numbers to appear when targets receive damage.

In regards to Wait Target Data, only the first two points are relevant because the damage numbers are UI that is handled on the local player side anyway. In regards to what needs to be replicated to the server:

  1. The targets that the client found and received from the target data.
  2. A validation of said targets to ensure no cheating.
  3. Whether or not the client confirmed or cancelled targeting.

At the current stage of my project, I am not yet worried about cheating, but there are functions included in my logic that can be used as the designated points of server-side verification. For now, all the server needs to know is that the client either confirm or cancelled the targeting, and if confirming, retrieving the targets found in order to apply damage and clean up the ability as it is ended.


Additional Reading:

The knowledge learned of this topic came from the ability class, ULyraGameplayAbility_RangedWeapon, which is how Lyra handles client-side target data replication to the server when using ranged weapons. Additionally, much of the boilerplate code is either from default GAS or pulled directly from Lyra, with only some modifications made for the purposes of my project.

ULyraGameplayAbility_RangedWeapon


To begin, when we activate our ability, we will want to check if the ability Is Locally Controlled, and if so, run the Wait Target Data task:

Now that we are running the Wait Target Data task, how do we inform the Server when we either confirm or cancel the targeting? The answer lies in the following inside of the UAbilityTask_WaitTargetData class:

AbilityTask_WaitTargetData.h:

UFUNCTION()
virtual void OnTargetDataReadyCallback(const FGameplayAbilityTargetDataHandle& Data);

UFUNCTION()
virtual void OnTargetDataCancelledCallback(const FGameplayAbilityTargetDataHandle& Data);

AbilityTask_WaitTargetData.cpp:

/** The TargetActor we spawned locally has called back with valid target data */
void UAbilityTask_WaitTargetData::OnTargetDataReadyCallback(const FGameplayAbilityTargetDataHandle& Data)
{
	UAbilitySystemComponent* ASC = AbilitySystemComponent.Get();
	if (!Ability || !ASC)
	{
		return;
	}

	FScopedPredictionWindowScopedPrediction(ASC, ShouldReplicateDataToServer());
	
	const FGameplayAbilityActorInfo* Info = Ability->GetCurrentActorInfo();
	if (IsPredictingClient())
	{
		if (!TargetActor->ShouldProduceTargetDataOnServer)
		{
			FGameplayTag ApplicationTag; // Fixme: where would this be useful?
			ASC->CallServerSetReplicatedTargetData(GetAbilitySpecHandle(), GetActivationPredictionKey(), Data, ApplicationTag, ASC->ScopedPredictionKey);
		}
		else if (ConfirmationType == EGameplayTargetingConfirmation::UserConfirmed)
		{
			// We aren't going to send the target data, but we will send a generic confirmed message.
			ASC->ServerSetReplicatedEvent(EAbilityGenericReplicatedEvent::GenericConfirm, GetAbilitySpecHandle(), GetActivationPredictionKey(), ASC->ScopedPredictionKey);
		}
	}

	if (ShouldBroadcastAbilityTaskDelegates())
	{
		ValidData.Broadcast(Data);
	}

	if (ConfirmationType != EGameplayTargetingConfirmation::CustomMulti)
	{
		EndTask();
	}
}
/** The TargetActor we spawned locally has called back with a cancel event (they still include the 'last/best' targetdata but the consumer of this may want to discard it) */
void UAbilityTask_WaitTargetData::OnTargetDataCancelledCallback(const FGameplayAbilityTargetDataHandle& Data)
{
	UAbilitySystemComponent* ASC = AbilitySystemComponent.Get();
	if (!ASC)
	{
		return;
	}

	FScopedPredictionWindow ScopedPrediction(ASC, IsPredictingClient());

	if (IsPredictingClient())
	{
		if (!TargetActor->ShouldProduceTargetDataOnServer)
		{
			ASC->ServerSetReplicatedTargetDataCancelled(GetAbilitySpecHandle(), GetActivationPredictionKey(), ASC->ScopedPredictionKey );
		}
		else
		{
			// We aren't going to send the target data, but we will send a generic confirmed message.
			ASC->ServerSetReplicatedEvent(EAbilityGenericReplicatedEvent::GenericCancel, GetAbilitySpecHandle(), GetActivationPredictionKey(), ASC->ScopedPredictionKey);
		}
	}
	Cancelled.Broadcast(Data);
	EndTask();
}

AbilitySystemComponent.h

/** Replicates targeting data to the server */
UFUNCTION(Server, reliable, WithValidation)
void ServerSetReplicatedTargetData(FGameplayAbilitySpecHandle AbilityHandle, FPredictionKey AbilityOriginalPredictionKey, const FGameplayAbilityTargetDataHandle& ReplicatedTargetDataHandle, FGameplayTag ApplicationTag, FPredictionKey CurrentPredictionKey);

/** Replicates to the server that targeting has been cancelled */
UFUNCTION(Server, reliable, WithValidation)
void ServerSetReplicatedTargetDataCancelled(FGameplayAbilitySpecHandle AbilityHandle, FPredictionKey AbilityOriginalPredictionKey, FPredictionKey CurrentPredictionKey);

/** Returns TargetDataSet delegate for a given Ability/PredictionKey pair */
FAbilityTargetDataSetDelegate& AbilityTargetDataSetDelegate(FGameplayAbilitySpecHandle AbilityHandle, FPredictionKey AbilityOriginalPredictionKey);

/** Returns TargetData Cancelled delegate for a given Ability/PredictionKey pair */
FSimpleMulticastDelegate& AbilityTargetDataCancelledDelegate(FGameplayAbilitySpecHandle AbilityHandle, FPredictionKey AbilityOriginalPredictionKey);

void CallServerSetReplicatedTargetData(FGameplayAbilitySpecHandle AbilityHandle, FPredictionKey AbilityOriginalPredictionKey, const FGameplayAbilityTargetDataHandle& ReplicatedTargetDataHandle, FGameplayTag ApplicationTag, FPredictionKey CurrentPredictionKey);

/** Consumes cached TargetData from client (only TargetData) */
void ConsumeClientReplicatedTargetData(FGameplayAbilitySpecHandle AbilityHandle, FPredictionKey AbilityOriginalPredictionKey);

AbilitySystemComponent_Abilities.cpp

	void UAbilitySystemComponent::ServerSetReplicatedTargetData_Implementation(FGameplayAbilitySpecHandle AbilityHandle, FPredictionKey AbilityOriginalPredictionKey, const FGameplayAbilityTargetDataHandle& ReplicatedTargetDataHandle, FGameplayTag ApplicationTag, FPredictionKey CurrentPredictionKey)
{
	FScopedPredictionWindow ScopedPrediction(this, CurrentPredictionKey);

	// Always adds to cache to store the new data
	TSharedRef<FAbilityReplicatedDataCache> ReplicatedData = AbilityTargetDataMap.FindOrAdd(FGameplayAbilitySpecHandleAndPredictionKey(AbilityHandle, AbilityOriginalPredictionKey));

	if (ReplicatedData->TargetData.Num() > 0)
	{
		FGameplayAbilitySpec* Spec = FindAbilitySpecFromHandle(AbilityHandle);
		if (Spec && Spec->Ability)
		{
			// Can happen under normal circumstances if ServerForceClientTargetData is hit
			ABILITY_LOG(Display, TEXT("Ability %s is overriding pending replicated target data."), *Spec->Ability->GetName());
		}
	}

	ReplicatedData->TargetData = ReplicatedTargetDataHandle;
	ReplicatedData->ApplicationTag = ApplicationTag;
	ReplicatedData->bTargetConfirmed = true;
	ReplicatedData->bTargetCancelled = false;
	ReplicatedData->PredictionKey = CurrentPredictionKey;

	ReplicatedData->TargetSetDelegate.Broadcast(ReplicatedTargetDataHandle, ReplicatedData->ApplicationTag);
}

bool UAbilitySystemComponent::ServerSetReplicatedTargetData_Validate(FGameplayAbilitySpecHandle AbilityHandle, FPredictionKey AbilityOriginalPredictionKey, const FGameplayAbilityTargetDataHandle& ReplicatedTargetDataHandle, FGameplayTag ApplicationTag, FPredictionKey CurrentPredictionKey)
{
	// check the data coming from the client to ensure it's valid
	for (const TSharedPtr<FGameplayAbilityTargetData>& TgtData : ReplicatedTargetDataHandle.Data)
	{
		if (!ensure(TgtData.IsValid()))
		{
			return false;
		}
	}

	return true;
}

void UAbilitySystemComponent::ServerSetReplicatedTargetDataCancelled_Implementation(FGameplayAbilitySpecHandle AbilityHandle, FPredictionKey AbilityOriginalPredictionKey, FPredictionKey CurrentPredictionKey)
{
	FScopedPredictionWindow ScopedPrediction(this, CurrentPredictionKey);

	// Always adds to cache to store the new data
	TSharedRef<FAbilityReplicatedDataCache> ReplicatedData = AbilityTargetDataMap.FindOrAdd(FGameplayAbilitySpecHandleAndPredictionKey(AbilityHandle, AbilityOriginalPredictionKey));

	ReplicatedData->Reset();
	ReplicatedData->bTargetCancelled = true;
	ReplicatedData->PredictionKey = CurrentPredictionKey;
	ReplicatedData->TargetCancelledDelegate.Broadcast();
}

bool UAbilitySystemComponent::ServerSetReplicatedTargetDataCancelled_Validate(FGameplayAbilitySpecHandle AbilityHandle, FPredictionKey AbilityOriginalPredictionKey, FPredictionKey CurrentPredictionKey)
{
	return true;
}

FAbilityTargetDataSetDelegate& UAbilitySystemComponent::AbilityTargetDataSetDelegate(FGameplayAbilitySpecHandle AbilityHandle, FPredictionKey AbilityOriginalPredictionKey)
{
	return AbilityTargetDataMap.FindOrAdd(FGameplayAbilitySpecHandleAndPredictionKey(AbilityHandle, AbilityOriginalPredictionKey))->TargetSetDelegate;
}

FSimpleMulticastDelegate& UAbilitySystemComponent::AbilityTargetDataCancelledDelegate(FGameplayAbilitySpecHandle AbilityHandle, FPredictionKey AbilityOriginalPredictionKey)
{
	return AbilityTargetDataMap.FindOrAdd(FGameplayAbilitySpecHandleAndPredictionKey(AbilityHandle, AbilityOriginalPredictionKey))->TargetCancelledDelegate;
}

void UAbilitySystemComponent::ConsumeClientReplicatedTargetData(FGameplayAbilitySpecHandle AbilityHandle, FPredictionKey AbilityOriginalPredictionKey)
{
	TSharedPtr<FAbilityReplicatedDataCache> CachedData = AbilityTargetDataMap.Find(FGameplayAbilitySpecHandleAndPredictionKey(AbilityHandle, AbilityOriginalPredictionKey));
	if (CachedData.IsValid())
	{
		CachedData->TargetData.Clear();
		CachedData->bTargetConfirmed = false;
		CachedData->bTargetCancelled = false;
	}
}

void UAbilitySystemComponent::CallServerSetReplicatedTargetData(FGameplayAbilitySpecHandle AbilityHandle, FPredictionKey AbilityOriginalPredictionKey, const FGameplayAbilityTargetDataHandle& ReplicatedTargetDataHandle, FGameplayTag ApplicationTag, FPredictionKey CurrentPredictionKey)
{
	UE_CLOG(AbilitySystemLogServerRPCBatching, LogAbilitySystem, Display, TEXT("::CallServerSetReplicatedTargetData %s %s %s %s %s"), 
		*AbilityHandle.ToString(), *AbilityOriginalPredictionKey.ToString(), ReplicatedTargetDataHandle.IsValid(0) ? *ReplicatedTargetDataHandle.Get(0)->ToString() : TEXT("NULL"), *ApplicationTag.ToString(), *CurrentPredictionKey.ToString());

	/** Queue this call up if we are in  a batch window, otherwise just push it through now */
	if (FServerAbilityRPCBatch* ExistingBatchData = LocalServerAbilityRPCBatchData.FindByKey(AbilityHandle))
	{
		if (!ExistingBatchData->Started)
		{
			// A batch window was setup but we didn't see the normal try activate -> target data -> end path. So let this unbatched rpc through.
			FGameplayAbilitySpec* Spec = FindAbilitySpecFromHandle(AbilityHandle);
			UE_CLOG(AbilitySystemLogServerRPCBatching, LogAbilitySystem, Display, TEXT("::CallServerSetReplicatedTargetData called for ability (%s) when CallServerTryActivateAbility has not been called"), Spec ? *GetNameSafe(Spec->Ability) : TEXT("INVALID"));
			ServerSetReplicatedTargetData(AbilityHandle, AbilityOriginalPredictionKey, ReplicatedTargetDataHandle, ApplicationTag, CurrentPredictionKey);
			return;
		}

		if (ExistingBatchData->PredictionKey.IsValidKey() == false)
		{
			FGameplayAbilitySpec* Spec = FindAbilitySpecFromHandle(AbilityHandle);
			ABILITY_LOG(Warning, TEXT("::CallServerSetReplicatedTargetData called for ability (%s) when the prediction key is not valid."), Spec ? *GetNameSafe(Spec->Ability) : TEXT("INVALID"));
		}


		ExistingBatchData->TargetData = ReplicatedTargetDataHandle;
	}
	else
	{
		ServerSetReplicatedTargetData(AbilityHandle, AbilityOriginalPredictionKey, ReplicatedTargetDataHandle, ApplicationTag, CurrentPredictionKey);
	}

}

GameplayAbilityTargetActor.h

/** Replicated target data was received from a client. Possibly sanitize/verify. return true if data is good and we should broadcast it as valid data. */
virtual bool OnReplicatedTargetDataReceived(FGameplayAbilityTargetDataHandle& Data) const;

GameplayAbilityTargetActor.cpp

bool AGameplayAbilityTargetActor::OnReplicatedTargetDataReceived(FGameplayAbilityTargetDataHandle& Data) const
{
	return true;
}

Now that we know a bit about what functionality is provided to us by the gameplay ability system, let’s take a look at what I do to utilize it and properly replicate the client-side target data to the server:

To start, on ActivateAbility() I do the following, though the initial checkfor ActorInfo->IsNetAuthority() my be redundant since we also check IsLocallyControlled in blueprint on ActivateAbility:

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

	if(ActorInfo->IsNetAuthority() || HasAuthorityOrPredictionKey(ActorInfo, &ActivationInfo))
	{
		// Bind target data callback
		UAbilitySystemComponent* MyAbilityComponent = CurrentActorInfo->AbilitySystemComponent.Get();
		check(MyAbilityComponent);
		OnTargetDataReadyCallbackDelegateHandle = MyAbilityComponent->AbilityTargetDataSetDelegate(CurrentSpecHandle, CurrentActivationInfo.GetActivationPredictionKey()).AddUObject(this, &ThisClass::OnTargetDataReadyCallback);
		MyAbilityComponent->SetUserAbilityActivationInhibited(true);
	}
	
	Super::ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData);
}

The most important line being:

OnTargetDataReadyCallbackDelegateHandle = MyAbilityComponent->AbilityTargetDataSetDelegate(CurrentSpecHandle, CurrentActivationInfo.GetActivationPredictionKey()).AddUObject(this, &ThisClass::OnTargetDataReadyCallback);

This is where I am binding my ability function, OnTargetDataReadyCallback to the AbilityTargetDataSetDelegate; which means, when the target data is set, I will get my function called. The OnTargetDataReadyCallback looks like this:

/**

  • @brief Callback function invoked when the aim cast target data is ready to be processed.
  • This function is called when the aim cast target data is ready to be processed. It takes in the aim cast target data handle and the application tag.
  • @param InData The target data handle containing the aim cast target data.
  • @param ApplicationTag The tag of the ability application that caused the target data to be ready.
    */
    void OnTargetDataReadyCallback(const FGameplayAbilityTargetDataHandle& InData, FGameplayTag ApplicationTag);
void UGASCourseAimcastGameplayAbility::OnTargetDataReadyCallback(const FGameplayAbilityTargetDataHandle& InData,
	FGameplayTag ApplicationTag)
{
	UAbilitySystemComponent* MyAbilityComponent = CurrentActorInfo->AbilitySystemComponent.Get();
	check(MyAbilityComponent);

	if (const FGameplayAbilitySpec* AbilitySpec = MyAbilityComponent->FindAbilitySpecFromHandle(CurrentSpecHandle))
	{
		FScopedPredictionWindow	ScopedPrediction(MyAbilityComponent);

		// Take ownership of the target data to make sure no callbacks into game code invalidate it out from under us
		FGameplayAbilityTargetDataHandle LocalTargetDataHandle(MoveTemp(const_cast<FGameplayAbilityTargetDataHandle&>(InData)));

		const bool bShouldNotifyServer = CurrentActorInfo->IsLocallyControlled() && !CurrentActorInfo->IsNetAuthority();
		if (bShouldNotifyServer)
		{
			MyAbilityComponent->CallServerSetReplicatedTargetData(CurrentSpecHandle, CurrentActivationInfo.GetActivationPredictionKey(), LocalTargetDataHandle, ApplicationTag, MyAbilityComponent->ScopedPredictionKey);
		}

		const bool bIsTargetDataValid = true;

#if WITH_SERVER_CODE
		if (AController* Controller = GetControllerFromActorInfo())
		{
			if (Controller->GetLocalRole() == ROLE_Authority)
			{
				//TODO: Confirm target data somehow?
			}
		}

#endif //WITH_SERVER_CODE

		if(bIsTargetDataValid)
		{
			OnAimCastTargetDataReady(LocalTargetDataHandle);
		}
		else
		{
			K2_EndAbility();
		}
	}

	// We've processed the data
	MyAbilityComponent->ConsumeClientReplicatedTargetData(CurrentSpecHandle, CurrentActivationInfo.GetActivationPredictionKey());
	MyAbilityComponent->SetUserAbilityActivationInhibited(false);
}

It is also within the OnTargetDataReadyCallback that I do the following:

  • I have a WITH_SERVER_CODE check to handle whether or not the data is valid; as mentioned earlier, this doesn’t do anything just yet but this is the place where you could validate the target data by the server.
  • I call a blueprint implementable event, OnAimCastTargetDataReady, that I use in my ability blueprint to handle applying damage to the targets returned by the LocalTargetDataHandle. Below is the what the signature for the OnAimCastTargetDataReady event looks like; I also show OnAimCastTargetDataCancelled.

/**

  • @brief Event called when the aim cast target data is ready.
  • This event is blueprint implementable, allowing developers to define their own functionality in blueprint.
  • It is called when the aim cast target data is ready to be processed.
  • @param TargetData The target data handle containing the aim cast target data.
    */
    UFUNCTION(BlueprintImplementableEvent)
    void OnAimCastTargetDataReady(const FGameplayAbilityTargetDataHandle& TargetData);

/**

  • @brief Event called when the aim cast target data is cancelled.
  • This event is blueprint implementable, allowing developers to define their own functionality in blueprint.
  • It is called when the aim cast target data is cancelled.
    */
    UFUNCTION(BlueprintImplementableEvent)
    void OnAimCastTargetDataCancelled();

To also show what the target data cancelled workflow looks like, here is the OnTargetDataCancelledCallback function:

/**

  • @brief Callback function invoked when the aim cast target data is cancelled.
  • This function is called when the aim cast target data is cancelled. It takes in the target data handle as the parameter.
  • @param Data The handle containing the aim cast target data that was cancelled.
    */
    void OnTargetDataCancelledCallback(const FGameplayAbilityTargetDataHandle& Data);

The OnTargetDataCancelledCallback function right now looks almost identical to the OnTargetDataReadyCallback but only worries about clearing client replicated target data, ending the ability, and calling the blueprint implementable event OnAimCastTargetDataCancelled().

void UGASCourseAimcastGameplayAbility::OnTargetDataCancelledCallback(const FGameplayAbilityTargetDataHandle& Data)
{
	UAbilitySystemComponent* MyAbilityComponent = CurrentActorInfo->AbilitySystemComponent.Get();
	check(MyAbilityComponent);

	if(bSendTargetDataToServerOnCancelled)
	{
		if (const FGameplayAbilitySpec* AbilitySpec = MyAbilityComponent->FindAbilitySpecFromHandle(CurrentSpecHandle))
		{
			FScopedPredictionWindow	ScopedPrediction(MyAbilityComponent);

			// Take ownership of the target data to make sure no callbacks into game code invalidate it out from under us
			const FGameplayAbilityTargetDataHandle LocalTargetDataHandle(MoveTemp(const_cast<FGameplayAbilityTargetDataHandle&>(Data)));
			
			if (CurrentActorInfo->IsLocallyControlled() && !CurrentActorInfo->IsNetAuthority())
			{
				MyAbilityComponent->CallServerSetReplicatedTargetData(CurrentSpecHandle, CurrentActivationInfo.GetActivationPredictionKey(), LocalTargetDataHandle, FGameplayTag::EmptyTag, MyAbilityComponent->ScopedPredictionKey);
			}

			const bool bIsTargetDataValid = true;

#if WITH_SERVER_CODE
			if (AController* Controller = GetControllerFromActorInfo())
			{
				if (Controller->GetLocalRole() == ROLE_Authority)
				{
					//TODO: Confirm target data somehow?
				}
			}

#endif //WITH_SERVER_CODE
		}
	}
	else
	{
		OnAimCastTargetDataCancelled();
	}
	// We've processed the data
	MyAbilityComponent->ConsumeClientReplicatedTargetData(CurrentSpecHandle, CurrentActivationInfo.GetActivationPredictionKey());
	MyAbilityComponent->SetUserAbilityActivationInhibited(false);

	if(bEndAbilityOnTargetDataCancelled)
	{
		K2_EndAbility();
	}
}

This is what these blueprint implementable events look like inside of my aimcast ability blueprint:

Now, inside of my target actor base class, I have the following functions to use as a means to send confirmation or cancel decisions back to the calling/owning ability:

virtual void SendTargetDataBacktoServer(const FGameplayAbilityTargetDataHandle& InData, FGameplayTag ApplicationTag);

virtual void SendCancelledTargetDataBackToServer(const FGameplayAbilityTargetDataHandle& InData);

From my extended AGASCourseTargetActor_CameraTrace class, I override these functions and add the following logic:

void AGASCourseTargetActor_CameraTrace::SendTargetDataBacktoServer(const FGameplayAbilityTargetDataHandle& InData,
	FGameplayTag ApplicationTag)
{
	if(bHasDataBeenSentToServer)
	{
		return;
	}
	
	if(UGASCourseAimcastGameplayAbility* LocalAbility = Cast<UGASCourseAimcastGameplayAbility>(OwningAbility))
	{
		LocalAbility->OnTargetDataReadyCallback(InData, ApplicationTag);
		bHasDataBeenSentToServer = true;
	}
	Super::SendTargetDataBacktoServer(InData, ApplicationTag);
}

void AGASCourseTargetActor_CameraTrace::SendCancelledTargetDataBackToServer(
	const FGameplayAbilityTargetDataHandle& InData)
{
	if(bHasDataBeenSentToServer)
	{
		return;
	}
	
	if(UGASCourseAimcastGameplayAbility* LocalAbility = Cast<UGASCourseAimcastGameplayAbility>(OwningAbility))
	{
		LocalAbility->OnTargetDataCancelledCallback(InData);
		bHasDataBeenSentToServer = true;
	}
	Super::SendCancelledTargetDataBackToServer(InData);
}

These functions are what are responsible for notifying the server of whether we confirmed or cancelled our targeting, and to handle the approach replication from client to server, as well as calling the necessary delegates back to the ability performing the targeting. We call these functions inside of our child targeting class, AGASCourseTargetActor_CameraTrace, in the ConfirmTargeting() and CancelTargeting() functions respectively.

void AGASCourseTargetActor_CameraTrace::ConfirmTargeting()
{
	check(ShouldProduceTargetData());
	if (SourceActor)
	{
		const FVector Origin = PerformTrace(SourceActor).Location;
		const FGameplayAbilityTargetDataHandle Handle = MakeTargetData(PerformOverlap(Origin), Origin);
		TargetDataReadyDelegate.Broadcast(Handle);
		SendTargetDataBacktoServer(Handle, FGameplayTag());
		Super::ConfirmTargeting();
	}
}

void AGASCourseTargetActor_CameraTrace::CancelTargeting()
{
	if(SourceActor)
	{
		const FVector Origin = PerformTrace(SourceActor).Location;
		const FGameplayAbilityTargetDataHandle Handle = MakeTargetData(PerformOverlap(Origin), Origin);
		SendCancelledTargetDataBackToServer(Handle);
		Super::CancelTargeting();
	}
}

Here is the final result:


References:

LYRA

Sending Gameplay Ability Data From Client to Server - x157


Thank you for taking the time to read this post, and I hope you were able to take something away from it. Please let me know if I got any information wrong, or explained something incorrectly! Also add any thoughts or questions or code review feedback so we can all learn together :slight_smile:


Next Blog Post Topic:

Attribute Sets & Initialization

4 Likes