Unreal gameplay effect just got better — Introduction to gameplay effect components

I read through the gameplay effect components source code so you don’t have to.

Bobby Liu
9 min readMay 8, 2024

Introduction

Gameplay Effect Component, also known as GEComponents is a new component introduced in Unreal Engine 5.3, and is included and used by the gameplay effect class. The introduction of this component marks a pivotal shift away from the monolithic class design approach. GEComponents allow developers to customize gameplay effect behavior without inheriting and creating project specific classes from UGameplayEffect. The addition of GEComponents not only improves the flexibility of gameplay effects but also enhances class security by significantly reducing the need for class modification. In this article, I will explore the ins and outs of GEComponents. I will start with an overview of key gameplay effect terminologies, discussing in depth the differences between "added", "executed", and "applied" in the context of gameplay effect. Next, we will take a look of some key parts of the GEComponents, analyzing its source code within the engine, and examining each function in detail. Finally, we will review 2 different GEComponents sample provided by Epic Games to understand the component in action. Hopefully by the end of this article, you will have a decent understanding of this important yet overlooked change to the gameplay ability system, and gain the ability to leverage them effectively in your own projects.

Applying, adding and executing gameplay effects

To understand Gameplay Effect Components better, let’s start with the terminology that the GEComponents recommends we learn first. Let's take a look at the difference between adding, executing and applying a gameplay effect.

  • Applied: When a gameplay effect is applied to a target, the gameplay effect spec is successfully added to the target’s ability system component for processing. This is also when the requirements for the gameplay effect for both source and target is checked. This used to be handled by an array of UGameplayEffectCustomApplicationRequirement, but it has been deprecated in favour of the gameplay effect component. Note that even if the gameplay effect is successfully applied to the target, it does not guarantee that the effect will be executed.
  • Added: Added is reserved to describe the has duration and infinite type of gameplay effects. In this case, the target ability system component receives the gameplay effect spec, and then adds them to their active gameplay effect container. Note that this does not guarantee that the gameplay effect will apply the attribute and gameplay tag changes associated with it.
  • Executed: Finally, executed is used to refer when the actual changes to attributes and gameplay tags take affect. In the case of an instant gameplay effect, the gameplay effect is immediately executed. As for periodic gameplay effects, the effect will be executed multiple times based on the period. The execution of periodic gameplay effects used to be controlled by the ongoing tag requirements, although it has been deprecated since and replaced by the UTargetTagsGameplayEffectComponents instead.

Introduction to GEComponents

Now that we have a better understanding of the different stages of gameplay effect application, the purpose and the importance of the gameplay effect component becomes clearer. Prior to UE 5.3, any gameplay effect requirement checks are gameplay tags based. With GEComponents, Gameplay effect exposes the checking function, makes controlling the flow of gameplay effect easier and more straightforward. To create a custom gameplay effect component, we override specific functions that are called at different stages of a gameplay effect's activation. This customization process currently needs to be written in C++ and there are 5 key functions to override. Let's take a closer look at them.

  1. CanGameplayEffectApply: Called when a gameplay effect is about to be applied to the target, the return value of this function determines if the gameplay effect can be Applied. The gameplay effect will only be applied if all the GEComponents CanGameplayEffectApply function returns true.
  2. OnActiveGameplayEffectAdded: Called only when a periodic gameplay effect is added to target's active gameplay effect container for the first time (Or when the gameplay effect is replicated). The return value determines if the added gameplay effect should be active or not. Note that even if the function returns false, it doesn't necessarily mean the gameplay effect will be removed. It will remain within the active gameplay effect container until explicitly removed.
  3. OnGameplayEffectExecuted: Activated every time a gameplay effect is executed. For an instant type of gameplay effect, this will activate immediately after the gameplay effect is successfully applied to the target. And for periodic gameplay effect, this function will activate every time the change of attributes / gameplay tags takes place.
  4. OnGameplayEffectApplied: This function is called only once for both instant and periodic effects after it has been successfully applied to the target. This is the opportunity to perform any pre-execution requirement checks on the gameplay effect.
  5. OnGameplayEffectChanged: Finally, this functions is called when the data within the gameplay effect is changed. This could be happening during development, (Changes within the editor) or at run-time where the value of the gameplay effects are adjusted dynamically. I'm still not 100% sure what the use cases are for this function, but it seems like an opportunity to perform data and gameplay effect validation to ensure that the gameplay effect is error free.

Each gameplay effect can only have one of each type of the gameplay effect component and each type of component activates only once per gameplay effect instance.

