DEV Community

Cover image for Target Data in GAS
Marko Petrić
Marko Petrić

Posted on • Originally published at marko-ue.hashnode.dev

Target Data in GAS


What is target data and why should you use it

Target data is an ability task in GAS used for sending certain data to and from the server. It's used when you are doing some calculations on clients that you want the server to know, most commonly being hit results from various traces, which is the case I will cover in this post.

So for example, if you're doing a line trace for a hitscan weapon, you'd have the result of the trace go through target data so the server can use it authoritatively inside a locally predicted gameplay ability. Trying to use the hit result the client calculated without going through the server will result in broken behavior.

Additionally, this ability task automatically solves the problem of having to deal with if the target data will arrive first, or if the RPC will attempt to be called first.

The multiple types of target data built into GAS are all based on an FGameplayAbilityTargetData struct, which has different variations. You can also create a custom struct for additional functionality, which I will also cover in this post.


Using target data

When using target data, there are a few moving parts and boilerplate you must set up.

Binding and passing in the hit result

The first step in using target data is binding to a callback. It's advisable to bind as soon as possible, especially before any async operations or before producing hit results, to ensure you receive the target data properly.

The callback function you bind to needs to be a UFUNCTION() and have an FGameplayAbilityTargetDataHandle& parameter, and an FGameplayTag parameter.

// Callback function signature
UFUNCTION()
void OnTargetDataReceived(const FGameplayAbilityTargetDataHandle& DataHandle, FGameplayTag ApplicationTag);
Enter fullscreen mode Exit fullscreen mode

To bind to it, you get the ability system component, then access the AbilityTargetDataSetDelegate function and pass in your ability spec handle and the original ability prediction key, then use .AddObject to bind to your callback.

// Binding to your callback
UAbilitySystemComponent* ASC = GetAbilitySystemComponentFromActorInfo();
ASC->AbilityTargetDataSetDelegate(Handle, ActivationInfo.GetActivationPredictionKey())
    .AddUObject(this, &UUtility_Enforcer::OnTargetDataReceived);
Enter fullscreen mode Exit fullscreen mode

After this, you can calculate your hit result, and once you have it stored in an FHitResult, you need to add it to your data handle that the callback function uses to allow you to access information about the hit result.

To pass the hit result into the handle, you simply create a FGameplayAbilityTargetDataHandle variable, then call .Add on it, and pass in your grapple hit with the new keyword, and the specific target data struct you need to use in your case (in this case the SingleTargetHit variety).

// The hit result is passed into target data handle
FGameplayAbilityTargetDataHandle DataHandle;
DataHandle.Add(new FGameplayAbilityTargetData_SingleTargetHit(GrappleHit));
Enter fullscreen mode Exit fullscreen mode

Now you can call the ServerSetReplicatedTargetData function on the ASC which handles getting your data handle to the server. For the parameters, you need to pass in your ability's spec handle, the original prediction key, the data handle you created, a gameplay tag if you need it (or just pass in FGameplayTag()), and the current prediction key which you can get by taking the ScopedPredictionKey from the ability system component. This will make the callback fire on the server authoritatively when the server RPC arrives.

// Passing information into the function that gets the target data to the server
ASC->ServerSetReplicatedTargetData(
    Handle,
    ActivationInfo.GetActivationPredictionKey(),
    DataHandle,
    FGameplayTag(),
    ASC->ScopedPredictionKey
);
Enter fullscreen mode Exit fullscreen mode

Now you should also call the callback directly for client responsiveness.

OnTargetDataReceived(DataHandle, FGameplayTag());
Enter fullscreen mode Exit fullscreen mode

Note that the manual call does not do anything authoritatively, it's simply done so the client can immediately see the result of what they would do (and will do if the server allows it).


Accessing and using the passed in target data in the callback

Now that you passed in your target data, you can use it in the callback function to do many things authoritatively (as this function runs authoritatively only on the server).

The first step in the function is to always get the ability system component and call ConsumeReplicatedTargetData, passing in the current ability spec handle and the activation prediction key from the current activation info (which is the prediction key used when the ability was originally activated, not the current one). This function clears the stored target data on the ASC, so the same target data doesn't get processed again on subsequent ability activations.

It's also a good idea to return if the data handle is not valid.

GetAbilitySystemComponentFromActorInfo()->ConsumeClientReplicatedTargetData(
    GetCurrentAbilitySpecHandle(),
    GetCurrentActivationInfo().GetActivationPredictionKey()
);

if (!DataHandle.IsValid(0)) return;
Enter fullscreen mode Exit fullscreen mode

