Creating your own Gameplay Effect Context.

There could be many reasons you want to create your own Effect Context within the Gameplay Ability System. I have listed a few reasons below. Before you get started, you should create your own game specific AbilitySystemGlobals class, link here: https://www.thegames.dev/?p=52. So here are the 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 GameplayEffectContext.

First you will need a cpp and header file to hold the new struct. I recommend making 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.

USTRUCT(BlueprintType)
struct FKaosGameplayEffectContext : public FGameplayEffectContext
{
	GENERATED_BODY()
public:
	bool IsFatalHit() const { return bIsFatalHit; }
	bool IsCriticalHit() const { return bIsCriticalHit; }
	float 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; }

protected:
	UPROPERTY()
	bool bIsFatalHit;

	UPROPERTY()
	bool bIsCriticalHit;

	UPROPERTY()
	int32 CartridgeID;

	UPROPERTY()
	float SourceLevel;

public:
	/** 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;
		NewContext->AddActors(Actors);
		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;
};

template <>
struct TStructOpsTypeTraits<FKaosGameplayEffectContext> : public TStructOpsTypeTraitsBase2<FKaosGameplayEffectContext>
{
	enum
	{
		WithNetSerializer = true,
		WithCopy = true // Necessary so that TSharedPtr<FHitResult> Data is copied around
	};
};

As you can see, we now have a few things in our special subclass of the EffectContext, namely: bIsFatalHit, bIsCriticalHit, CartridgeID and SourceLevel. You can have many things that you might need to pass around.

Now we get to the fun part, and this is we need to handle replicating these properties. We do this by overriding, NetSerialize function.

bool FKaosGameplayEffectContext::NetSerialize(FArchive& Ar, UPackageMap* Map, bool& bOutSuccess)
{
 enum RepFlag
	{
		REP_Instigator,
		REP_EffectCauser,
		REP_AbilityCDO,
		REP_SourceObject,
		REP_Actors,
		REP_HitResult,
		REP_WorldOrigin,
		REP_IsFatalHit,
		REP_CartridgeID,
		REP_IsCriticalHit,
		REP_SourceLevel,
		REP_MAX
	};
	
	uint16 RepBits = 0;
	if (Ar.IsSaving())
	{
		if (Instigator.IsValid())
		{
			RepBits |= 1 << REP_Instigator;
		}
		if (EffectCauser.IsValid())
		{
			RepBits |= 1 << REP_EffectCauser;
		}
		if (AbilityCDO.IsValid())
		{
			RepBits |= 1 << REP_AbilityCDO;
		}
		if (bReplicateSourceObject && SourceObject.IsValid())
		{
			RepBits |= 1 << REP_SourceObject;
		}
		if (Actors.Num() > 0)
		{
			RepBits |= 1 << REP_Actors;
		}
		if (HitResult.IsValid())
		{
			RepBits |= 1 << REP_HitResult;
		}
		if (bHasWorldOrigin)
		{
			RepBits |= 1 << REP_WorldOrigin;
		}
		if (bIsFatalHit)
		{
			RepBits |= 1 << REP_IsFatalHit;
		}
		if (bIsCriticalHit)
		{
			RepBits |= 1 << REP_IsCriticalHit;
		}
		if (SourceLevel > 0)
		{
			RepBits |= 1 << REP_SourceLevel;
		}
		if (CartridgeID > 0)
		{
			RepBits |= 1 << REP_CartrideID;
		}
	}

	Ar.SerializeBits(&RepBits, REP_MAX);

	if (RepBits & (1 << REP_Instigator))
	{
		Ar << Instigator;
	}
	if (RepBits & (1 << REP_EffectCauser))
	{
		Ar << EffectCauser;
	}
	if (RepBits & (1 << REP_AbilityCDO))
	{
		Ar << AbilityCDO;
	}
	if (RepBits & (1 << REP_SourceObject))
	{
		Ar << SourceObject;
	}
	if (RepBits & (1 << REP_Actors))
	{
		SafeNetSerializeTArray_Default<31>(Ar, Actors);
	}
	if (RepBits & (1 << REP_HitResult))
	{
		if (Ar.IsLoading())
		{
			if (!HitResult.IsValid())
			{
				HitResult = TSharedPtr<FHitResult>(new FHitResult());
			}
		}
		HitResult->NetSerialize(Ar, Map, bOutSuccess);
	}
	if (RepBits & (1 << REP_WorldOrigin))
	{
		Ar << WorldOrigin;
		bHasWorldOrigin = true;
	}
	else
	{
		bHasWorldOrigin = false;
	}
	if (RepBits & (1 << REP_IsFatalHit))
	{
		Ar << bIsFatalHit;
	}
	if (RepBits & (1 << REP_IsCriticalHit))
	{
		Ar << bIsCriticalHit;
	}
	if (RepBits & (1 << REP_SourceLevel))
	{
		Ar << SourceLevel;
	}
	if (RepBits & (1 << REP_CartrideID))
	{
		Ar << CartridgeID;
	}

	if (Ar.IsLoading())
	{
		AddInstigator(Instigator.Get(), EffectCauser.Get()); // Just to initialize InstigatorAbilitySystemComponent
	}	
	
	bOutSuccess = true;
	return true;
}

Phew that is a lot of stuff. But the general gist is, we want to serialise the bits for replication. If you want a more detailed description on the above, then let me know.

With the above, we can now tell our AbilitySystemGlobals, we want to use our new Effect Context. We do this by overriding

	virtual FGameplayEffectContext* AllocGameplayEffectContext() const override;

in our game specific AbilitySystemGlobals header, and defining that as:

FGameplayEffectContext* UKaosAbilitySystemGlobals::AllocGameplayEffectContext() const
{
	return new FKaosGameplayEffectContext();
}

in the corresponding .cpp file.

Restarting the editor will now have the engine using our Effect Context.

Now you may ask, how do i access or set these values?. Well that is a good question, and i will explain below, the easiest way to access them is to make some static functions inside a blueprint function library class. Example of such static function is listed below:

	UFUNCTION(BlueprintPure, Category = "KaosAbilityLibrary|Effects")
	static bool IsFatalHit(const FGameplayEffectContextHandle& EffectContext);
bool UKaosAbilityLibrary::IsFatalHit(const FGameplayEffectContextHandle& EffectContext)
{
	const FKaosGameplayEffectContext* KaosEffectContext = static_cast<const FKaosGameplayEffectContext*>(EffectContext.Get());
	if (KaosEffectContext)
	{
		return KaosEffectContext->IsFatalHit();
	}
	return false;
}

Now this is fine for accessing, but what about setting? Well this is a tad more tricky, and i don’t recommend exposing these to Blueprint, and keeping this stuff in native code. But you are more than welcome to if you want.

So to access the Gameplay Effect Context from an Execution Calculation class, you can do :

FGameplayEffectSpec* MutableSpec = ExecutionParams.GetOwningSpecForPreExecuteMod();
FKaosGameplayEffectContext* Context = static_cast<FKaosGameplayEffectContext*>(MutableSpec->GetContext().Get());
Context->SetIsCriticalHit(true);

This will give you a mutable context in which you can now freely call your set functions on the Effect Context, like i have shown above. You can also mutate/read from the context in your attribute sets, like so:

	FKaosGameplayEffectContext* Context = static_cast<FKaosGameplayEffectContext*>(Data.GetContext().Get);

Basically anywhere you can get hold of an EffectSpecHandle or EffectSpec, you can get access to your Effect Context.

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.