Before we dive into some examples of GEComponents in action, let's take a quick look at the IsDataValid function. This function helps us understand at its base what makes a valid gameplay effect component.

const UGameplayEffect* Owner = GetOwner();
if (Owner)
{
const UGameplayEffectComponent* FirstComponentOfThisType = Owner->FindComponent(ThisClass);
if (!FirstComponentOfThisType)
{
Context.AddError(LOCTEXT("ComponentNotOnGE", "Component does not exist in its Owner's GEComponents Array"));
Result = EDataValidationResult::Invalid;
}
else if (FirstComponentOfThisType != this)
{
Context.AddError(FText::FormatOrdered(LOCTEXT("MultipleComponentsOfSameTypeError", "Two or more types of {0} exist on GE"), FText::FromString(ThisClass->GetName())));
Result = EDataValidationResult::Invalid;
}
}
else
{
FText ErrorText = FText::FormatOrdered(LOCTEXT("NoGEOwner", "{0} has invalid Outer: {1}"), FText::FromString(GetNameSafe(this)), FText::FromString(GetNameSafe(GetOuter())));
Context.AddError(ErrorText);

UE_LOG(LogGameplayEffects, Error, TEXT("%s"), *ErrorText.ToString());

Result = EDataValidationResult::Invalid;
}

The default implementation of IsDataValid essentially checks for 3 things:

  1. Valid outer: First, it checks if GEComponents has a valid outer, which is the gameplay effect class itself.
  2. Contains Components: Next, the function checks if the gameplay effect contains the GEComponents performing the data validation.
  3. Multiple Instance: Finally, we make sure that there is only 1 instance of this type of GEComponents in the gameplay effect. If the GEComponents is not the first one of its kind in the array, it throws an error and let's the developer know.
  4. And as side note, currently all the data needed for gameplay effect component is passed using the Gameplay effect spec struct. Modify the spec as needed for now, but there are plans to create its own struct for GEComponents in the future.

GEComponents examples

UChanceToApplyGameplayEffectComponent

Let’s start with a simple example to determine if a gameplay effect should apply based on the ChanceToApplyToTarget.

/** Probability that this gameplay effect will be applied to the target actor (0.0 for never, 1.0 for always) */
UPROPERTY(EditDefaultsOnly, Category = Application, meta = (GameplayAttribute = "True"))
FScalableFloat ChanceToApplyToTarget;

Let’s unpack the ChanceToApplyToTarget UPROPERTY. ChanceToApplyToTarget is a FScalableFloat variable, which not only allows the designer to manually put in a fixed float probability, but if we add a curve table to the variable, the probability can be adjusted automatically based on character level. Talk about flexibility!

To check if the gameplay effect will apply, we override the CanGameplayEffectAppbuly function