All hit result information is held in the data handle parameter, so to get your hit result, you call .Get on the data handle, passing in the index (since the data handle can hold multiple hit results, but in this case there will be only one so you can pass in 0. For more hit results, you simply iterate), and then you can call GetHitResult, at which point you will have access to all the information you'd have if using the hit result regularly, such as the impact point or something else. Now is the correct time to do server-only operations such as applying damage.

// Getting the hit result from the passed in data handle containing the hit result
const FHitResult* HitResult = DataHandle.Get(0)->GetHitResult();
Enter fullscreen mode Exit fullscreen mode

This covers the core usage of target data. I will now show you how you can expand on it by creating and using a custom SingleTargetHit struct to pass in additional information.

Why would you need a custom target data struct

Using the default target data struct lets you access everything from the hit result you passed into the data handle. This can often be enough, but in certain cases, you may also wish to pass in some custom data along with it that needs to be taken into consideration when doing the trace.

In my case, I have a shield actor, and if you fire through it and hit an enemy, you do bonus damage. I need to pass this along to the server so it has that information to apply bonus damage, since clients can't modify authoritative variables directly.


Creating and using the custom target data struct

Creating the struct

For the struct, you will first a header file where you will store it. In my case, this will be ComplyAbilityTypes, the same file where I store my custom gameplay effect context. The file must #pragma once, #include Abilities/GameplayAbilityTargetTypes.h, and the generated.h of the header.

To create it, you first need to put the USTRUCT() macro, then put the name for your struct and have it derive from the type of target data you want, which in this case will be the _SingleTargetHit. After that, you need a GENERATED_BODY() and then your custom variables. Next you need to handle some boilerplate. The first function you must override is GetScriptStruct, that just returns the StaticStruct. Next, you must override the Clone function which handles creating your custom struct, then passing in the hit result and your custom variables and returning it. Last up, you need to override NetSerialize which handles serializing your variables, which I cover in detail in the linked post.

USTRUCT()
struct FComplyGameplayAbilityTargetData_SingleTargetHit : public FGameplayAbilityTargetData_SingleTargetHit
{
    GENERATED_BODY()

    UPROPERTY()
    bool bPassedThroughShield = false;

    virtual UScriptStruct* GetScriptStruct() const override
    {
        return StaticStruct();
    }

    virtual FGameplayAbilityTargetData* Clone() const
    {
        FComplyGameplayAbilityTargetData_SingleTargetHit* NewData =
            new FComplyGameplayAbilityTargetData_SingleTargetHit();

        NewData->HitResult = HitResult;
        NewData->bPassedThroughShield = bPassedThroughShield;

        return NewData;
    }

    virtual bool NetSerialize(FArchive& Ar, class UPackageMap* Map, bool& bOutSuccess)
    {
        Super::NetSerialize(Ar, Map, bOutSuccess);
        Ar << bPassedThroughShield;
        bOutSuccess = true;
        return true;
    }
};
Enter fullscreen mode Exit fullscreen mode

Next you must implement the TStructOpsTypeTraits template struct, and set WithNetSerializer and WithCopy to true. I cover this struct in more detail here.

template<>
struct TStructOpsTypeTraits<FComplyGameplayAbilityTargetData_SingleTargetHit> : TStructOpsTypeTraitsBase2<FComplyGameplayAbilityTargetData_SingleTargetHit>
{
    enum
    {
        WithNetSerializer = true,
        WithCopy = true
    };
};
Enter fullscreen mode Exit fullscreen mode

Using the struct

Now wherever you are passing in _SingleTargetHit and need this extra information, you would instead use your custom struct, and set the variable directly.

// Creating the new struct and passing in information to it
FComplyGameplayAbilityTargetData_SingleTargetHit* Data =
    new FComplyGameplayAbilityTargetData_SingleTargetHit();
Data->HitResult = Hit;
Data->bPassedThroughShield = bPassedThroughShield;
Enter fullscreen mode Exit fullscreen mode

Now in your target data callback, you can do a static_cast to it in order to get the custom struct you just passed in, and then you can use the hit result and custom variables from it.

// Getting the custom struct by casting to it and using its custom variables
const FComplyGameplayAbilityTargetData_SingleTargetHit* CustomData = 
    static_cast<const FComplyGameplayAbilityTargetData_SingleTargetHit*>(Data.Get());

if (CustomData)
{
    bPassedThroughShield = CustomData->bPassedThroughShield; 
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Target data is often a necessity when working with GAS in multiplayer projects. It's almost always the best way to get hit results from the client to the server. This post should give you enough knowledge to be able to use them effectively and extend them further by going beyond the basics.

If you have any questions or feedback, feel free to contact me on LinkedIn, or email me: petric.marko04@gmail.com

Feel free to also check out my website where I have everything in one place, plus additional content!

Top comments (0)