This is it! I've had a hard time finding any information on the internet to create your very own TargetData in GAS, but no more. In this post, we'll talk about what is TargetData and how to create your own. In a follow up post, I'll go a little more in depth on how to use your new TargetData within a GameplayAbility.
Disclaimer! This post assumes that you know the basics of UnrealEngine's Gameplay Ability System. If you don't know what a GameplayAbility or how to apply GameplayEffects, this tutorial will probably not going to make much sense. I'd suggest following Tranek's GAS Documentation on GitHub or watch the official Guided Tour of Gameplay Abilities by UnrealEngine.
What is TargetData?
TargetData is a generic structure used in GAS to pass data across the network and between AbilitySystemComponents. It allows the sharing of necessary information so that you don't centralize all behavior for a GameplayAbility and reuse certain behaviors while keeping them agnostic from the GameplayAbility that spawned them.
Out of the box, GAS offers the TargetData FGameplayAbilityTargetData. This structure is not meant to be used, but to be subclassed. GAS also offers three subclasses of FGameplayAbilityTargetData:
- FGameplayAbilityTargetData_ActorArray
- FGameplayAbilityTargetData_LocationInfo
- FGameplayAbilityTargetData_SingleTargetHit
As one can tell from the names, each TargetData is very specialized in the data they hold. This is by design to avoid serialization of superfluous data. TargetData is passed between systems through a TargetDataHandle, which has an array of multiple TargetData. This way, you can chain together any TargetData that is needed for different behaviors.
Creating a Custom TargetData Struct
In this tutorial, we'll sub-classing the FGameplayAbilityTarget and creating FGameplayAbilityTargetData_ImpulseInfo. Our game has multiple knock-direction effects (knock-back, knock-up, knock-from-point, etc.). To reduce code duplication, we'll be creating a new GameplayAbility GA_Event_KnockDirection which will be applied to an Actor that will be knocked around and pass the impulse information of that through a TargetData. For this, we'll create two separate pieces the TargetData object itself and a static utility to use and translate TargetData in Blueprint.
TargetData Object
GameplayAbilityTargetData_ImpulseInfo.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37 | USTRUCT(BlueprintType)
struct GASSHOOTER_API FGameplayAbilityTargetData_ImpulseInfo : public FGameplayAbilityTargetData
{
GENERATED_USTRUCT_BODY()
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = Targeting)
FVector Direction;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = Targeting)
float Magnitude;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = Targeting)
float Duration;
virtual FTransform GetEndPointTransform() const override;
virtual UScriptStruct* GetScriptStruct() const override
{
return FGameplayAbilityTargetData_ImpulseInfo::StaticStruct();
}
virtual FString ToString() const override
{
return TEXT("FGameplayAbilityTargetData_ImpulseInfo");
}
bool NetSerialize(FArchive& Ar, class UPackageMap* Map, bool& bOutSuccess);
};
template<>
struct TStructOpsTypeTraits<FGameplayAbilityTargetData_ImpulseInfo> : public TStructOpsTypeTraitsBase2<FGameplayAbilityTargetData_ImpulseInfo>
{
enum
{
WithNetSerializer = true // For now this is REQUIRED for FGameplayAbilityTargetDataHandle net serialization to work
};
}; |
Here we have the important pieces for our header. First thing to notice is lines 7 - 13. These are all the values we'll need to apply impulse to our actor. These values can be anything that is serializable. If you want to send data that is not readily serializable, you'll have to create your own serialization code to convert the data into serializable objects.
Next up is GetScriptStruct() (lines 17 - 20). This is used to identify what what type of struct this is. This is a great utility for when you have multiple TargetData chained in a TargetDataHandle. With this implemented, you can fetch the necessary TargetData for different GameplayAbilities. We will see this function in action in part 2, where we implement the GameplayAbility using this TargetData.
Lastly, we have our networking pieces. Line 27 has NetSerialize(...) which will be required to serialize the values of this struct. Lines 30 - 37 has boilerplate code to let the engine know that our struct has a NetSerialize(...) implementation. This code is needed if you want the data to be networked between server and client. If this code will only run on client, feel free to not implement serialization and set line 35 to false.
GameplayAbilityTargetData_ImpulseInfo.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 | FTransform FGameplayAbilityTargetData_ImpulseInfo::GetEndPointTransform() const
{
FTransform ImpulseInfo = FTransform(Direction.GetSafeNormal() * Magnitude);
ImpulseInfo.SetScale3D(FVector(
Duration,
0,
0));
return ImpulseInfo;
}
bool FGameplayAbilityTargetData_ImpulseInfo::NetSerialize(FArchive& Ar, UPackageMap* Map, bool& bOutSuccess)
{
Ar << Direction;
Ar << Magnitude;
Ar << Duration;
bOutSuccess = true;
return true;
} |
Implementation is quite short but a little bit obtuse due to holdover naming's of certain fields and functions. To see what I mean, let's look at a snippet of from GameplayAbilityTargetTypes:
GameplayAbilityTargetTypes.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29 | /** Override to true if GetOrigin will work */
virtual bool HasOrigin() const
{
return false;
}
/** Override to return an origin point, which may be derived from other data */
virtual FTransform GetOrigin() const
{
return FTransform::Identity;
}
/** Override to true if GetEndPoint/Transform will work */
virtual bool HasEndPoint() const
{
return false;
}
/** Override to return a target/end point */
virtual FVector GetEndPoint() const
{
return FVector::ZeroVector;
}
/** Override to return a transform, default will create one from just the location */
virtual FTransform GetEndPointTransform() const
{
return FTransform(GetEndPoint());
} |
Originally TargetData was hyper-specific in its use. As such, it used specific object types. Now that we're building on top of TargetData, we are required to fit our data into the fields provided by TargetData. In our case, we are putting the the direction and magnitude of our impulse into the the location field of the FTransform returned in GetEndPointTransform() (line 1 - 10 of GameplayAbilityTargetData_ImpulseInfo.cpp) and the duration will be placed inside the scale field of the FTransform, returned by the same function. One could go through the refactoring process of making these names more generic, but for our purposes we'll be creating a static utility component later on to translate this data for more generic use in Blueprints.
Another implementation required is NetSerialize(...). We're fortunate enough that all our data is serializable, but if you had an object that wasn't this is where you would call the appropriate serialization functions to get a serialized version of your objects and add it to FArchive.
And that's it! Now that we have created a custom TargetData, let's create a utility class with node implementations to use in Blueprint.
Creating a Utility Class to Use TargetData
At this point, you should be able to use the custom TargetData. However, it becomes unwieldy to use outside of C++, so let's create a UBluprintFunctionLibrary for your new TargetData. Below is a snippet from ImpulseTargetDataLibrary:
ImpulseTargetDataLibrary.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44 | FGameplayAbilityTargetDataHandle USWImpulseTargetDataLibrary::MakeImpulseTargetData(
const FVector Direction,
const float Magnitude,
const float Duration,
const bool IsAdditive)
{
FGameplayAbilityTargetData_ImpulseInfo* NewData =
new FGameplayAbilityTargetData_ImpulseInfo();
NewData->Direction = Direction.GetSafeNormal();
NewData->Magnitude = Magnitude;
NewData->Duration = Duration;
NewData->IsAdditive = IsAdditive;
FGameplayAbilityTargetDataHandle Handle;
Handle.Data.Add(TSharedPtr<FGameplayAbilityTargetData_ImpulseInfo>(NewData));
return Handle;
}
void USWImpulseTargetDataLibrary::BreakImpulseTargetData(
const FGameplayAbilityTargetDataHandle& TargetData,
const int TargetDataIndex,
FVector& Direction,
float& Magnitude,
float& Duration,
bool& IsAdditive)
{
if(!HasImpulseTargetData(TargetData))
{
return;
}
Direction = GetDirectionFromImpulseTargetData(TargetData, TargetDataIndex);
Magnitude = GetMagnitudeFromImpulseTargetData(TargetData, TargetDataIndex);
Duration = GetDurationFromImpulseTargetData(TargetData, TargetDataIndex);
IsAdditive = GetIsAdditiveFromImpulseTargetData(TargetData, TargetDataIndex);
}
FVector USWImpulseTargetDataLibrary::GetDirectionFromImpulseTargetData(
const FGameplayAbilityTargetDataHandle& TargetData,
const int TargetDataIndex)
{
return TargetData.Get(TargetDataIndex)->GetEndPointTransform().GetLocation().GetSafeNormal();
} |
MakeImpulseTargetData(...) and BreakImpulseTargetData(...) are the most important in this file, as they allow you to create a node to construct the TargetData and create a node to deconstruct the TargetData. There's a few functions omitted for spacing (lines 34-36), but they are rather similar to GetDirectionFromImpulseTargetData(...).
In Summary...
That's it! Now you know how to create your own TargetData and write up blueprint nodes for it. I'll be making another post soon to show how to use TargetData in general with blueprints. That tutorial should be generic for any TargetData, so it will be a good follow to see how this custom TargetData works in practice.
Until next time!
-Francisco