bool UChanceToApplyGameplayEffectComponent::CanGameplayEffectApply(const FActiveGameplayEffectsContainer& ActiveGEContainer, const FGameplayEffectSpec& GESpec) const
{
const FString ContextString = GESpec.Def->GetName();
const float CalculatedChanceToApplyToTarget = ChanceToApplyToTarget.GetValueAtLevel(GESpec.GetLevel(), &ContextString);

// check probability to apply
if ((CalculatedChanceToApplyToTarget < 1.f - SMALL_NUMBER) && (FMath::FRand() > CalculatedChanceToApplyToTarget))
{
return false;

return true;
}

This function is pretty straightforward. We fetched the adjusted probability based on the character’s level and the associated curve table. If the probability to apply is not 100%, the adjusted probability is compared against a randomly generated number and will return true — only if the random number is smaller.

UTargetTagRequirementsGameplayEffectComponents

Next up, the UTargetTagRequirementsGameplayEffectComponents . This is the first GEComponent I've used and piqued my curiosity. As the name suggests, this GEComponent specifies the tag requirement that the target must have in order to execute. Additionally, for periodic gameplay effect, this component also sets the required and blocked tags to allow the effect to continue to execute. This component uses 3 FGameplayTagRequirements to manage the tags needed to apply, maintain, and remove the gameplay effect.

/** Tag requirements the target must have for this GameplayEffect to be applied. This is pass/fail at the time of application. If fail, this GE fails to apply. */
UPROPERTY(EditDefaultsOnly, Category = Tags)
FGameplayTagRequirements ApplicationTagRequirements;

/** Once Applied, these tags requirements are used to determined if the GameplayEffect is "on" or "off". A GameplayEffect can be off and do nothing, but still applied. */
UPROPERTY(EditDefaultsOnly, Category = Tags)
FGameplayTagRequirements OngoingTagRequirements;

/** Tag requirements that if met will remove this effect. Also prevents effect application. */
UPROPERTY(EditDefaultsOnly, Category = Tags)
FGameplayTagRequirements RemovalTagRequirements;

Moving on to the functions in the gameplay effect component, this component overrides CanGameplayEffectApply , OnActiveGameplayEffectAdded and the IsDataValid function. Plus, it has a few internal callback functions -- specifically to handle removing the gameplay effect and responding to changes of the gameplay tags.

Let’s begin with the CanGameplayEffectApply function. This function is pretty self-explanatory. If the target is missing the necessary application requirement tags or contains any removal requirement tags, then the effect will simply not apply.

bool UTargetTagRequirementsGameplayEffectComponent::CanGameplayEffectApply(const FActiveGameplayEffectsContainer& ActiveGEContainer, const FGameplayEffectSpec& GESpec) const
{
FGameplayTagContainer Tags;
ActiveGEContainer.Owner->GetOwnedGameplayTags(Tags);
if (ApplicationTagRequirements.RequirementsMet(Tags) == false)
{
return false;
}

if (!RemovalTagRequirements.IsEmpty() && RemovalTagRequirements.RequirementsMet(Tags) == true)
{
return false;
}

return true;
}

Moving on to OnActiveGameplayEffectAdded. This function is jam packed with some of the best coding and optimization practices for C++. Let's start with a Lambda function that appends a TArrayto another TArray with no duplicates.

// Quick method of appending a TArray to another TArray with no duplicates.
auto AppendUnique = [](TArray<FGameplayTag>& Destination, const TArray<FGameplayTag>& Source)
{
// Make sure the array won't allocate during the loop
if (Destination.GetSlack() < Source.Num())
{
Destination.Reserve(Destination.Num() + Source.Num());
}
const TConstArrayView<FGameplayTag> PreModifiedDestinationView{ Destination.GetData(), Destination.Num() };
for (const FGameplayTag& Tag : Source)
{
if (!Algo::Find(PreModifiedDestinationView, Tag))
{
Destination.Emplace(Tag);
}
}
};

By allocating spaces for the new elements before entering the loop, this could prevent multiple re-allocations that may occur during the for loop. This is a significant performance boost — especially as the number of gameplay tags increases. Next, Using TConstArrayView ensures that the search operations over the destination array does not modify it -- providing both safety and efficiency through a read-only view of the array. Finally, the choice to use emplace over add avoids the overhead of creating temporary objects and unnecessary copy operations, where the FGameplayTag is directly constructed in the destination array.

// Add our tag requirements to the ASC's Callbacks map. This helps filter down the amount of callbacks we'll get due to tag changes
// (rather than registering for the one callback whenever any tag changes). We also need to keep track to remove those registered delegates in OnEffectRemoved.
TArray<TTuple<FGameplayTag, FDelegateHandle>> AllBoundEvents;
for (const FGameplayTag& Tag : GameplayTagsToBind)
{
FOnGameplayEffectTagCountChanged& OnTagEvent = ASC->RegisterGameplayTagEvent(Tag, EGameplayTagEventType::NewOrRemoved);
FDelegateHandle Handle = OnTagEvent.AddUObject(this, &UTargetTagRequirementsGameplayEffectComponent::OnTagChanged, ActiveGEHandle);
AllBoundEvents.Emplace(Tag, Handle);
}

// Now when this Effect is removed, we should remove all of our registered callbacks.
EventSet->OnEffectRemoved.AddUObject(this, &UTargetTagRequirementsGameplayEffectComponent::OnGameplayEffectRemoved, ASC, MoveTemp(AllBoundEvents));
}

There are two things I would like to focus on with this block of code. It effectively narrows the focus of callbacks and improves its efficiency by registering the callback on only the necessary tags. On the other hand, using MoveTemp to pass AllBoundEvents to its event handler improves the overall efficiency. This improves memory usage by transferring ownership instead of copying it to ensure efficient use of resources.

Summary

In our journey of exploring Unreal Engine 5.3’s GEComponent, we've navigated through some key gameplay effect terminologies, unpacked its source code and dived into some key examples of the flexibilities and efficiency of the components. I am excited to incorporate GEComponent more in my own development projects and I cannot wait to applying the new tricks and tips I've learned to write efficient and functional Unreal-specific C++ code. See you all in the next article!

References

  1. https://dev.epicgames.com/documentation/en-us/unreal-engine/gameplay-effects-for-the-gameplay-ability-system-in-unreal-engine?application_version=5.3

--

--

No responses yet