Creating your own Gameplay Effect Context.
Updated for UE 5.8: the NetSerialize sample now builds on the engine’s own
FGameplayEffectContext::NetSerialize, the Ability System Globals class is now set in Project Settings, and the accessor examples were fixed.
There could be many reasons you want to create your own Effect Context within the Gameplay Ability System. A few reasons:
-
Passing in info about a specific thing (like if a damage effect was fatal, was critical, etc).
-
Passing around some kind of level for a specific weapon/item that applied that effect.
-
Passing around an ID for say a shotgun cartridge for reproducing hits on simulated proxies locally.
There are many other things you can do, but we will use the above examples as a basis for our custom Gameplay Effect Context. Before you get started, you should create your own game specific AbilitySystemGlobals class, link here: /blog/creating-and-setting-up-a-custom-uabilitysystemglobals-class/.
The context struct
First you will need a cpp and header file to hold the new struct. I recommend an AbilityTypes.cpp and AbilityTypes.h, for example I have KaosAbilityTypes.cpp and KaosAbilityTypes.h. Now we can derive from FGameplayEffectContext and add our own custom stuff.
/** Gameplay effect context carrying game specific hit information. */
USTRUCT(BlueprintType)
struct FKaosGameplayEffectContext : public FGameplayEffectContext
{
GENERATED_BODY()
public:
bool IsFatalHit() const { return bIsFatalHit; }
bool IsCriticalHit() const { return bIsCriticalHit; }
int32 GetCartridgeID() const { return CartridgeID; }
float GetSourceLevel() const { return SourceLevel; }
void SetIsFatalHit(bool bInIsFatalHit) { bIsFatalHit = bInIsFatalHit; }
void SetIsCriticalHit(bool bInIsCriticalHit) { bIsCriticalHit = bInIsCriticalHit; }
void SetCartridgeID(int32 InID) { CartridgeID = InID; }
void SetSourceLevel(float InLevel) { SourceLevel = InLevel; }
/** Returns the actual struct used for serialization, subclasses must override this! */
virtual UScriptStruct* GetScriptStruct() const override
{
return StaticStruct();
}
/** Creates a copy of this context, used to duplicate for later modifications. */
virtual FKaosGameplayEffectContext* Duplicate() const override
{
FKaosGameplayEffectContext* NewContext = new FKaosGameplayEffectContext();
*NewContext = *this;
if (GetHitResult())
{
// Does a deep copy of the hit result
NewContext->AddHitResult(*GetHitResult(), true);
}
return NewContext;
}
virtual bool NetSerialize(FArchive& Ar, UPackageMap* Map, bool& bOutSuccess) override;
protected:
UPROPERTY()
bool bIsFatalHit = false;
UPROPERTY()
bool bIsCriticalHit = false;
UPROPERTY()
int32 CartridgeID = 0;
UPROPERTY()
float SourceLevel = 0.f;
};
template <>
struct TStructOpsTypeTraits<FKaosGameplayEffectContext> : public TStructOpsTypeTraitsBase2<FKaosGameplayEffectContext>
{
enum
{
WithNetSerializer = true,
WithCopy = true // Necessary so that TSharedPtr<FHitResult> Data is copied around
};
};
We now have a few extra things in our subclass of the Effect Context: bIsFatalHit, bIsCriticalHit, CartridgeID and SourceLevel. You can have as many as you need. The *NewContext = *this copy in Duplicate already copies the base members (instigator, actors, etc.), so it doesn’t need anything extra, apart from the deep copy of the hit result which is a TSharedPtr and would otherwise be shared. Also note the base context already has an AbilityLevel (GetAbilityLevel()), so you may not need a separate SourceLevel at all; I’m keeping it here just as an example of extra data.
Replicating it
Now we get to the fun part, which is where we need to handle replicating these properties. We do this by overriding the NetSerialize function.
In my original version of this post I copied the whole engine implementation of NetSerialize into my struct and appended extra bits. That is fragile: the engine’s version has changed over the years (it now only writes the instigator and effect causer when the context says they should replicate, for example, and it has a matching Iris net serializer that has to be kept in sync), and my copy fell out of date. The much simpler and safer approach is to call the base implementation first and then serialize only our own data after it:
bool FKaosGameplayEffectContext::NetSerialize(FArchive& Ar, UPackageMap* Map, bool& bOutSuccess)
{
// Serialize everything the engine's context knows about first.
Super::NetSerialize(Ar, Map, bOutSuccess);
// Then our own data. One bit per flag, and the ints/floats are only sent when they hold something.
enum ERepFlag
{
REP_IsFatalHit,
REP_IsCriticalHit,
REP_CartridgeID,
REP_SourceLevel,
REP_MAX
};
uint8 RepBits = 0;
if (Ar.IsSaving())
{
if (bIsFatalHit)
{
RepBits |= 1 << REP_IsFatalHit;
}
if (bIsCriticalHit)
{
RepBits |= 1 << REP_IsCriticalHit;
}
if (CartridgeID > 0)
{
RepBits |= 1 << REP_CartridgeID;
}
if (SourceLevel > 0.f)
{
RepBits |= 1 << REP_SourceLevel;
}
}
Ar.SerializeBits(&RepBits, REP_MAX);
bIsFatalHit = (RepBits & (1 << REP_IsFatalHit)) != 0;
bIsCriticalHit = (RepBits & (1 << REP_IsCriticalHit)) != 0;
if (RepBits & (1 << REP_CartridgeID))
{
Ar << CartridgeID;
}
else if (Ar.IsLoading())
{
CartridgeID = 0;
}
if (RepBits & (1 << REP_SourceLevel))
{
Ar << SourceLevel;
}
else if (Ar.IsLoading())
{
SourceLevel = 0.f;
}
bOutSuccess = true;
return true;
}
Super is the typedef GENERATED_BODY() gives you for the parent struct. A few things I fixed compared to my original sample: the flags now live in a uint8 that matches the number of bits written (the old one used a uint16 with SerializeBits on the address of it, which is only right on little endian), a typo’d REP_CartrideID that wouldn’t compile is gone, and values are reset when loading a context that didn’t send them, otherwise a reused context object could keep stale data.
The general gist is: we want to serialize only the data that is actually set, to keep bandwidth down. Bit-packing bools like this is cheap, and it means a context with no extra data only costs a handful of bits on top of the base context.
Telling the engine to use it
With the above, we can tell our AbilitySystemGlobals we want to use our new Effect Context. We do this by overriding
/** Allocates our game specific effect context. */
virtual FGameplayEffectContext* AllocGameplayEffectContext() const override;
in our game specific AbilitySystemGlobals header, and defining it as:
FGameplayEffectContext* UKaosAbilitySystemGlobals::AllocGameplayEffectContext() const
{
return new FKaosGameplayEffectContext();
}
in the corresponding .cpp file.
Your globals class has to actually be the one in use. In 5.8 you set this in Project Settings > Game > Gameplay Abilities Settings > Ability System Globals Class (it lives in UGameplayAbilitiesDeveloperSettings, the old AbilitySystemGlobalsClassName entry in DefaultGame.ini under AbilitySystemGlobals is deprecated since 5.5). That setting requires an editor restart to take effect. After that the engine will allocate our Effect Context whenever it creates one.
Reading the values
Now you may ask, how do I access or set these values? The easiest way to access them is to make some static functions inside a blueprint function library class. An example:
/** Returns true if the effect context marks the hit as fatal. */
UFUNCTION(BlueprintPure, Category = "KaosAbilityLibrary|Effects")
static bool IsFatalHit(const FGameplayEffectContextHandle& EffectContext);
bool UKaosAbilityLibrary::IsFatalHit(const FGameplayEffectContextHandle& EffectContext)
{
if (const FGameplayEffectContext* BaseContext = EffectContext.Get())
{
// Make sure it really is our type before casting, other systems can create plain contexts.
if (BaseContext->GetScriptStruct()->IsChildOf(FKaosGameplayEffectContext::StaticStruct()))
{
return static_cast<const FKaosGameplayEffectContext*>(BaseContext)->IsFatalHit();
}
}
return false;
}
Writing the values
Setting is a tad more tricky, and I don’t recommend exposing the setters to Blueprint, keep that in native code. But you are more than welcome to if you want.
To access the Gameplay Effect Context from an Execution Calculation class, you can do:
FGameplayEffectSpec* MutableSpec = ExecutionParams.GetOwningSpecForPreExecuteMod();
FGameplayEffectContext* BaseContext = MutableSpec->GetContext().Get();
if (BaseContext && BaseContext->GetScriptStruct()->IsChildOf(FKaosGameplayEffectContext::StaticStruct()))
{
static_cast<FKaosGameplayEffectContext*>(BaseContext)->SetIsCriticalHit(true);
}
GetOwningSpecForPreExecuteMod gives you a non-const spec (the engine’s own comment says to be careful with it, especially when modifying after attribute capture), and through it a mutable context you can call your setters on. You can also read/mutate the context in your attribute sets, for example in PostGameplayEffectExecute:
FGameplayEffectContext* BaseContext = Data.EffectSpec.GetContext().Get();
if (BaseContext && BaseContext->GetScriptStruct()->IsChildOf(FKaosGameplayEffectContext::StaticStruct()))
{
FKaosGameplayEffectContext* KaosContext = static_cast<FKaosGameplayEffectContext*>(BaseContext);
// ...
}
Basically anywhere you can get hold of an effect spec handle or spec, you can get access to your Effect Context.
Gotchas
- The context is copied when specs are copied (
Duplicateis what does this), so any new data you add must survive*NewContext = *this(plain data does, raw pointers you own would not). - Anything you set only on the server only reaches clients if it is in
NetSerializeand the context is actually replicated in the case you care about (for example via a Gameplay Cue’s parameters or replicated target data). - The engine’s own
NetSerializewarns that any change to it also needs to be done in its Iris net serializer. I haven’t tested a subclassed context under Iris, so if you use Iris replication, test that your extra data makes it across.
Hope this helps people. If you need more clarity or more information, feel free to contact me. Contact details can be found in the Contact page.