Skip to content

My understanding of Unreal Engine 4's GameplayAbilitySystem plugin with a simple multiplayer sample project.

License

Notifications You must be signed in to change notification settings

Taurenzhi/GASDocumentation

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

GASDocumentation

My understanding of Unreal Engine 4's GameplayAbilitySystem plugin (GAS) with a simple multiplayer sample project. This is not official documentation and neither this project nor myself are affiliated with Epic Games. I make no guarantee for the accuracy of this information.

The goal of this documentation is to explain the major concepts and classes in GAS and provide some additional commentary based on my experience with it. There is a lot of 'tribal knowledge' of GAS among users in the community and I aim to share all of mine here.

The Sample Project and documentation are current with Unreal Engine 4.26. There are branches of this documentation for older versions of Unreal Engine, but they are no longer supported and are liable to have bugs or out of date information.

GASShooter is a sister Sample Project demonstrating advanced techniques with GAS for a multiplayer FPS/TPS.

The best documentation will always be the plugin source code.

Table of Contents

  1. Intro to the GameplayAbilitySystem Plugin
  2. Sample Project
  3. Setting Up a Project Using GAS
  4. Concepts
    4.1 Ability System Component
          4.1.1 Replication Mode
          4.1.2 Setup and Initialization
    4.2 Gameplay Tags
          4.2.1 Responding to Changes in Gameplay Tags
    4.3 Attributes
          4.3.1 Attribute Definition
          4.3.2 BaseValue vs CurrentValue
          4.3.3 Meta Attributes
          4.3.4 Responding to Attribute Changes
          4.3.5 Derived Attributes
    4.4 Attribute Set
          4.4.1 Attribute Set Definition
          4.4.2 Attribute Set Design
                4.4.2.1 Subcomponents with Individual Attributes
                4.4.2.2 Adding and Removing AttributeSets at Runtime
                4.4.2.3 Item Attributes (Weapon Ammo)
                      4.4.2.3.1 Plain Floats on the Item
                      4.4.2.3.2 AttributeSet on the Item
                      4.4.2.3.3 ASC on the Item
          4.4.3 Defining Attributes
          4.4.4 Initializing Attributes
          4.4.5 PreAttributeChange()
          4.4.6 PostGameplayEffectExecute()
          4.4.7 OnAttributeAggregatorCreated()
    4.5 Gameplay Effects
          4.5.1 Gameplay Effect Definition
          4.5.2 Applying Gameplay Effects
          4.5.3 Removing Gameplay Effects
          4.5.4 Gameplay Effect Modifiers
                4.5.4.1 Multiply and Divide Modifiers
                4.5.4.2 Gameplay Tags on Modifiers
          4.5.5 Stacking Gameplay Effects
          4.5.6 Granted Abilities
          4.5.7 Gameplay Effect Tags
          4.5.8 Immunity
          4.5.9 Gameplay Effect Spec
                4.5.9.1 SetByCallers
          4.5.10 Gameplay Effect Context
          4.5.11 Modifier Magnitude Calculation
          4.5.12 Gameplay Effect Execution Calculation
                4.5.12.1 Sending Data to Execution Calculations
                      4.5.12.1.1 SetByCaller
                      4.5.12.1.2 Backing Data Attribute Calculation Modifier
                      4.5.12.1.3 Backing Data Temporary Variable Calculation Modifier
                      4.5.12.1.4 Gameplay Effect Context
          4.5.13 Custom Application Requirement
          4.5.14 Cost Gameplay Effect
          4.5.15 Cooldown Gameplay Effect
                4.5.15.1 Get the Cooldown Gameplay Effect's Remaining Time
                4.5.15.2 Listening for Cooldown Begin and End
                4.5.15.3 Predicting Cooldowns
          4.5.16 Changing Active Gameplay Effect Duration
          4.5.17 Creating Dynamic Gameplay Effects at Runtime
          4.5.18 Gameplay Effect Containers
    4.6 Gameplay Abilities
          4.6.1 Gameplay Ability Definition
                4.6.1.1 Replication Policy
                4.6.1.2 Server Respects Remote Ability Cancellation
                4.6.1.3 Replicate Input Directly
          4.6.2 Binding Input to the ASC
                4.6.2.1 Binding to Input without Activating Abilities
          4.6.3 Granting Abilities
          4.6.4 Activating Abilities
                4.6.4.1 Passive Abilities
          4.6.5 Canceling Abilities
          4.6.6 Getting Active Abilities
          4.6.7 Instancing Policy
          4.6.8 Net Execution Policy
          4.6.9 Ability Tags
          4.6.10 Gameplay Ability Spec
          4.6.11 Passing Data to Abilities
          4.6.12 Ability Cost and Cooldown
          4.6.13 Leveling Up Abilities
          4.6.14 Ability Sets
          4.6.15 Ability Batching
          4.6.16 Net Security Policy
    4.7 Ability Tasks
          4.7.1 Ability Task Definition
          4.7.2 Custom Ability Tasks
          4.7.3 Using Ability Tasks
          4.7.4 Root Motion Source Ability Tasks
    4.8 Gameplay Cues
          4.8.1 Gameplay Cue Definition
          4.8.2 Triggering Gameplay Cues
          4.8.3 Local Gameplay Cues
          4.8.4 Gameplay Cue Parameters
          4.8.5 Gameplay Cue Manager
          4.8.6 Prevent Gameplay Cues from Firing
          4.8.7 Gameplay Cue Batching
                4.8.7.1 Manual RPC
                4.8.7.2 Multiple GCs on one GE
          4.8.8 Gameplay Cue Events
          4.8.9 Gameplay Cue Reliability
    4.9 Ability System Globals
          4.9.1 InitGlobalData()
    4.10 Prediction
          4.10.1 Prediction Key
          4.10.2 Creating New Prediction Windows in Abilities
          4.10.3 Predictively Spawning Actors
          4.10.4 Future of Prediction in GAS
          4.10.5 Network Prediction Plugin
    4.11 Targeting
          4.11.1 Target Data
          4.11.2 Target Actors
          4.11.3 Target Data Filters
          4.11.4 Gameplay Ability World Reticles
          4.11.5 Gameplay Effect Containers Targeting
  5. Commonly Implemented Abilities and Effects
    5.1 Stun
    5.2 Sprint
    5.3 Aim Down Sights
    5.4 Lifesteal
    5.5 Generating a Random Number on Client and Server
    5.6 Critical Hits
    5.7 Non-Stacking Gameplay Effects but Only the Greatest Magnitude Actually Affects the Target
    5.8 Generate Target Data While Game is Paused
    5.9 One Button Interaction System
  6. Debugging GAS
    6.1 showdebug abilitysystem
    6.2 Gameplay Debugger
    6.3 GAS Logging
  7. Optimizations
    7.1 Ability Batching
    7.2 Gameplay Cue Batching
    7.3 AbilitySystemComponent Replication Mode
    7.4 Attribute Proxy Replication
    7.5 ASC Lazy Loading
  8. Quality of Life Suggestions
    8.1 Gameplay Effect Containers
    8.2 Blueprint AsyncTasks to Bind to ASC Delegates
  9. Troubleshooting
    9.1 LogAbilitySystem: Warning: Can't activate LocalOnly or LocalPredicted ability %s when not local!
    9.2 ScriptStructCache errors
    9.3 Animation Montages are not replicating to clients
    9.4 Duplicating Blueprint Actors is setting AttributeSets to nullptr
  10. Common GAS Acronyms
  11. Other Resources
  12. GAS Changelog

1. Intro to the GameplayAbilitySystem Plugin

From the Official Documentation:

The Gameplay Ability System is a highly-flexible framework for building abilities and attributes of the type you might find in an RPG or MOBA title. You can build actions or passive abilities for the characters in your games to use, status effects that can build up or wear down various attributes as a result of these actions, implement "cooldown" timers or resource costs to regulate the usage of these actions, change the level of the ability and its effects at each level, activate particle or sound effects, and more. Put simply, this system can help you to design, implement, and efficiently network in-game abilities as simple as jumping or as complex as your favorite character's ability set in any modern RPG or MOBA title.

The GameplayAbilitySystem plugin is developed by Epic Games and comes with Unreal Engine 4 (UE4). It has been battle tested in AAA commercial games such as Paragon and Fortnite among others.

The plugin provides an out-of-the-box solution in single and multiplayer games for:

  • Implementing level-based character abilities or skills with optional costs and cooldowns (GameplayAbilities)
  • Manipulating numerical Attributes belonging to actors (Attributes)
  • Applying status effects to actors (GameplayEffects)
  • Applying GameplayTags to actors (GameplayTags)
  • Spawning visual or sound effects (GameplayCues)
  • Replication of everything mentioned above

In multiplayer games, GAS provides support for client-side prediction of:

  • Ability activation
  • Playing animation montages
  • Changes to Attributes
  • Applying GameplayTags
  • Spawning GameplayCues
  • Movement via RootMotionSource functions connected to the CharacterMovementComponent.

GAS must be set up in C++, but GameplayAbilities and GameplayEffects can be created in Blueprint by the designers.

Current issues with GAS:

  • GameplayEffect latency reconciliation (can't predict ability cooldowns resulting in players with higher latencies having lower rate of fire for low cooldown abilities compared to players with lower latencies).
  • Cannot predict the removal of GameplayEffects. We can however predict adding GameplayEffects with the inverse effects, effectively removing them. This is not always appropriate or feasible and still remains an issue.
  • Lack of boilerplate templates, multiplayer examples, and documentation. Hopefully this helps with that!

⬆ Back to Top

2. Sample Project

A multiplayer third person shooter sample project is included with this documentation aimed at people new to the GameplayAbilitySystem Plugin but not new to Unreal Engine 4. Users are expected to know C++, Blueprints, UMG, Replication, and other intermediate topics in UE4. This project provides an example of how to set up a basic third person shooter multiplayer-ready project with the AbilitySystemComponent (ASC) on the PlayerState class for player/AI controlled heroes and the ASC on the Character class for AI controlled minions.

The goal is to keep this project simple while showing the GAS basics and demonstrating some commonly requested abilities with well-commented code. Because of its beginner focus, the project does not show advanced topics like predicting projectiles.

Concepts demonstrated:

  • ASC on PlayerState vs Character
  • Replicated Attributes
  • Replicated animation montages
  • GameplayTags
  • Applying and removing GameplayEffects inside of and externally from GameplayAbilities
  • Applying damage mitigated by armor to change health of a character
  • GameplayEffectExecutionCalculations
  • Stun effect
  • Death and respawn
  • Spawning actors (projectiles) from an ability on the server
  • Predictively changing the local player's speed with aim down sights and sprinting
  • Constantly draining stamina to sprint
  • Using mana to cast abilities
  • Passive abilities
  • Stacking GameplayEffects
  • Targeting actors
  • GameplayAbilities created in Blueprint
  • GameplayAbilities created in C++
  • Instanced per Actor GameplayAbilities
  • Non-Instanced GameplayAbilities (Jump)
  • Static GameplayCues (FireGun projectile impact particle effect)
  • Actor GameplayCues (Sprint and Stun particle effects)

The hero class has the following abilities:

Ability Input Bind Predicted C++ / Blueprint Description
Jump Space Bar Yes C++ Makes the hero jump.
Gun Left Mouse Button No C++ Fires a projectile from the hero's gun. The animation is predicted but the projectile is not.
Aim Down Sights Right Mouse Button Yes Blueprint While the button is held, the hero will walk slower and the camera will zoom in to allow more precise shots with the gun.
Sprint Left Shift Yes Blueprint While the button is held, the hero will run faster draining stamina.
Forward Dash Q Yes Blueprint The hero dashes forward at the cost of stamina.
Passive Armor Stacks Passive No Blueprint Every 4 seconds the hero gains a stack of armor up to a maximum of 4 stacks. Receiving damage removes one stack of armor.
Meteor R No Blueprint Player targets a location to drop a meteor on the enemies causing damage and stunning them. The targeting is predicted while spawning the meteor is not.

It does not matter if GameplayAbilities are created in C++ or Blueprint. A mixture of the two were used here for example of how to do them in each language.

Minions do not come with any predefined GameplayAbilities. The Red Minions have more health regen while the Blue Minions have higher starting health.

For GameplayAbility naming, I used the suffix _BP to denote the GameplayAbility's logic was created in Blueprint. The lack of suffix means the logic was created in C++.

Blueprint Asset Naming Prefixes

Prefix Asset Type
GA_ GameplayAbility
GC_ GameplayCue
GE_ GameplayEffect

⬆ Back to Top

3. Setting Up a Project Using GAS

Basic steps to set up a project using GAS:

  1. Enable GameplayAbilitySystem plugin in the Editor
  2. Edit YourProjectName.Build.cs to add "GameplayAbilities", "GameplayTags", "GameplayTasks" to your PrivateDependencyModuleNames
  3. Refresh/Regenerate your Visual Studio project files
  4. Starting with 4.24, it is now mandatory to call UAbilitySystemGlobals::InitGlobalData() to use TargetData. The Sample Project does this in UEngineSubsystem::Initialize(). See InitGlobalData() for more information.

That's all that you have to do to enable GAS. From here, add an ASC and AttributeSet to your Character or PlayerState and start making GameplayAbilities and GameplayEffects!

⬆ Back to Top

4. GAS Concepts

Sections

4.1 Ability System Component
4.2 Gameplay Tags
4.3 Attributes
4.4 Attribute Set
4.5 Gameplay Effects
4.6 Gameplay Abilities
4.7 Ability Tasks
4.8 Gameplay Cues
4.9 Ability System Globals
4.10 Prediction

4.1 Ability System Component

The AbilitySystemComponent (ASC) is the heart of GAS. It's a UActorComponent (UAbilitySystemComponent) that handles all interactions with the system. Any Actor that wishes to use GameplayAbilities, have Attributes, or receive GameplayEffects must have one ASC attached to them. These objects all live inside of and are managed and replicated by (with the exception of Attributes which are replicated by their AttributeSet) the ASC. Developers are expected but not required to subclass this.

The Actor with the ASC attached to it is referred to as the OwnerActor of the ASC. The physical representation Actor of the ASC is called the AvatarActor. The OwnerActor and the AvatarActor can be the same Actor as in the case of a simple AI minion in a MOBA game. They can also be different Actors as in the case of a player controlled hero in a MOBA game where the OwnerActor is the PlayerState and the AvatarActor is the hero's Character class. Most Actors will have the ASC on themselves. If your Actor will respawn and need persistence of Attributes or GameplayEffects between spawns (like a hero in a MOBA), then the ideal location for the ASC is on the PlayerState.

Note: If your ASC is on your PlayerState, then you will need to increase the NetUpdateFrequency of your PlayerState. It defaults to a very low value on the PlayerState and can cause delays or perceived lag before changes to things like Attributes and GameplayTags happen on the clients. Be sure to enable Adaptive Network Update Frequency, Fortnite uses it.

Both, the OwnerActor and the AvatarActor if different Actors, should implement the IAbilitySystemInterface. This interface has one function that must be overriden, UAbilitySystemComponent* GetAbilitySystemComponent() const, which returns a pointer to its ASC. ASCs interact with each other internally to the system by looking for this interface function.

The ASC holds its current active GameplayEffects in FActiveGameplayEffectsContainer ActiveGameplayEffects.

The ASC holds its granted Gameplay Abilities in FGameplayAbilitySpecContainer ActivatableAbilities. Any time that you plan to iterate over ActivatableAbilities.Items, be sure to add ABILITYLIST_SCOPE_LOCK(); above your loop to lock the list from changing (due to removing an ability). Every ABILITYLIST_SCOPE_LOCK(); in scope increments AbilityScopeLockCount and then decrements when it falls out of scope. Do not try to remove an ability inside the scope of ABILITYLIST_SCOPE_LOCK(); (the clear ability functions check AbilityScopeLockCount internally to prevent removing abilities if the list is locked).

4.1.1 Replication Mode

The ASC defines three different replication modes for replicating GameplayEffects, GameplayTags, and GameplayCues - Full, Mixed, and Minimal. Attributes are replicated by their AttributeSet.

Replication Mode When to Use Description
Full Single Player Every GameplayEffect is replicated to every client.
Mixed Multiplayer, player controlled Actors GameplayEffects are only replicated to the owning client. Only GameplayTags and GameplayCues are replicated to everyone.
Minimal Multiplayer, AI controlled Actors GameplayEffects are never replicated to anyone. Only GameplayTags and GameplayCues are replicated to everyone.

Note: Mixed replication mode expects the OwnerActor's Owner to be the Controller. PlayerState's Owner is the Controller by default but Character's is not. If using Mixed replication mode with the OwnerActor not the PlayerState, then you need to call SetOwner() on the OwnerActor with a valid Controller.

Starting with 4.24, PossessedBy() now sets the owner of the Pawn to the new Controller.

⬆ Back to Top

4.1.2 Setup and Initialization

ASCs are typically constructed in the OwnerActor's constructor and explicitly marked replicated. This must be done in C++.

AGDPlayerState::AGDPlayerState()
{
	// Create ability system component, and set it to be explicitly replicated
	AbilitySystemComponent = CreateDefaultSubobject<UGDAbilitySystemComponent>(TEXT("AbilitySystemComponent"));
	AbilitySystemComponent->SetIsReplicated(true);
	//...
}

The ASC needs to be initialized with its OwnerActor and AvatarActor on both the server and the client. You want to initialize after the Pawn's Controller has been set (after possession). Single player games only need to worry about the server path.

For player controlled characters where the ASC lives on the Pawn, I typically initialize on the server in the Pawn's PossessedBy() function and initialize on the client in the PlayerController's AcknowledgePossession() function.

void APACharacterBase::PossessedBy(AController * NewController)
{
	Super::PossessedBy(NewController);

	if (AbilitySystemComponent)
	{
		AbilitySystemComponent->InitAbilityActorInfo(this, this);
	}

	// ASC MixedMode replication requires that the ASC Owner's Owner be the Controller.
	SetOwner(NewController);
}
void APAPlayerControllerBase::AcknowledgePossession(APawn* P)
{
	Super::AcknowledgePossession(P);

	APACharacterBase* CharacterBase = Cast<APACharacterBase>(P);
	if (CharacterBase)
	{
		CharacterBase->GetAbilitySystemComponent()->InitAbilityActorInfo(CharacterBase, CharacterBase);
	}

	//...
}

For player controlled characters where the ASC lives on the PlayerState, I typically initialize the server in the Pawn's PossessedBy() function and initialize on the client in the Pawn's OnRep_PlayerState() function. This ensures that the PlayerState exists on the client.

// Server only
void AGDHeroCharacter::PossessedBy(AController * NewController)
{
	Super::PossessedBy(NewController);

	AGDPlayerState* PS = GetPlayerState<AGDPlayerState>();
	if (PS)
	{
		// Set the ASC on the Server. Clients do this in OnRep_PlayerState()
		AbilitySystemComponent = Cast<UGDAbilitySystemComponent>(PS->GetAbilitySystemComponent());

		// AI won't have PlayerControllers so we can init again here just to be sure. No harm in initing twice for heroes that have PlayerControllers.
		PS->GetAbilitySystemComponent()->InitAbilityActorInfo(PS, this);
	}
	
	//...
}
// Client only
void AGDHeroCharacter::OnRep_PlayerState()
{
	Super::OnRep_PlayerState();

	AGDPlayerState* PS = GetPlayerState<AGDPlayerState>();
	if (PS)
	{
		// Set the ASC for clients. Server does this in PossessedBy.
		AbilitySystemComponent = Cast<UGDAbilitySystemComponent>(PS->GetAbilitySystemComponent());

		// Init ASC Actor Info for clients. Server will init its ASC when it possesses a new Actor.
		AbilitySystemComponent->InitAbilityActorInfo(PS, this);
	}

	// ...
}

If you get the error message LogAbilitySystem: Warning: Can't activate LocalOnly or LocalPredicted ability %s when not local! then you did not initialize your ASC on the client.

⬆ Back to Top

4.2 Gameplay Tags

FGameplayTags are hierarchical names in the form of Parent.Child.Grandchild... that are registered with the GameplayTagManager. These tags are incredibly useful for classifying and describing the state of an object. For example, if a character is stunned, we could give it a State.Debuff.Stun GameplayTag for the duration of the stun.

You will find yourself replacing things that you used to handle with booleans or enums with GameplayTags and doing boolean logic on whether or not objects have certain GameplayTags.

When giving tags to an object, we typically add them to its ASC if it has one so that GAS can interact with them. UAbilitySystemComponent implements the IGameplayTagAssetInterface giving functions to access its owned GameplayTags.

Multiple GameplayTags can be stored in an FGameplayTagContainer. It is preferable to use a GameplayTagContainer over a TArray<FGameplayTag> since the GameplayTagContainers add some efficiency magic. While tags are standard FNames, they can be efficiently packed together in FGameplayTagContainers for replication if Fast Replication is enabled in the project settings. Fast Replication requires that the server and the clients have the same list of GameplayTags. This generally shouldn't be a problem so you should enable this option. GameplayTagContainers can also return a TArray<FGameplayTag> for iteration.

GameplayTags stored in FGameplayTagCountContainer have a TagMap that stores the number of instances of that GameplayTag. A FGameplayTagCountContainer may still have the GameplayTag in it but its TagMapCount is zero. You may encounter this while debugging if an ASC still has a GameplayTag. Any of the HasTag() or HasMatchingTag() or similar functions will check the TagMapCount and return false if the GameplayTag is not present or its TagMapCount is zero.

GameplayTags must be defined ahead of time in the DefaultGameplayTags.ini. The UE4 Editor provides an interface in the project settings to let developers manage GameplayTags without needing to manually edit the DefaultGameplayTags.ini. The GameplayTag editor can create, rename, search for references, and delete GameplayTags.

GameplayTag Editor in Project Settings

Searching for GameplayTag references will bring up the familiar Reference Viewer graph in the Editor showing all the assets that reference the GameplayTag. This will not however show any C++ classes that reference the GameplayTag.

Renaming GameplayTags creates a redirect so that assets still referencing the original GameplayTag can redirect to the new GameplayTag. I prefer if possible to instead create a new GameplayTag, update all the references manually to the new GameplayTag, and then delete the old GameplayTag to avoid creating a redirect.

In addition to Fast Replication, the GameplayTag editor has an option to fill in commonly replicated GameplayTags to optimize them further.

GameplayTags are replicated if they're added from a GameplayEffect. The ASC allows you to add LooseGameplayTags that are not replicated and must be managed manually. The Sample Project uses a LooseGameplayTag for State.Dead so that the owning clients can immediately respond to when their health drops to zero. Respawning manually sets the TagMapCount back to zero. Only manually adjust the TagMapCount when working with LooseGameplayTags. It is preferable to use the UAbilitySystemComponent::AddLooseGameplayTag() and UAbilitySystemComponent::RemoveLooseGameplayTag() functions than manually adjusting the TagMapCount.

Getting a reference to a GameplayTag in C++:

FGameplayTag::RequestGameplayTag(FName("Your.GameplayTag.Name"))

For advanced GameplayTag manipulation like getting the parent or children GameplayTags, look at the functions offered by the GameplayTagManager. To access the GameplayTagManager, include GameplayTagManager.h and call it with UGameplayTagManager::Get().FunctionName. The GameplayTagManager actually stores the GameplayTags as relational nodes (parent, child, etc) for faster processing than constant string manipulation and comparisons.

GameplayTags and GameplayTagContainers can have the optional UPROPERTY specifier Meta = (Categories = "GameplayCue") that filters the tags in the Blueprint to show only GameplayTags that have the parent tag of GameplayCue. This is useful when you know the GameplayTag or GameplayTagContainer variable should only be used for GameplayCues.

Alternatively, there's a separate structure called FGameplayCueTag that encapsulates a FGameplayTag and also automatically filters GameplayTags in Blueprint to only show those tags with the parent tag of GameplayCue.

If you want to filter a GameplayTag parameter in a function, use the UFUNCTION specifier Meta = (GameplayTagFilter = "GameplayCue"). GameplayTagContainer parameters in functions can not be filtered. If you would like to edit your engine to allow this, look at how SGameplayTagGraphPin::ParseDefaultValueData() from Engine\Plugins\Editor\GameplayTagsEditor\Source\GameplayTagsEditor\Private\SGameplayTagGraphPin.cpp calls FilterString = UGameplayTagsManager::Get().GetCategoriesMetaFromField(PinStructType); and passes FilterString to SGameplayTagWidget in SGameplayTagGraphPin::GetListContent(). The GameplayTagContainer version of these functions in Engine\Plugins\Editor\GameplayTagsEditor\Source\GameplayTagsEditor\Private\SGameplayTagContainerGraphPin.cpp do not check for the meta field properties and pass along the filter.

The Sample Project extensively uses GameplayTags.

⬆ Back to Top

4.2.1 Responding to Changes in Gameplay Tags

The ASC provides a delegate for when GameplayTags are added or removed. It takes in a EGameplayTagEventType that can specify only to fire when the GameplayTag is added/removed or for any change in the GameplayTag's TagMapCount.

AbilitySystemComponent->RegisterGameplayTagEvent(FGameplayTag::RequestGameplayTag(FName("State.Debuff.Stun")), EGameplayTagEventType::NewOrRemoved).AddUObject(this, &AGDPlayerState::StunTagChanged);

The callback function has a parameter for the GameplayTag and the new TagCount.

virtual void StunTagChanged(const FGameplayTag CallbackTag, int32 NewCount);

⬆ Back to Top

4.3 Attributes

4.3.1 Attribute Definition

Attributes are float values defined by the struct FGameplayAttributeData. These can represent anything from the amount of health a character has to the character's level to the number of charges that a potion has. If it is a gameplay-related numerical value belonging to an Actor, you should consider using an Attribute for it. Attributes should generally only be modified by GameplayEffects so that the ASC can predict the changes.

Attributes are defined by and live in an AttributeSet. The AttributeSet is responsible for replicating Attributes that are marked for replication. See the section on AttributeSets for how to define Attributes.

Tip: If you don't want an Attribute to show up in the Editor's list of Attributes, you can use the Meta = (HideInDetailsView) property specifier.

⬆ Back to Top

4.3.2 BaseValue vs CurrentValue

An Attribute is composed of two values - a BaseValue and a CurrentValue. The BaseValue is the permanent value of the Attribute whereas the CurrentValue is the BaseValue plus temporary modifications from GameplayEffects. For example, your Character may have a movespeed Attribute with a BaseValue of 600 units/second. Since there are no GameplayEffects modifying the movespeed yet, the CurrentValue is also 600 u/s. If she gets a temporary 50 u/s movespeed buff, the BaseValue stays the same at 600 u/s while the CurrentValue is now 600 + 50 for a total of 650 u/s. When the movespeed buff expires, the CurrentValue reverts back to the BaseValue of 600 u/s.

Often beginners to GAS will confuse BaseValue with a maximum value for an Attribute and try to treat it as such. This is an incorrect approach. Maximum values for Attributes that can change or are referenced in abilities or UI should be treated as separate Attributes. For hardcoded maximum and minimum values, there is a way to define a DataTable with FAttributeMetaData that can set maximum and minimum values, but Epic's comment above the struct calls it a "work in progress". See AttributeSet.h for more information. To prevent confusion, I recommend that maximum values that can be referenced in abilities or UI be made as separate Attributes and hardcoded maximum and minimum values that are only used for clamping Attributes be defined as hardcoded floats in the AttributeSet. Clamping of Attributes is discussed in PreAttributeChange() for changes to the CurrentValue and PostGameplayEffectExecute() for changes to the BaseValue from GameplayEffects.

Permanent changes to the BaseValue come from Instant GameplayEffects whereas Duration and Infinite GameplayEffects change the CurrentValue. Periodic GameplayEffects are treated like instant GameplayEffects and change the BaseValue.

⬆ Back to Top

4.3.3 Meta Attributes

Some Attributes are treated as placeholders for temporary values that are intended to interact with Attributes. These are called Meta Attributes. For example, we commonly define damage as a Meta Attribute. Instead of a GameplayEffect directly changing our health Attribute, we use a Meta Attribute called damage as a placeholder. This way the damage value can be modified with buffs and debuffs in an GameplayEffectExecutionCalculation and can be further manipulated in the AttributeSet, for example subtracting the damage from a current shield Attribute, before finally subtracting the remainder from the health Attribute. The damage Meta Attribute has no persistence between GameplayEffects and is overriden by every one. Meta Attributes are not typically replicated.

Meta Attributes provide a good logical separation for things like damage and healing between "How much damage did we do?" and "What do we do with this damage?". This logical separation means our Gameplay Effects and Execution Calculations don't need to know how the Target handles the damage. Continuing our damage example, the Gameplay Effect determines how much damage and then the AttributeSet decides what to do with that damage. Not all characters may have the same Attributes, especially if you use subclassed AttributeSets. The base AttributeSet class may only have a health Attribute, but a subclassed AttributeSet may add a shield Attribute. The subclassed AttributeSet with the shield Attribute would distribute the damage received differently than the base AttributeSet class.

While Meta Attributes are a good design pattern, they are not mandatory. If you only ever have one Execution Calculation used for all instances of damage and one Attribute Set class shared by all characters, then you may be fine doing the damage distribution to health, shields, etc. inside of the Execution Calculation and directly modifying those Attributes. You'll only be sacrificing flexibility, but that may be okay for you.

⬆ Back to Top

4.3.4 Responding to Attribute Changes

To listen for when an Attribute changes to update the UI or other gameplay, use UAbilitySystemComponent::GetGameplayAttributeValueChangeDelegate(FGameplayAttribute Attribute). This function returns a delegate that you can bind to that will be automatically called whenever an Attribute changes. The delegate provides a FOnAttributeChangeData parameter with the NewValue, OldValue, and FGameplayEffectModCallbackData. Note: The FGameplayEffectModCallbackData will only be set on the server.

AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AttributeSetBase->GetHealthAttribute()).AddUObject(this, &AGDPlayerState::HealthChanged);
virtual void HealthChanged(const FOnAttributeChangeData& Data);

The Sample Project binds to the Attribute value changed delegates on the GDPlayerState to update the HUD and to respond to player death when health reaches zero.

A custom Blueprint node that wraps this into an ASyncTask is included in the Sample Project. It is used in the UI_HUD UMG Widget to update the health, mana, and stamina values. This AsyncTask will live forever until manually called EndTask(), which we do in the UMG Widget's Destruct event. See AsyncTaskAttributeChanged.h/cpp.

Listen for Attribute Change BP Node

⬆ Back to Top

4.3.5 Derived Attributes

To make an Attribute that has some or all of its value derived from one or more other Attributes, use an Infinite GameplayEffect with one or more Attribute Based or MMC Modifiers. The Derived Attribute will update automatically when an Attribute that it depends on is updated.

The final formula for all the Modifiers on a Derived Attribute is the same formula for Modifier Aggregators. If you need calculations to happen in a certain order, do it all inside of an MMC.

((CurrentValue + Additive) * Multiplicitive) / Division

Note: If playing with multiple clients in PIE, you need to disable Run Under One Process in the Editor Preferences otherwise the Derived Attributes will not update when their independent Attributes update on clients other than the first.

In this example, we have an Infinite GameplayEffect that derives the value of TestAttrA from the Attributes, TestAttrB and TestAttrC, in the formula TestAttrA = (TestAttrA + TestAttrB) * ( 2 * TestAttrC). TestAttrA recalculates its value automatically whenever any of the Attributes update their values.

Derived Attribute Example

⬆ Back to Top

4.4 Attribute Set

4.4.1 Attribute Set Definition

The AttributeSet defines, holds, and manages changes to Attributes. Developers should subclass from UAttributeSet. Creating an AttributeSet in an OwnerActor's constructor automatically registers it with its ASC. This must be done in C++.

⬆ Back to Top

4.4.2 Attribute Set Design

An ASC may have one or many AttributeSets. AttributeSets have negligible memory overhead so how many AttributeSets to use is an organizational decision left up to the developer.

It is acceptable to have one large monolithic AttributeSet shared by every Actor in your game and only use attributes if needed while ignoring unused attributes.

Alternatively, you may choose to have more than one AttributeSet representing groupings of Attributes that you selectively add to your Actors as needed. For example, you could have an AttributeSet for health related Attributes, an AttributeSet for mana related Attributes, and so on. In a MOBA game, heroes might need mana but minions might not. Therefore the heroes would get the mana AttributeSet and minions would not.

Additionally, AttributeSets can be subclassed as another means of selectively choosing which Attributes an Actor has. Attributes are internally referred to as AttributeSetClassName.AttributeName. When you subclass an AttributeSet, all of the Attributes from the parent class will still have the parent class's name as the prefix.

While you can have more than one AttributeSet, you should not have more than one AttributeSet of the same class on an ASC. If you have more than one AttributeSet from the same class, it won't know which AttributeSet to use and will just pick one.

4.4.2.1 Subcomponents with Individual Attributes

In the scenario where you have multiple damageable components on a Pawn like individually damageable armor pieces, I recommend that if you know the maximum number of damageable components that a Pawn could have to make that many health Attributes on one AttributeSet - DamageableCompHealth0, DamageableCompHealth1, etc. to represent logical 'slots' for those damageable components. In your damageable component class instance, assign the slot number Attribute that can be read by GameplayAbilities or Executions to know which Attribute to apply damage to. Pawns that have less than the maximum number or zero of damageable components are fine. Just because a AttributeSet has an Attribute, doesn't mean that you have to use it. Unused Attributes take up trivial amount of memory.

If your subcomponents need many Attributes each, there's potentially an unbounded number of subcomponents, the subcomponents can detach and be used by other players (e.g. weapons), or for any other reason this approach doesn't work for you, I'd recommend switching away from Attributes and instead store plain old floats on the components. See Item Attributes.

4.4.2.2 Adding and Removing AttributeSets at Runtime

AttributeSets can be added and removed from an ASC at runtime; however, removing AttributeSets can be dangerous. For example, if an AttributeSet is removed on a client before the server and an Attribute value change is replicated to client, the Attribute won't find its AttributeSet and crash the game.

On weapon add to inventory:

AbilitySystemComponent->SpawnedAttributes.AddUnique(WeaponAttributeSetPointer);
AbilitySystemComponent->ForceReplication();

On weapon remove from inventory:

AbilitySystemComponent->SpawnedAttributes.Remove(WeaponAttributeSetPointer);
AbilitySystemComponent->ForceReplication();

4.4.2.3 Item Attributes (Weapon Ammo)

There's a few ways to implement equippable items with Attributes (weapon ammo, armor durability, etc). All of these approaches store values directly on the item. This is necessary for items that can be equipped by more than one player over its lifetime.

  1. Use plain floats on the item (Recommended)
  2. Separate AttributeSet on the item
  3. Separate ASC on the item

4.4.2.3.1 Plain Floats on the Item

Instead of Attributes, store plain float values on the item class instance. Fortnite and GASShooter handle gun ammo this way. For a gun, store the max clip size, current ammo in clip, reserve ammo, etc directly as replicated floats (COND_OwnerOnly) on the gun instance. If weapons share reserve ammo, you would move the reserve ammo onto the character as an Attribute in a shared ammo AttributeSet (reload abilities can use a Cost GE to pull from reserve ammo into the gun's float clip ammo). Since you're not using Attributes for current clip ammo, you will need to override some functions in UGameplayAbility to check and apply cost against the floats on the gun. Making the gun the SourceObject in the GameplayAbilitySpec when granting the ability means you'll have access to the gun that granted the ability inside the ability.

To prevent the gun from replicating back the ammo amount and clobbering the local ammo amount during automatic fire, disable replication while the player has a IsFiring GameplayTag in PreReplication(). You're essentially doing your own local prediction here.

void AGSWeapon::PreReplication(IRepChangedPropertyTracker& ChangedPropertyTracker)
{
	Super::PreReplication(ChangedPropertyTracker);

	DOREPLIFETIME_ACTIVE_OVERRIDE(AGSWeapon, PrimaryClipAmmo, (IsValid(AbilitySystemComponent) && !AbilitySystemComponent->HasMatchingGameplayTag(WeaponIsFiringTag)));
	DOREPLIFETIME_ACTIVE_OVERRIDE(AGSWeapon, SecondaryClipAmmo, (IsValid(AbilitySystemComponent) && !AbilitySystemComponent->HasMatchingGameplayTag(WeaponIsFiringTag)));
}

Benefits:

  1. Avoids limitations of using AttributeSets (see below)

Limitations:

  1. Can not use existing GameplayEffect workflow (Cost GEs for ammo use, etc)
  2. Requires work to override key functions on UGameplayAbility to check and apply ammo costs against the gun's floats

4.4.2.3.2 AttributeSet on the Item

Using a separate AttributeSet on the item that gets added to the player's ASC on adding it to the player's inventory can work, but it has some major limitations. I had this working in early versions of GASShooter for the weapon ammo. The weapon stores its Attributes such as max clip size, current ammo in clip, reserve ammo, etc in an AttributeSet that lives on the weapon class. If weapons share reserve ammo, you would move the reserve ammo onto the character in a shared ammo AttributeSet. When a weapon is added to the player's inventory on the server, the weapon would add its AttributeSet to the player's ASC::SpawnedAttributes. The server would then replicate this down to the client. If the weapon is removed from the inventory, it would remove its AttributeSet from the ASC::SpawnedAttributes.

When the AttributeSet lives on something other than the OwnerActor (say a weapon), you'll initially get some compilation errors in the AttributeSet. The fix is to construct the AttributeSet in BeginPlay() instead of in the constructor and to implement IAbilitySystemInterface (set the pointer to the ASC when you add the weapon to the player inventory) on the weapon.

void AGSWeapon::BeginPlay()
{
	if (!AttributeSet)
	{
		AttributeSet = NewObject<UGSWeaponAttributeSet>(this);
	}
	//...
}

You can see it in practice by checking out this older version of GASShooter.

Benefits:

  1. Can use existing GameplayAbility and GameplayEffect workflow (Cost GEs for ammo use, etc)
  2. Simple to setup for a very small set of items

Limitations:

  1. You have to make a new AttributeSet class for every weapon type. ASCs can only functionally have one AttributeSet instance of a class since changes to an Attribute look for the first instance of their AttributeSet class in the ASCs SpawnedAttributes array. Additional instances of the same AttributeSet class are ignored.
  2. You can only have one of each type of weapon in the player's inventory due to previous reason of one AttributeSet instance per AttributeSet class.
  3. Removing an AttributeSet is dangerous. In GASShooter if the player killed himself from a rocket, the player would immediately remove the rocket launcher from his inventory (including its AttributeSet from the ASC). When the server replicated that the rocket launcher's ammo Attribute changed, the AttributeSet no longer existed on the client's ASC and the game crashed.

4.4.2.3.3 ASC on the Item

Putting a whole AbilitySystemComponent on each item is an extreme approach. I have not personally done this nor have I seen it in the wild. It would take a lot of engineering to make it work.

Is it viable to have several AbilitySystemComponents which have the same owner but different avatars (e.g. on pawn and weapon/items/projectiles with Owner set to PlayerState)?

The first problem I see there would be implementing the IGameplayTagAssetInterface and IAbilitySystemInterface on the owning actor. The former may be possible: just aggregate the tags from all all ASCs (but watch out -HasAllMatchingGameplayTags may be met only via cross ASC aggregation. It wouldn't be enough to just forward that calls to each ASC and OR the results together). But the later is even trickier: which ASC is the authoritative one? If someone wants to apply a GE -which one should receive it? Maybe you can work these out but this side of the problem will be the hardest: owners will multiple ASCs beneath them.

Separate ASCs on the pawn and the weapon can make sense on its own though. E.g, distinguishing between tags the describe the weapon vs those that describe the owning pawn. Maybe it does make sense that tags granted to the weapon also “apply” to the owner and nothing else (E.g, attributes and GEs are independent but the owner will aggregate the owned tags like I describe above). This could work out, I am sure. But having multiple ASCs with the same owner may get dicey.

Dave Ratti from Epic's answer to community questions #6

Benefits:

  1. Can use existing GameplayAbility and GameplayEffect workflow (Cost GEs for ammo use, etc)
  2. Can reuse AttributeSet classes (one on each weapon's ASC)

Limitations:

  1. Unknown engineering cost
  2. Is it even possible?

⬆ Back to Top

4.4.3 Defining Attributes

Attributes can only be defined in C++ in the AttributeSet's header file. It is recommended to add this block of macros to the top of every AttributeSet header file. It will automatically generate getter and setter functions for your Attributes.

// Uses macros from AttributeSet.h
#define ATTRIBUTE_ACCESSORS(ClassName, PropertyName) \
	GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \
	GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \
	GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \
	GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName)

A replicated health attribute would be defined like this:

UPROPERTY(BlueprintReadOnly, Category = "Health", ReplicatedUsing = OnRep_Health)
FGameplayAttributeData Health;
ATTRIBUTE_ACCESSORS(UGDAttributeSetBase, Health)

Also define the OnRep function in the header:

UFUNCTION()
virtual void OnRep_Health(const FGameplayAttributeData& OldHealth);

The .cpp file for the AttributeSet should fill in the OnRep function with the GAMEPLAYATTRIBUTE_REPNOTIFY macro used by the prediction system:

void UGDAttributeSetBase::OnRep_Health(const FGameplayAttributeData& OldHealth)
{
	GAMEPLAYATTRIBUTE_REPNOTIFY(UGDAttributeSetBase, Health, OldHealth);
}

Finally, the Attribute needs to be added to GetLifetimeReplicatedProps:

void UGDAttributeSetBase::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);

	DOREPLIFETIME_CONDITION_NOTIFY(UGDAttributeSetBase, Health, COND_None, REPNOTIFY_Always);
}

REPNOTIFY_Always tells the OnRep function to trigger if the local value is already equal to the value being repped down from the Server (due to prediction). By default it won't trigger the OnRep function if the local value is the same as the value being repped down from the Server.

If the Attribute is not replicated like a Meta Attribute, then the OnRep and GetLifetimeReplicatedProps steps can be skipped.

⬆ Back to Top

4.4.4 Initializing Attributes

There are multiple ways to initialize Attributes (set their BaseValue and consequently their CurrentValue to some initial value). Epic recommends using an instant GameplayEffect. This is the method used in the Sample Project too.

See GE_HeroAttributes Blueprint in the Sample Project for how to make an instant GameplayEffect to initialize Attributes. Application of this GameplayEffect happens in C++.

If you used the ATTRIBUTE_ACCESSORS macro when you defined your Attributes, an initialization function will automatically be generated on the AttributeSet for each Attribute that you can call at your leisure in C++.

// InitHealth(float InitialValue) is an automatically generated function for an Attribute 'Health' defined with the `ATTRIBUTE_ACCESSORS` macro
AttributeSet->InitHealth(100.0f);

See AttributeSet.h for more ways to initialize Attributes.

Note: Prior to 4.24, FAttributeSetInitterDiscreteLevels did not work with FGameplayAttributeData. It was created when Attributes were raw floats and will complain about FGameplayAttributeData not being Plain Old Data (POD). This is fixed in 4.24 https://issues.unrealengine.com/issue/UE-76557.

⬆ Back to Top

4.4.5 PreAttributeChange()

PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue) is one of the main functions in the AttributeSet to respond to changes to an Attribute's CurrentValue before the change happens. It is the ideal place to clamp incoming changes to CurrentValue via the reference parameter NewValue.

For example to clamp movespeed modifiers the Sample Project does it like so:

if (Attribute == GetMoveSpeedAttribute())
{
	// Cannot slow less than 150 units/s and cannot boost more than 1000 units/s
	NewValue = FMath::Clamp<float>(NewValue, 150, 1000);
}

The GetMoveSpeedAttribute() function is created by the macro block that we added to the AttributeSet.h (Defining Attributes).

This is triggered from any changes to Attributes, whether using Attribute setters (defined by the macro block in AttributeSet.h (Defining Attributes)) or using GameplayEffects.

Note: Any clamping that happens here does not permanently change the modifier on the ASC. It only changes the value returned from querying the modifier. This means anything that recalculates the CurrentValue from all of the modifiers like GameplayEffectExecutionCalculations and ModifierMagnitudeCalculations need to implement clamping again.

Note: Epic's comments for PreAttributeChange() say not to use it for gameplay events and instead use it mainly for clamping. The recommended place for gameplay events on Attribute change is UAbilitySystemComponent::GetGameplayAttributeValueChangeDelegate(FGameplayAttribute Attribute) (Responding to Attribute Changes).

⬆ Back to Top

4.4.6 PostGameplayEffectExecute()

PostGameplayEffectExecute(const FGameplayEffectModCallbackData & Data) only triggers after changes to the BaseValue of an Attribute from an instant GameplayEffect. This is a valid place to do more Attribute manipulation when they change from a GameplayEffect.

For example, in the Sample Project we subtract the final damage Meta Attribute from the health Attribute here. If there was a shield Attribute, we would subtract the damage from it first before subtracting the remainder from health. The Sample Project also uses this location to apply hit react animations, show floating Damage Numbers, and assign experience and gold bounties to the killer. By design, the damage Meta Attribute will always come through an instant GameplayEffect and never the Attribute setter.

Other Attributes that will only have their BaseValue changed from instant GameplayEffects like mana and stamina can also be clamped to their maximum value counterpart Attributes here.

Note: When PostGameplayEffectExecute() is called, changes to the Attribute have already happened, but they have not replicated back to clients yet so clamping values here will not cause two network updates to clients. Clients will only receive the update after clamping.

⬆ Back to Top

4.4.7 OnAttributeAggregatorCreated()

OnAttributeAggregatorCreated(const FGameplayAttribute& Attribute, FAggregator* NewAggregator) triggers when an Aggregator is created for an Attribute in this set. It allows custom setup of FAggregatorEvaluateMetaData. AggregatorEvaluateMetaData is used by the Aggregator in evaluating the CurrentValue of an Attribute based on all the Modifiers applied to it. By default, AggregatorEvaluateMetaData is only used by the Aggregator to determine which Modifiers qualify with the example of MostNegativeMod_AllPositiveMods which allows all positive Modifiers but restricts negative Modifiers to only the most negative one. This was used by Paragon to only allow the most negative move speed slow effect to apply to a player regardless of how many slow effects where on them at any one time while applying all positive move speed buffs. Modifiers that don't qualify still exist on the ASC, they just aren't aggregated into the final CurrentValue. They can potentially qualify later once conditions change, like in the case if the most negative Modifier expires, the next most negative Modifier (if one exists) then qualifies.

To use AggregatorEvaluateMetaData in the example of only allowing the most negative Modifier and all positive Modifiers:

virtual void OnAttributeAggregatorCreated(const FGameplayAttribute& Attribute, FAggregator* NewAggregator) const override;
void UGSAttributeSetBase::OnAttributeAggregatorCreated(const FGameplayAttribute& Attribute, FAggregator* NewAggregator) const
{
	Super::OnAttributeAggregatorCreated(Attribute, NewAggregator);

	if (!NewAggregator)
	{
		return;
	}

	if (Attribute == GetMoveSpeedAttribute())
	{
		NewAggregator->EvaluationMetaData = &FAggregatorEvaluateMetaDataLibrary::MostNegativeMod_AllPositiveMods;
	}
}

Your custom AggregatorEvaluateMetaData for qualifiers should be added to FAggregatorEvaluateMetaDataLibrary as static variables.

⬆ Back to Top

4.5 Gameplay Effects

4.5.1 Gameplay Effect Definition

GameplayEffects (GE) are the vessels through which abilities change Attributes and GameplayTags on themselves and others. They can cause immediate Attribute changes like damage or healing or apply long term status buff/debuffs like a movespeed boost or stunning. The UGameplayEffect class is a meant to be a data-only class that defines a single gameplay effect. No additional logic should be added to GameplayEffects. Typically designers will create many Blueprint child classes of UGameplayEffect.

GameplayEffects change Attributes through Modifiers and Executions (GameplayEffectExecutionCalculation).

GameplayEffects have three types of duration: Instant, Duration, and Infinite.

Additionally, GameplayEffects can add/execute GameplayCues. An Instant GameplayEffect will call Execute on the GameplayCue GameplayTags whereas a Duration or Infinite GameplayEffect will call Add and Remove on the GameplayCue GameplayTags.

Duration Type GameplayCue Event When to use
Instant Execute For immediate permanent changes to Attribute's BaseValue. GameplayTags will not be applied, not even for a frame.
Duration Add & Remove For temporary changes to Attribute's CurrentValue and to apply GameplayTags that will be removed when the GameplayEffect expires or is manually removed. The duration is specified in the UGameplayEffect class/Blueprint.
Infinite Add & Remove For temporary changes to Attribute's CurrentValue and to apply GameplayTags that will be removed when the GameplayEffect is removed. These will never expire on their own and must be manually removed by an ability or the ASC.

Duration and Infinite GameplayEffects have the option of applying Periodic Effects that apply its Modifiers and Executions every X seconds as defined by its Period. Periodic Effects are treated as Instant GameplayEffects when it comes to changing the Attribute's BaseValue and Executing GameplayCues. These are useful for damage over time (DOT) type effects. Note: Periodic Effects cannot be predicted.

Duration and Infinite GameplayEffects can be temporarily turned off and on after application if their Ongoing Tag Requirements are not met/met (Gameplay Effect Tags). Turning off a GameplayEffect removes the effects of its Modifiers and applied GameplayTags but does not remove the GameplayEffect. Turning the GameplayEffect back on reapplies its Modifiers and GameplayTags.

If you need to manually recalculate the Modifiers of a Duration or Infinite GameplayEffect (say you have an MMC that uses data that doesn't come from Attributes), you can call UAbilitySystemComponent::ActiveGameplayEffects.SetActiveGameplayEffectLevel(FActiveGameplayEffectHandle ActiveHandle, int32 NewLevel) with the same level that it already has using UAbilitySystemComponent::ActiveGameplayEffects.GetActiveGameplayEffect(ActiveHandle).Spec.GetLevel(). Modifiers that are based on backing Attributes automatically update when those backing Attributes update. The key functions of SetActiveGameplayEffectLevel() to update the Modifiers are:

MarkItemDirty(Effect);
Effect.Spec.CalculateModifierMagnitudes();
// Private function otherwise we'd call these three functions without needing to set the level to what it already is
UpdateAllAggregatorModMagnitudes(Effect);

GameplayEffects are not typically instantiated. When an ability or ASC wants to apply a GameplayEffect, it creates a GameplayEffectSpec from the GameplayEffect's ClassDefaultObject. Successfully applied GameplayEffectSpecs are then added to a new struct called FActiveGameplayEffect which is what the ASC keeps track of in a special container struct called ActiveGameplayEffects.

⬆ Back to Top

4.5.2 Applying Gameplay Effects

GameplayEffects can be applied in many ways from functions on GameplayAbilities and functions on the ASC and usually take the form of ApplyGameplayEffectTo. The different functions are essentially convenience functions that will eventually call UAbilitySystemComponent::ApplyGameplayEffectSpecToSelf() on the Target.

To apply GameplayEffects outside of a GameplayAbility for example from a projectile, you need to get the Target's ASC and use one of its functions to ApplyGameplayEffectToSelf.

You can listen for when any Duration or Infinite GameplayEffects are applied to an ASC by binding to its delegate:

AbilitySystemComponent->OnActiveGameplayEffectAddedDelegateToSelf.AddUObject(this, &APACharacterBase::OnActiveGameplayEffectAddedCallback);

The callback function:

virtual void OnActiveGameplayEffectAddedCallback(UAbilitySystemComponent* Target, const FGameplayEffectSpec& SpecApplied, FActiveGameplayEffectHandle ActiveHandle);

The server will always call this function regardless of replication mode. The autonomous proxy will only call this for replicated GameplayEffects in Full and Mixed replication modes. Simulated proxies will only call this in Full replication mode.

⬆ Back to Top

4.5.3 Removing Gameplay Effects

GameplayEffects can be removed in many ways from functions on GameplayAbilities and functions on the ASC and usually take the form of RemoveActiveGameplayEffect. The different functions are essentially convenience functions that will eventually call FActiveGameplayEffectsContainer::RemoveActiveEffects() on the Target.

To remove GameplayEffects outside of a GameplayAbility, you need to get the Target's ASC and use one of its functions to RemoveActiveGameplayEffect.

You can listen for when any Duration or Infinite GameplayEffects are removed from an ASC by binding to its delegate:

AbilitySystemComponent->OnAnyGameplayEffectRemovedDelegate().AddUObject(this, &APACharacterBase::OnRemoveGameplayEffectCallback);

The callback function:

virtual void OnRemoveGameplayEffectCallback(const FActiveGameplayEffect& EffectRemoved);

The server will always call this function regardless of replication mode. The autonomous proxy will only call this for replicated GameplayEffects in Full and Mixed replication modes. Simulated proxies will only call this in Full replication mode.

⬆ Back to Top

4.5.4 Gameplay Effect Modifiers

Modifiers change an Attribute and are the only way to predictively change an Attribute. A GameplayEffect can have zero or many Modifiers. Each Modifier is responsible for changing only one Attribute via a specified operation.

Operation Description
Add Adds the result to the Modifier's specified Attribute. Use a negative value for subtraction.
Multiply Multiplies the result to the Modifier's specified Attribute.
Divide Divides the result against the Modifier's specified Attribute.
Override Overrides the Modifier's specified Attribute with the result.

The CurrentValue of an Attribute is the aggregate result of all of its Modifiers added to its BaseValue. The formula for how Modifiers are aggregated is defined as follows in FAggregatorModChannel::EvaluateWithBase in GameplayEffectAggregator.cpp:

((InlineBaseValue + Additive) * Multiplicitive) / Division

Any Override Modifiers will override the final value with the last applied Modifier taking precedence.

Note: For percentage based changes, make sure to use the Multiply operation so that it happens after addition.

Note: Prediction has trouble with percentage changes.

There are four types of Modifiers: Scalable Float, Attribute Based, Custom Calculation Class, and Set By Caller. They all generate some float value that is then used to change the specified Attribute of the Modifier based on its operation.

Modifier Type Description
Scalable Float FScalableFloats are a structure that can point to a Data Table that has the variables as rows and levels as columns. The Scalable Floats will automatically read the value of the specified table row at the ability's current level (or different level if overriden on the GameplayEffectSpec). This value can further be manipulated by a coefficient. If no Data Table/Row is specified, it treats the value as a 1 so the coefficient can be used to hard code in a single value at all levels. ScalableFloat
Attribute Based Attribute Based Modifiers take the CurrentValue or BaseValue of a backing Attribute on the Source (who created the GameplayEffectSpec) or Target (who received the GameplayEffectSpec) and further modifies it with a coefficient and pre and post coefficient additions. Snapshotting means the backing Attribute is captured when the GameplayEffectSpec is created whereas no snapshotting means the Attribute is captured when the GameplayEffectSpec is applied.
Custom Calculation Class Custom Calculation Class provides the most flexibility for complex Modifiers. This Modifier takes a ModifierMagnitudeCalculation class and can further manipulate the resulting float value with a coefficient and pre and post coefficient additions.
Set By Caller SetByCaller Modifiers are values that are set outside of the GameplayEffect at runtime by the ability or whoever made the GameplayEffectSpec on the GameplayEffectSpec. For example, you would use a SetByCaller if you want to set the damage to be based on how long the player held down a button to charge the ability. SetByCallers are essentially TMap<FGameplayTag, float> that live on the GameplayEffectSpec. The Modifier is just telling the Aggregator to look for a SetByCaller value associated with the supplied GameplayTag. The SetByCallers used by Modifiers can only use the GameplayTag version of the concept. The FName version is disabled here. If the Modifier is set to SetByCaller but a SetByCaller with the correct GameplayTag does not exist on the GameplayEffectSpec, the game will throw a runtime error and return a value of 0. This might cause issues in the case of a Divide operation. See SetByCallers for more information on how to use SetByCallers.

⬆ Back to Top

4.5.4.1 Multiply and Divide Modifiers

By default, all Multiply and Divide Modifiers are added together before multiplying or dividing them into the Attribute's BaseValue.

float FAggregatorModChannel::EvaluateWithBase(float InlineBaseValue, const FAggregatorEvaluateParameters& Parameters) const
{
	...
	float Additive = SumMods(Mods[EGameplayModOp::Additive], GameplayEffectUtilities::GetModifierBiasByModifierOp(EGameplayModOp::Additive), Parameters);
	float Multiplicitive = SumMods(Mods[EGameplayModOp::Multiplicitive], GameplayEffectUtilities::GetModifierBiasByModifierOp(EGameplayModOp::Multiplicitive), Parameters);
	float Division = SumMods(Mods[EGameplayModOp::Division], GameplayEffectUtilities::GetModifierBiasByModifierOp(EGameplayModOp::Division), Parameters);
	...
	return ((InlineBaseValue + Additive) * Multiplicitive) / Division;
	...
}
float FAggregatorModChannel::SumMods(const TArray<FAggregatorMod>& InMods, float Bias, const FAggregatorEvaluateParameters& Parameters)
{
	float Sum = Bias;

	for (const FAggregatorMod& Mod : InMods)
	{
		if (Mod.Qualifies())
		{
			Sum += (Mod.EvaluatedMagnitude - Bias);
		}
	}

	return Sum;
}

from GameplayEffectAggregator.cpp

Both Multiply and Divide Modifiers have a Bias value of 1 in this formula (Addition has a Bias of 0). So it would look something like:

1 + (Mod1.Magnitude - 1) + (Mod2.Magnitude - 1) + ...

This formula leads to some unexpected results. Firstly, this formula adds all the modifiers together before multiplying or dividing them into the BaseValue. Most people would expect it to multiply or divide them together. For example, if you have two Multiply modifiers of 1.5, most people would expect the BaseValue to be multiplied by 1.5 x 1.5 = 2.25. Instead, this adds the 1.5s together to multiply the BaseValue by 2 (50% increase + another 50% increase = 100% increase). This was for the example from GameplayPrediction.h of a 10% speed buff on 500 base speed would be 550. Add another 10% speed buff and it will be 600.

Secondly, this formula has some undocumented rules about what values can be used as it was designed with Paragon in mind.

Rules for Multiply and Divide multiplication addition formula:

  • (No more than one value < 1) AND (Any number of values [1, 2))
  • OR (One value >= 2)

The Bias in the formula basically subtracts out the integer digit of numbers in the range [1, 2). The first Modifier's Bias subtracts out from the starting Sum value (set to the Bias before the loop) which is why any value by itself works and why one value < 1 will work with the numbers in the range [1, 2).

Some examples with Multiply:
Multipliers: 0.5
1 + (0.5 - 1) = 0.5, correct

Multipliers: 0.5, 0.5
1 + (0.5 - 1) + (0.5 - 1) = 0, incorrect expected 1? Multiple values less than 1 don't make sense for adding multipliers. Paragon was designed to only use the greatest negative value for Multiply Modifiers so there would only ever be at most one value less than 1 multiplying into the BaseValue.

Multipliers: 1.1, 0.5
1 + (0.5 - 1) + (1.1 - 1) = 0.6, correct

Multipliers: 5, 5
1 + (5 - 1) + (5 - 1) = 9, incorrect expected 10. Will always be the sum of the Modifiers - number of Modifiers + 1.

Many games will want their Modify and Divide Modifiers to multiply and divide together before applying to the BaseValue. To achieve this, you will need to change the engine code for FAggregatorModChannel::EvaluateWithBase().

float FAggregatorModChannel::EvaluateWithBase(float InlineBaseValue, const FAggregatorEvaluateParameters& Parameters) const
{
	...
	float Multiplicitive = MultiplyMods(Mods[EGameplayModOp::Multiplicitive], Parameters);
	...

	return ((InlineBaseValue + Additive) * Multiplicitive) / Division;
}
float FAggregatorModChannel::MultiplyMods(const TArray<FAggregatorMod>& InMods, const FAggregatorEvaluateParameters& Parameters)
{
	float Multiplier = 1.0f;

	for (const FAggregatorMod& Mod : InMods)
	{
		if (Mod.Qualifies())
		{
			Multiplier *= Mod.EvaluatedMagnitude;
		}
	}

	return Multiplier;
}

⬆ Back to Top

4.5.4.2 Gameplay Tags on Modifiers

SourceTags and TargetTags can be set for each Modifier. They work the same like the Application Tag requirements of a GameplayEffect. So the tags are considered only when the effect is applied. I.e. when having a periodic, infinite effect, they are only taken into consideration on the first application of the effect but not on each periodic execution.

Attribute Based Modifiers can also set SourceTagFilter and TargetTagFilter. When determining the magnitude of the attribute which is the source of the Attribute Based Modifier, these filters are used to exclude certain Modifiers to that attribute. Modifiers which source or target didn't have all of the tags of the filter are excluded.

This means in detail: The tags of the source ASC and the target ASC are captured by GameplayEffects. The source ASC tags are captured, when the GameplayEffectSpec is created, the target ASC tags are captured on execution of the effect. When determining, if a Modifier of an infinite or duration effect "qualifies" to be applied (i.e. its Aggregator qualifies) and those filters are set, the captured tags are compared against the filters.

⬆ Back to Top

4.5.5 Stacking Gameplay Effects

GameplayEffects by default will apply new instances of the GameplayEffectSpec that don't know or care about previously existing instances of the GameplayEffectSpec on application. GameplayEffects can be set to stack where instead of a new instance of the GameplayEffectSpec is added, the currently existing GameplayEffectSpec's stack count is changed. Stacking only works for Duration and Infinite GameplayEffects.

There are two types of stacking: Aggregate by Source and Aggregate by Target.

Stacking Type Description
Aggregate by Source There is a separate instance of stacks per Source ASC on the Target. Each Source can apply X amount of stacks.
Aggregate by Target There is only one instance of stacks on the Target regardless of Source. Each Source can apply a stack up to the shared stack limit.

Stacks also have policies for expiration, duration refresh, and period reset. They have helpful hover tooltips in the GameplayEffect Blueprint.

The Sample Project includes a custom Blueprint node that listens for GameplayEffect stack changes. The HUD UMG Widget uses it to update the amount of passive armor stacks that the player has. This AsyncTask will live forever until manually called EndTask(), which we do in the UMG Widget's Destruct event. See AsyncTaskEffectStackChanged.h/cpp.

Listen for GameplayEffect Stack Change BP Node

⬆ Back to Top

4.5.6 Granted Abilities

GameplayEffects can grant new GameplayAbilities to ASCs. Only Duration and Infinite GameplayEffects can grant abilities.

A common usecase for this is when you want to force another player to do something like moving them from a knockback or pull. You would apply a GameplayEffect to them that grants them an automatically activating ability (see Passive Abilities for how to automatically activate an ability when it is granted) that does the desired action to them.

Designers can choose which abilities a GameplayEffect grants, what level to grant them at, what input to bind them at and the removal policy for the granted ability.

Removal Policy Description
Cancel Ability Immediately The granted ability is canceled and removed immediately when the GameplayEffect that granted it is removed from the Target.
Remove Ability on End The granted ability is allowed to finish and then is removed from the Target.
Do Nothing The granted ability is not affected by the removal of the granting GameplayEffect from the Target. The Target has the ability permanently until it is manually removed later.

⬆ Back to Top

4.5.7 Gameplay Effect Tags

GameplayEffects carry multiple GameplayTagContainers. Designers will edit the Added and Removed GameplayTagContainers for each category and the result will show up in the Combined GameplayTagContainer on compilation. Added tags are new tags that this GameplayEffect adds that its parents did not previously have. Removed tags are tags that parent classes have but this subclass does not have.

Category Description
Gameplay Effect Asset Tags Tags that the GameplayEffect has. They do not do any function on their own and serve only the purpose of describing the GameplayEffect.
Granted Tags Tags that live on the GameplayEffect but are also given to the ASC that the GameplayEffect is applied to. They are removed from the ASC when the GameplayEffect is removed. This only works for Duration and Infinite GameplayEffects.
Ongoing Tag Requirements Once applied, these tags determine whether the GameplayEffect is on or off. A GameplayEffect can be off and still be applied. If a GameplayEffect is off due to failing the Ongoing Tag Requirements, but the requirements are then met, the GameplayEffect will turn on again and reapply its modifiers. This only works for Duration and Infinite GameplayEffects.
Application Tag Requirements Tags on the Target that determine if a GameplayEffect can be applied to the Target. If these requirements are not met, the GameplayEffect is not applied.
Remove Gameplay Effects with Tags GameplayEffects on the Target that have any of these tags in their Asset Tags or Granted Tags will be removed from the Target when this GameplayEffect is successfully applied.

⬆ Back to Top

4.5.8 Immunity

GameplayEffects can grant immunity, effectively blocking the application of other GameplayEffects, based on GameplayTags. While immunity can be effectively achieved through other means like Application Tag Requirements, using this system provides a delegate for when GameplayEffects are blocked due to immunity UAbilitySystemComponent::OnImmunityBlockGameplayEffectDelegate.

GrantedApplicationImmunityTags checks if the Source ASC (including tags from the Source ability's AbilityTags if there was one) has any of the specified tags. This is a way to provide immunity from all GameplayEffects from certain characters or sources based on their tags.

Granted Application Immunity Query checks the incoming GameplayEffectSpec if it matches any of the queries to block or allow its application.

The queries have helpful hover tooltips in the GameplayEffect Blueprint.

⬆ Back to Top

4.5.9 Gameplay Effect Spec

The GameplayEffectSpec (GESpec) can be thought of as the instantiations of GameplayEffects. They hold a reference to the GameplayEffect class that they represent, what level it was created at, and who created it. These can be freely created and modified at runtime before application unlike GameplayEffects which should be created by designers prior to runtime. When applying a GameplayEffect, a GameplayEffectSpec is created from the GameplayEffect and that is actually what is applied to the Target.

GameplayEffectSpecs are created from GameplayEffects using UAbilitySystemComponent::MakeOutgoingSpec() which is BlueprintCallable. GameplayEffectSpecs do not have to be immediately applied. It is common to pass a GameplayEffectSpec to a projectile created from an ability that the projectile can apply to the target it hits later. When GameplayEffectSpecs are successfully applied, they return a new struct called FActiveGameplayEffect.

Notable GameplayEffectSpec Contents:

  • The GameplayEffect class that this GameplayEffect was created from.
  • The level of this GameplayEffectSpec. Usually the same as the level of the ability that created the GameplayEffectSpec but can be different.
  • The duration of the GameplayEffectSpec. Defaults to the duration of the GameplayEffect but can be different.
  • The period of the GameplayEffectSpec for periodic effects. Defaults to the period of the GameplayEffect but can be different.
  • The current stack count of this GameplayEffectSpec. The stack limit is on the GameplayEffect.
  • The GameplayEffectContextHandle tells us who created this GameplayEffectSpec.
  • Attributes that were captured at the time of the GameplayEffectSpec's creation due to snapshotting.
  • DynamicGrantedTags that the GameplayEffectSpec grants to the Target in addition to the GameplayTags that the GameplayEffect grants.
  • DynamicAssetTags that the GameplayEffectSpec has in addition to the AssetTags that the GameplayEffect has.
  • SetByCaller TMaps.

⬆ Back to Top

4.5.9.1 SetByCallers

SetByCallers allow the GameplayEffectSpec to carry float values associated with a GameplayTag or FName around. They are stored in their respective TMaps: TMap<FGameplayTag, float> and TMap<FName, float> on the GameplayEffectSpec. These can be used to as Modifiers on the GameplayEffect or as generic means of ferrying floats around. It is common to pass numerical data generated inside of an ability to GameplayEffectExecutionCalculations or ModifierMagnitudeCalculations via SetByCallers.

SetByCaller Use Notes
Modifiers Must be defined ahead of time in the GameplayEffect class. Can only use the GameplayTag version. If one is defined on the GameplayEffect class but the GameplayEffectSpec does not have the corresponding tag and float value pair, the game will have a runtime error on application of the GameplayEffectSpec and return 0. This is a potential problem for a Divide operation. See Modifiers.
Elsewhere Does not need to be defined ahead of time anywhere. Reading a SetByCaller that does not exist on a GameplayEffectSpec can return a developer defined default value with optional warnings.

To assign SetByCaller values in Blueprint, use the Blueprint node for the version that you need (GameplayTag or FName):

Assigning SetByCaller

To read a SetByCaller value in Blueprint, you will need to make custom nodes in your Blueprint Library.

To assign SetByCaller values in C++, use the version of the function that you need (GameplayTag or FName):

void FGameplayEffectSpec::SetSetByCallerMagnitude(FName DataName, float Magnitude);
void FGameplayEffectSpec::SetSetByCallerMagnitude(FGameplayTag DataTag, float Magnitude);

To read a SetByCaller value in C++, use the version of the function that you need (GameplayTag or FName):

float GetSetByCallerMagnitude(FName DataName, bool WarnIfNotFound = true, float DefaultIfNotFound = 0.f) const;
float GetSetByCallerMagnitude(FGameplayTag DataTag, bool WarnIfNotFound = true, float DefaultIfNotFound = 0.f) const;

I recommend using the GameplayTag version over the FName version. This can prevent spelling errors in Blueprint and GameplayTags are more efficient to send over the network when the GameplayEffectSpec replicates than FNames since the TMaps replicate too.

⬆ Back to Top

4.5.10 Gameplay Effect Context

The GameplayEffectContext structure holds information about a GameplayEffectSpec's instigator and TargetData. This is also a good structure to subclass to pass arbitrary data around between places like ModifierMagnitudeCalculations / GameplayEffectExecutionCalculations, AttributeSets, and GameplayCues.

To subclass the GameplayEffectContext:

  1. Subclass FGameplayEffectContext
  2. Override FGameplayEffectContext::GetScriptStruct()
  3. Override FGameplayEffectContext::Duplicate()
  4. Override FGameplayEffectContext::NetSerialize() if your new data needs to be replicated
  5. Implement TStructOpsTypeTraits for your subclass, like the parent struct FGameplayEffectContext has
  6. Override AllocGameplayEffectContext() in your AbilitySystemGlobals class to return a new object of your subclass

GASShooter uses a subclassed GameplayEffectContext to add TargetData which can be accessed in GameplayCues, specifically for the shotgun since it can hit more than one enemy.

⬆ Back to Top

4.5.11 Modifier Magnitude Calculation

ModifierMagnitudeCalculations (ModMagCalc or MMC) are powerful classes used as Modifiers in GameplayEffects. They function similarly to GameplayEffectExecutionCalculations but are less powerful and most importantly they can be predicted. Their sole purpose is to return a float value from CalculateBaseMagnitude_Implementation(). You can subclass and override this function in Blueprint and C++.

MMCs can be used in any duration of GameplayEffects - Instant, Duration, Infinite, or Periodic.

MMCs' strength lies in their capability to capture the value of any number of Attributes on the Source or the Target of GameplayEffect with full access to the GameplayEffectSpec to read GameplayTags and SetByCallers. Attributes can either be snapshotted or not. Snapshotted Attributes are captured when the GameplayEffectSpec is created whereas non snapshotted Attributes are captured when the GameplayEffectSpec is applied and automatically update when the Attribute changes for Infinite and Duration GameplayEffects. Capturing Attributes recalculates their CurrentValue from existing mods on the ASC. This recalculation will not run PreAttributeChange() in the AbilitySet so any clamping must be done here again.

Snapshot Source or Target Captured on GameplayEffectSpec Automatically updates when Attribute changes for Infinite or Duration GE
Yes Source Creation No
Yes Target Application No
No Source Application Yes
No Target Application Yes

The resultant float from an MMC can further be modified in the GameplayEffect's Modifier by a coefficient and a pre and post coefficient addition.

An example MMC that captures the Target's mana Attribute reduces it from a poison effect where the amount reduced changes depending on how much mana the Target has and a tag that the Target might have:

UPAMMC_PoisonMana::UPAMMC_PoisonMana()
{

	//ManaDef defined in header FGameplayEffectAttributeCaptureDefinition ManaDef;
	ManaDef.AttributeToCapture = UPAAttributeSetBase::GetManaAttribute();
	ManaDef.AttributeSource = EGameplayEffectAttributeCaptureSource::Target;
	ManaDef.bSnapshot = false;

	//MaxManaDef defined in header FGameplayEffectAttributeCaptureDefinition MaxManaDef;
	MaxManaDef.AttributeToCapture = UPAAttributeSetBase::GetMaxManaAttribute();
	MaxManaDef.AttributeSource = EGameplayEffectAttributeCaptureSource::Target;
	MaxManaDef.bSnapshot = false;

	RelevantAttributesToCapture.Add(ManaDef);
	RelevantAttributesToCapture.Add(MaxManaDef);
}

float UPAMMC_PoisonMana::CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec & Spec) const
{
	// Gather the tags from the source and target as that can affect which buffs should be used
	const FGameplayTagContainer* SourceTags = Spec.CapturedSourceTags.GetAggregatedTags();
	const FGameplayTagContainer* TargetTags = Spec.CapturedTargetTags.GetAggregatedTags();

	FAggregatorEvaluateParameters EvaluationParameters;
	EvaluationParameters.SourceTags = SourceTags;
	EvaluationParameters.TargetTags = TargetTags;

	float Mana = 0.f;
	GetCapturedAttributeMagnitude(ManaDef, Spec, EvaluationParameters, Mana);
	Mana = FMath::Max<float>(Mana, 0.0f);

	float MaxMana = 0.f;
	GetCapturedAttributeMagnitude(MaxManaDef, Spec, EvaluationParameters, MaxMana);
	MaxMana = FMath::Max<float>(MaxMana, 1.0f); // Avoid divide by zero

	float Reduction = -20.0f;
	if (Mana / MaxMana > 0.5f)
	{
		// Double the effect if the target has more than half their mana
		Reduction *= 2;
	}
	
	if (TargetTags->HasTagExact(FGameplayTag::RequestGameplayTag(FName("Status.WeakToPoisonMana"))))
	{
		// Double the effect if the target is weak to PoisonMana
		Reduction *= 2;
	}
	
	return Reduction;
}

If you don't add the FGameplayEffectAttributeCaptureDefinition to RelevantAttributesToCapture in the MMC's constructor and try to capture Attributes, you will get an error about a missing Spec while capturing. If you don't need to capture Attributes, then you don't have to add anything to RelevantAttributesToCapture.

⬆ Back to Top

4.5.12 Gameplay Effect Execution Calculation

GameplayEffectExecutionCalculations (ExecutionCalculation, Execution (you will often see this term in the plugin's source code), or ExecCalc) are the most powerful way for GameplayEffects to make changes to an ASC. Like ModifierMagnitudeCalculations, these can capture Attributes and optionally snapshot them. Unlike MMCs, these can change more than one Attribute and essentially do anything else that the programmer wants. The downside to this power and flexibility is that they can not be predicted and they must be implemented in C++.

ExecutionCalculations can only be used with Instant and Periodic GameplayEffects. Anything with the word 'Execute' in it typically refers to these two types of GameplayEffects.

Snapshotting captures the Attribute when the GameplayEffectSpec is created whereas not snapshotting captures the Attribute when the GameplayEffectSpec is applied. Capturing Attributes recalculates their CurrentValue from existing mods on the ASC. This recalculation will not run PreAttributeChange() in the AbilitySet so any clamping must be done here again.

Snapshot Source or Target Captured on GameplayEffectSpec
Yes Source Creation
Yes Target Application
No Source Application
No Target Application

To set up Attribute capture, we follow a pattern set by Epic's ActionRPG Sample Project by defining a struct holding and defining how we capture the Attributes and creating one copy of it in the struct's constructor. You will have a struct like this for every ExecCalc. Note: Each struct needs a unique name as they share the same namespace. Using the same name for the structs will cause incorrect behavior in capturing your Attributes (mostly capturing the values of the wrong Attributes).

For Local Predicted, Server Only, and Server Initiated GameplayAbilities, the ExecCalc only calls on the Server.

Calculating damage received based on a complex formula reading from many attributes on the Source and the Target is the most common example of an ExecCalc. The included Sample Project has a simple ExecCalc for calculating damage that reads the value of damage from the GameplayEffectSpec's SetByCaller and then mitigates that value based on the armor Attribute captured from the Target. See GDDamageExecCalculation.cpp/.h.

⬆ Back to Top

4.5.12.1 Sending Data to Execution Calculations

There are a few ways to send data to an ExecutionCalculation in addition to capturing Attributes.

4.5.12.1.1 SetByCaller

Any SetByCallers set on the GameplayEffectSpec can be directly read in the ExecutionCalculation.

const FGameplayEffectSpec& Spec = ExecutionParams.GetOwningSpec();
float Damage = FMath::Max<float>(Spec.GetSetByCallerMagnitude(FGameplayTag::RequestGameplayTag(FName("Data.Damage")), false, -1.0f), 0.0f);

4.5.12.1.2 Backing Data Attribute Calculation Modifier

If you want to hardcode values to a GameplayEffect, you can pass them in using a CalculationModifier that uses one of the captured Attributes as the backing data.

In this screenshot example, we're adding 50 to the captured Damage Attribute. You could also set this to Override to just take in only the hardcoded value.

Backing Data Attribute Calculation Modifier

The ExecutionCalculation reads this value in when it captures the Attribute.

float Damage = 0.0f;
// Capture optional damage value set on the damage GE as a CalculationModifier under the ExecutionCalculation
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().DamageDef, EvaluationParameters, Damage);

4.5.12.1.3 Backing Data Temporary Variable Calculation Modifier

If you want to hardcode values to a GameplayEffect, you can pass them in using a CalculationModifier that uses a Temporary Variable or Transient Aggregator as it's called in C++. The Temporary Variable is associated with a GameplayTag.

In this screenshot example, we're adding 50 to a Temporary Variable using the Data.Damage GameplayTag.

Backing Data Temporary Variable Calculation Modifier

Add backing Temporary Variables to your ExecutionCalculation's constructor:

ValidTransientAggregatorIdentifiers.AddTag(FGameplayTag::RequestGameplayTag("Data.Damage"));

The ExecutionCalculation reads this value in using special capture functions similar to the Attribute capture functions.

float Damage = 0.0f;
ExecutionParams.AttemptCalculateTransientAggregatorMagnitude(FGameplayTag::RequestGameplayTag("Data.Damage"), EvaluationParameters, Damage);

4.5.12.1.4 Gameplay Effect Context

You can send data to the ExecutionCalculation via a custom GameplayEffectContext on the GameplayEffectSpec.

In the ExecutionCalculation you can access the EffectContext from the FGameplayEffectCustomExecutionParameters.

const FGameplayEffectSpec& Spec = ExecutionParams.GetOwningSpec();
FGSGameplayEffectContext* ContextHandle = static_cast<FGSGameplayEffectContext*>(Spec.GetContext().Get());

If you need change something on the GameplayEffectSpec or the EffectContext:

FGameplayEffectSpec* MutableSpec = ExecutionParams.GetOwningSpecForPreExecuteMod();
FGSGameplayEffectContext* ContextHandle = static_cast<FGSGameplayEffectContext*>(MutableSpec->GetContext().Get());

Use caution if modifying the GameplayEffectSpec in the ExecutionCalculation. See the comment for GetOwningSpecForPreExecuteMod().

/** Non const access. Be careful with this, especially when modifying a spec after attribute capture. */
FGameplayEffectSpec* GetOwningSpecForPreExecuteMod() const;

⬆ Back to Top

4.5.13 Custom Application Requirement

CustomApplicationRequirement (CAR) classes give the designers advanced control over whether a GameplayEffect can be applied versus the simple GameplayTag checks on the GameplayEffect. These can be implemented in Blueprint by overriding CanApplyGameplayEffect() and in C++ by overriding CanApplyGameplayEffect_Implementation().

Examples of when to use CARs:

  • Target needs to have a certain amount of an Attribute
  • Target needs to have a certain number of stacks of a GameplayEffect

CARs can also do more advanced things like checking if an instance of this GameplayEffect is already on the Target and changing the duration of the existing instance instead of applying a new instance (return false for CanApplyGameplayEffect()).

⬆ Back to Top

4.5.14 Cost Gameplay Effect

GameplayAbilities have an optional GameplayEffect specifically designed to use as the cost of the ability. Costs are how much of an Attribute an ASC needs to have to be able to activate the GameplayAbility. If a GA cannot afford the Cost GE, then they will not be able to activate. This Cost GE should be an Instant GameplayEffect with one or more Modifiers that subtract from Attributes. By default, Cost GEs are meant to be predicted and it is recommended to maintain that capability meaning do not use ExecutionCalculations. MMCs are perfectly acceptable and encouraged for complex cost calculations.

When starting out, you will most likely have one unique Cost GE per GA that has a cost. A more advanced technique is to reuse one Cost GE for multiple GAs and just modify the GameplayEffectSpec created from the Cost GE with the GA-specific data (the cost value is defined on the GA). This only works for Instanced abilities.

Two techniques for reusing the Cost GE:

  1. Use an MMC. This is the easiest method. Create an MMC that reads the cost value from the GameplayAbility instance which you can get from the GameplayEffectSpec.
float UPGMMC_HeroAbilityCost::CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec & Spec) const
{
	const UPGGameplayAbility* Ability = Cast<UPGGameplayAbility>(Spec.GetContext().GetAbilityInstance_NotReplicated());

	if (!Ability)
	{
		return 0.0f;
	}

	return Ability->Cost.GetValueAtLevel(Ability->GetAbilityLevel());
}

In this example the cost value is an FScalableFloat on the GameplayAbility child class that I added to it.

UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Cost")
FScalableFloat Cost;

Cost GE With MMC

  1. Override UGameplayAbility::GetCostGameplayEffect(). Override this function and create a GameplayEffect at runtime that reads the cost value on the GameplayAbility.

⬆ Back to Top

4.5.15 Cooldown Gameplay Effect

GameplayAbilities have an optional GameplayEffect specifically designed to use as the cooldown of the ability. Cooldowns determine how long after activation the ability can be activated again. If a GA is still on cooldown, it cannot activate. This Cooldown GE should be a Duration GameplayEffect with no Modifiers and a unique GameplayTag per GameplayAbility or per ability slot (if your game has interchangeable abilities assigned to slots that share a cooldown) in the GameplayEffect's GrantedTags ("Cooldown Tag"). The GA actually checks for the presence for the Cooldown Tag instead of the presence of the Cooldown GE. By default, Cooldown GEs are meant to be predicted and it is recommended to maintain that capability meaning do not use ExecutionCalculations. MMCs are perfectly acceptable and encouraged for complex cooldown calculations.

When starting out, you will most likely have one unique Cooldown GE per GA that has a cooldown. A more advanced technique is to reuse one Cooldown GE for multiple GAs and just modify the GameplayEffectSpec created from the Cooldown GE with the GA-specific data (the cooldown duration and the Cooldown Tag are defined on the GA). This only works for Instanced abilities.

Two techniques for reusing the Cooldown GE:

  1. Use a SetByCaller. This is the easiest method. Set the duration of your shared Cooldown GE to SetByCaller with a GameplayTag. On your GameplayAbility subclass, define a float / FScalableFloat for the duration, a FGameplayTagContainer for the unique Cooldown Tag, and a temporary FGameplayTagContainer that we will use as the return pointer of the union of our Cooldown Tag and the Cooldown GE's tags.
UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Cooldown")
FScalableFloat CooldownDuration;

UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Cooldown")
FGameplayTagContainer CooldownTags;

// Temp container that we will return the pointer to in GetCooldownTags().
// This will be a union of our CooldownTags and the Cooldown GE's cooldown tags.
UPROPERTY()
FGameplayTagContainer TempCooldownTags;

Then override UGameplayAbility::GetCooldownTags() to return the union of our Cooldown Tags and any existing Cooldown GE's tags.

const FGameplayTagContainer * UPGGameplayAbility::GetCooldownTags() const
{
	FGameplayTagContainer* MutableTags = const_cast<FGameplayTagContainer*>(&TempCooldownTags);
	const FGameplayTagContainer* ParentTags = Super::GetCooldownTags();
	if (ParentTags)
	{
		MutableTags->AppendTags(*ParentTags);
	}
	MutableTags->AppendTags(CooldownTags);
	return MutableTags;
}

Finally, override UGameplayAbility::ApplyCooldown() to inject our Cooldown Tags and to add the SetByCaller to the cooldown GameplayEffectSpec.

void UPGGameplayAbility::ApplyCooldown(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo * ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo) const
{
	UGameplayEffect* CooldownGE = GetCooldownGameplayEffect();
	if (CooldownGE)
	{
		FGameplayEffectSpecHandle SpecHandle = MakeOutgoingGameplayEffectSpec(CooldownGE->GetClass(), GetAbilityLevel());
		SpecHandle.Data.Get()->DynamicGrantedTags.AppendTags(CooldownTags);
		SpecHandle.Data.Get()->SetSetByCallerMagnitude(FGameplayTag::RequestGameplayTag(FName(  OurSetByCallerTag  )), CooldownDuration.GetValueAtLevel(GetAbilityLevel()));
		ApplyGameplayEffectSpecToOwner(Handle, ActorInfo, ActivationInfo, SpecHandle);
	}
}

In this picture, the cooldown's duration Modifier is set to SetByCaller with a Data Tag of Data.Cooldown. Data.Cooldown would be OurSetByCallerTag in the code above.

Cooldown GE with SetByCaller

  1. Use an MMC. This has the same setup as above except for setting the SetByCaller as the duration on the Cooldown GE and in ApplyCooldown. Instead, set the duration to be a Custom Calculation Class and point to the new MMC that we will make.
UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Cooldown")
FScalableFloat CooldownDuration;

UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Cooldown")
FGameplayTagContainer CooldownTags;

// Temp container that we will return the pointer to in GetCooldownTags().
// This will be a union of our CooldownTags and the Cooldown GE's cooldown tags.
UPROPERTY()
FGameplayTagContainer TempCooldownTags;

Then override UGameplayAbility::GetCooldownTags() to return the union of our Cooldown Tags and any existing Cooldown GE's tags.

const FGameplayTagContainer * UPGGameplayAbility::GetCooldownTags() const
{
	FGameplayTagContainer* MutableTags = const_cast<FGameplayTagContainer*>(&TempCooldownTags);
	const FGameplayTagContainer* ParentTags = Super::GetCooldownTags();
	if (ParentTags)
	{
		MutableTags->AppendTags(*ParentTags);
	}
	MutableTags->AppendTags(CooldownTags);
	return MutableTags;
}

Finally, override UGameplayAbility::ApplyCooldown() to inject our Cooldown Tags into the cooldown GameplayEffectSpec.

void UPGGameplayAbility::ApplyCooldown(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo * ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo) const
{
	UGameplayEffect* CooldownGE = GetCooldownGameplayEffect();
	if (CooldownGE)
	{
		FGameplayEffectSpecHandle SpecHandle = MakeOutgoingGameplayEffectSpec(CooldownGE->GetClass(), GetAbilityLevel());
		SpecHandle.Data.Get()->DynamicGrantedTags.AppendTags(CooldownTags);
		ApplyGameplayEffectSpecToOwner(Handle, ActorInfo, ActivationInfo, SpecHandle);
	}
}
float UPGMMC_HeroAbilityCooldown::CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec & Spec) const
{
	const UPGGameplayAbility* Ability = Cast<UPGGameplayAbility>(Spec.GetContext().GetAbilityInstance_NotReplicated());

	if (!Ability)
	{
		return 0.0f;
	}

	return Ability->CooldownDuration.GetValueAtLevel(Ability->GetAbilityLevel());
}

Cooldown GE with MMC

⬆ Back to Top

4.5.15.1 Get the Cooldown Gameplay Effect's Remaining Time
bool APGPlayerState::GetCooldownRemainingForTag(FGameplayTagContainer CooldownTags, float & TimeRemaining, float & CooldownDuration)
{
	if (AbilitySystemComponent && CooldownTags.Num() > 0)
	{
		TimeRemaining = 0.f;
		CooldownDuration = 0.f;

		FGameplayEffectQuery const Query = FGameplayEffectQuery::MakeQuery_MatchAnyOwningTags(CooldownTags);
		TArray< TPair<float, float> > DurationAndTimeRemaining = AbilitySystemComponent->GetActiveEffectsTimeRemainingAndDuration(Query);
		if (DurationAndTimeRemaining.Num() > 0)
		{
			int32 BestIdx = 0;
			float LongestTime = DurationAndTimeRemaining[0].Key;
			for (int32 Idx = 1; Idx < DurationAndTimeRemaining.Num(); ++Idx)
			{
				if (DurationAndTimeRemaining[Idx].Key > LongestTime)
				{
					LongestTime = DurationAndTimeRemaining[Idx].Key;
					BestIdx = Idx;
				}
			}

			TimeRemaining = DurationAndTimeRemaining[BestIdx].Key;
			CooldownDuration = DurationAndTimeRemaining[BestIdx].Value;

			return true;
		}
	}

	return false;
}

Note: Querying the cooldown's time remaining on clients requires that they can receive replicated GameplayEffects. This will depend on their ASC's replication mode.

4.5.15.2 Listening for Cooldown Begin and End

To listen for when a cooldown begins, you can either respond to when the Cooldown GE is applied by binding to AbilitySystemComponent->OnActiveGameplayEffectAddedDelegateToSelf or when the Cooldown Tag is added by binding to AbilitySystemComponent->RegisterGameplayTagEvent(CooldownTag, EGameplayTagEventType::NewOrRemoved). I recommend listening for when the Cooldown GE is added because you also have access to the GameplayEffectSpec that applied it. From this you can determine if the Cooldown GE is the locally predicted one or the Server's correcting one.

To listen for when a cooldown ends, you can either respond to when the Cooldown GE is removed by binding to AbilitySystemComponent->OnAnyGameplayEffectRemovedDelegate() or when the Cooldown Tag is removed by binding to AbilitySystemComponent->RegisterGameplayTagEvent(CooldownTag, EGameplayTagEventType::NewOrRemoved). I recommend listening for when the Cooldown Tag is removed because when the Server's corrected Cooldown GE comes in, it will remove our locally predicted one causing the OnAnyGameplayEffectRemovedDelegate() to fire even though we're still on cooldown. The Cooldown Tag will not change during the removal of the predicted Cooldown GE and the application of the Server's corrected Cooldown GE.

Note: Listening for a GameplayEffect to be added or removed on clients requires that they can receive replicated GameplayEffects. This will depend on their ASC's replication mode.

The Sample Project includes a custom Blueprint node that listens for cooldowns beginning and ending. The HUD UMG Widget uses it to update the amount of time remaining on the Meteor's cooldown. This AsyncTask will live forever until manually called EndTask(), which we do in the UMG Widget's Destruct event. See AsyncTaskEffectCooldownChanged.h/cpp.

Listen for Cooldown Change BP Node

4.5.15.3 Predicting Cooldowns

Cooldowns cannot really be predicted currently. We can start UI cooldown timer's when the locally predicted Cooldown GE is applied but the GameplayAbility's actual cooldown is tied to the server's cooldown's time remaining. Depending on the player's latency, the locally predicted cooldown could expire but the GameplayAbility would still be on cooldown on the server and this would prevent the GameplayAbility's immediate re-activation until the server's cooldown expires.

The Sample Project handles this by graying out the Meteor ability's UI icon when the locally predicted cooldown begins and then starting the cooldown timer once the server's corrected Cooldown GE comes in.

A gameplay consequence of this is that players with high latencies have a lower rate of fire on short cooldown abilities than players with lower latencies putting them at a disadvantage. Fortnite avoids this by their weapons having custom bookkeeping that do not use cooldown GameplayEffects.

Allowing for true predicted cooldowns (player could activate a GameplayAbility when the local cooldown expires but the server is still on cooldown) is something that Epic would like to implement someday in a future iteration of GAS.

⬆ Back to Top

4.5.16 Changing Active Gameplay Effect Duration

To change the time remaining for a Cooldown GE or any Duration GameplayEffect, we need to change the GameplayEffectSpec's Duration, update its StartServerWorldTime, update its CachedStartServerWorldTime, update its StartWorldTime, and rerun the check on the duration with CheckDuration(). Doing this on the server and marking the FActiveGameplayEffect dirty will replicate the changes to clients. Note: This does involve a const_cast and may not be Epic's intended way of changing durations, but it seems to work well so far.

bool UPAAbilitySystemComponent::SetGameplayEffectDurationHandle(FActiveGameplayEffectHandle Handle, float NewDuration)
{
	if (!Handle.IsValid())
	{
		return false;
	}

	const FActiveGameplayEffect* ActiveGameplayEffect = GetActiveGameplayEffect(Handle);
	if (!ActiveGameplayEffect)
	{
		return false;
	}

	FActiveGameplayEffect* AGE = const_cast<FActiveGameplayEffect*>(ActiveGameplayEffect);
	if (NewDuration > 0)
	{
		AGE->Spec.Duration = NewDuration;
	}
	else
	{
		AGE->Spec.Duration = 0.01f;
	}

	AGE->StartServerWorldTime = ActiveGameplayEffects.GetServerWorldTime();
	AGE->CachedStartServerWorldTime = AGE->StartServerWorldTime;
	AGE->StartWorldTime = ActiveGameplayEffects.GetWorldTime();
	ActiveGameplayEffects.MarkItemDirty(*AGE);
	ActiveGameplayEffects.CheckDuration(Handle);

	AGE->EventSet.OnTimeChanged.Broadcast(AGE->Handle, AGE->StartWorldTime, AGE->GetDuration());
	OnGameplayEffectDurationChange(*AGE);

	return true;
}

⬆ Back to Top

4.5.17 Creating Dynamic Gameplay Effects at Runtime

Creating Dynamic GameplayEffects at runtime is an advanced topic. You shouldn't have to do this too often.

Only Instant GameplayEffects can be created at runtime from scratch in C++. Duration and Infinite GameplayEffects cannot be created dynamically at runtime because when they replicate they look for the GameplayEffect class definition that does not exist. To achieve this functionality, you should instead make an archetype GameplayEffect class like you would normally do in the Editor. Then customize the GameplayEffectSpec instance with what you need at runtime.

Instant GameplayEffects created at runtime can also be called from within a local predicted GameplayAbility. However, it is unknown yet if the dynamic creation can have side effects.

Examples

The Sample Project creates one to send the gold and experience points back to the killer of a character when it takes the killing blow in its AttributeSet.

// Create a dynamic instant Gameplay Effect to give the bounties
UGameplayEffect* GEBounty = NewObject<UGameplayEffect>(GetTransientPackage(), FName(TEXT("Bounty")));
GEBounty->DurationPolicy = EGameplayEffectDurationType::Instant;

int32 Idx = GEBounty->Modifiers.Num();
GEBounty->Modifiers.SetNum(Idx + 2);

FGameplayModifierInfo& InfoXP = GEBounty->Modifiers[Idx];
InfoXP.ModifierMagnitude = FScalableFloat(GetXPBounty());
InfoXP.ModifierOp = EGameplayModOp::Additive;
InfoXP.Attribute = UGDAttributeSetBase::GetXPAttribute();

FGameplayModifierInfo& InfoGold = GEBounty->Modifiers[Idx + 1];
InfoGold.ModifierMagnitude = FScalableFloat(GetGoldBounty());
InfoGold.ModifierOp = EGameplayModOp::Additive;
InfoGold.Attribute = UGDAttributeSetBase::GetGoldAttribute();

Source->ApplyGameplayEffectToSelf(GEBounty, 1.0f, Source->MakeEffectContext());

A second example shows a runtime GameplayEffect created within a local predicted GameplayAbility. Use at your own risk (see comments in code)!

UGameplayAbilityRuntimeGE::UGameplayAbilityRuntimeGE()
{
	NetExecutionPolicy = EGameplayAbilityNetExecutionPolicy::LocalPredicted;
}

void UGameplayAbilityRuntimeGE::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData)
{
	if (HasAuthorityOrPredictionKey(ActorInfo, &ActivationInfo))
	{
		if (!CommitAbility(Handle, ActorInfo, ActivationInfo))
		{
			EndAbility(Handle, ActorInfo, ActivationInfo, true, true);
		}

		// Create the GE at runtime.
		UGameplayEffect* GameplayEffect = NewObject<UGameplayEffect>(GetTransientPackage(), TEXT("RuntimeInstantGE"));
		GameplayEffect->DurationPolicy = EGameplayEffectDurationType::Instant; // Only instant works with runtime GE.

		// Add a simple scalable float modifier, which overrides MyAttribute with 42.
		// In real world applications, consume information passed via TriggerEventData.
		const int32 Idx = GameplayEffect->Modifiers.Num();
		GameplayEffect->Modifiers.SetNum(Idx + 1);
		FGameplayModifierInfo& ModifierInfo = GameplayEffect->Modifiers[Idx];
		ModifierInfo.Attribute.SetUProperty(UMyAttributeSet::GetMyModifiedAttribute());
		ModifierInfo.ModifierMagnitude = FScalableFloat(42.f);
		ModifierInfo.ModifierOp = EGameplayModOp::Override;

		// Apply the GE.

		// Create the GESpec here to avoid the behavior of ASC to create GESpecs from the GE class default object.
		// Since we have a dynamic GE here, this would create a GESpec with the base GameplayEffect class, so we
		// would lose our modifiers. Attention: It is unknown, if this "hack" done here can have drawbacks!
		// The spec prevents the GE object being collected by the GarbageCollector, since the GE is a UPROPERTY on the spec.
		FGameplayEffectSpec* GESpec = new FGameplayEffectSpec(GameplayEffect, {}, 0.f); // "new", since lifetime is managed by a shared ptr within the handle
		ApplyGameplayEffectSpecToOwner(Handle, ActorInfo, ActivationInfo, FGameplayEffectSpecHandle(GESpec));
	}
	EndAbility(Handle, ActorInfo, ActivationInfo, false, false);
}

⬆ Back to Top

4.5.18 Gameplay Effect Containers

Epic's Action RPG Sample Project implements a structure called FGameplayEffectContainer. These are not in vanilla GAS but are extremely handy for containing GameplayEffects and TargetData. It automates a some of the effort like creating GameplayEffectSpecs from GameplayEffects and setting default values in its GameplayEffectContext. Making a GameplayEffectContainer in a GameplayAbility and passing it to spawned projectiles is very easy and straightforward. I opted not to implement the GameplayEffectContainers in the included Sample Project to show how you would work without them in vanilla GAS, but I highly recommend looking into them and considering adding them to your project.

To access the GESpecs inside of the GameplayEffectContainers to do things like adding SetByCallers, break the FGameplayEffectContainer and access the GESpec reference by its index in the array of GESpecs. This requires that you know the index ahead of time of the GESpec that you want to access.

SetByCaller with a GameplayEffectContainer

GameplayEffectContainers also contain an optional efficient means of targeting.

⬆ Back to Top

4.6 Gameplay Abilities

4.6.1 Gameplay Ability Definition

GameplayAbilities (GA) are any actions or skills that an Actor can do in the game. More than one GameplayAbility can be active at one time for example sprinting and shooting a gun. These can be made in Blueprint or C++.

Examples of GameplayAbilities:

  • Jumping
  • Sprinting
  • Shooting a gun
  • Passively blocking an attack every X number of seconds
  • Using a potion
  • Opening a door
  • Collecting a resource
  • Constructing a building

Things that should not be implemented with GameplayAbilities:

  • Basic movement input
  • Some interactions with UIs - Don't use a GameplayAbility to purchase an item from a store.

These are not rules, just my recommendations. Your design and implementations may vary.

GameplayAbilities come with default functionality to have a level to modify the amount of change to attributes or to change the GameplayAbility's functionality.

GameplayAbilities run on the owning client and/or the server depending on the Net Execution Policy but not simulated proxies. The Net Execution Policy determines if a GameplayAbility will be locally predicted. They include default behavior for optional cost and cooldown GameplayEffects. GameplayAbilities use AbilityTasks for actions that happen over time like waiting for an event, waiting for an attribute change, waiting for players to choose a target, or moving a Character with Root Motion Source. Simulated clients will not run GameplayAbilities. Instead, when the server runs the ability, anything that visually needs to play on the simulated proxies (like animation montages) will be replicated or RPC'd through AbilityTasks or GameplayCues for cosmetic things like sounds and particles.

All GameplayAbilities will have their ActivateAbility() function overriden with your gameplay logic. Additional logic can be added to EndAbility() that runs when the GameplayAbility completes or is canceled.

Flowchart of a simple GameplayAbility: Simple GameplayAbility Flowchart

Flowchart of a more complex GameplayAbility: Complex GameplayAbility Flowchart

Complex abilities can be implemented using multiple GameplayAbilities that interact (activate, cancel, etc) with each other.

4.6.1.1 Replication Policy

Don't use this option. The name is misleading and you don't need it. GameplayAbilitySpecs are replicated from the server to the owning client by default. As mentioned above, GameplayAbilities don't run on simulated proxies. They use AbilityTasks and GameplayCues to replicate or RPC visual changes to the simulated proxies. Dave Ratti from Epic has stated his desire to remove this option in the future.

4.6.1.2 Server Respects Remote Ability Cancellation

This option causes trouble more often than not. It means if the client's GameplayAbility ends either due to cancellation or natural completion, it will force the server's version to end whether it completed or not. The latter issue is the important one, especially for locally predicted GameplayAbilities used by players with high latencies. Generally you will want to disable this option.

4.6.1.3 Replicate Input Directly

Setting this option will always replicate input press and release events to the server. Epic recommends not using this and instead relying on the Generic Replicated Events that are built into the existing input related AbilityTasks if you have your input bound to your ASC.

Epic's comment:

/** Direct Input state replication. These will be called if bReplicateInputDirectly is true on the ability and is generally not a good thing to use. (Instead, prefer to use Generic Replicated Events). */
UAbilitySystemComponent::ServerSetInputPressed()

⬆ Back to Top

4.6.2 Binding Input to the ASC

The ASC allows you to directly bind input actions to it and assign those inputs to GameplayAbilities when you grant them. Input actions assigned to GameplayAbilities automatically activate those GameplayAbilities when pressed if the GameplayTag requirements are met. Assigned input actions are required to use the built-in AbilityTasks that respond to input.

In addition to input actions assigned to activate GameplayAbilities, the ASC also accepts generic Confirm and Cancel inputs. These special inputs are used by AbilityTasks for confirming things like Target Actors or canceling them.

To bind input to an ASC, you must first create an enum that translates the input action name to a byte. The enum name must match exactly to the name used for the input action in the project settings. The DisplayName does not matter.

From the Sample Project:

UENUM(BlueprintType)
enum class EGDAbilityInputID : uint8
{
	// 0 None
	None			UMETA(DisplayName = "None"),
	// 1 Confirm
	Confirm			UMETA(DisplayName = "Confirm"),
	// 2 Cancel
	Cancel			UMETA(DisplayName = "Cancel"),
	// 3 LMB
	Ability1		UMETA(DisplayName = "Ability1"),
	// 4 RMB
	Ability2		UMETA(DisplayName = "Ability2"),
	// 5 Q
	Ability3		UMETA(DisplayName = "Ability3"),
	// 6 E
	Ability4		UMETA(DisplayName = "Ability4"),
	// 7 R
	Ability5		UMETA(DisplayName = "Ability5"),
	// 8 Sprint
	Sprint			UMETA(DisplayName = "Sprint"),
	// 9 Jump
	Jump			UMETA(DisplayName = "Jump")
};

If your ASC lives on the Character, then in SetupPlayerInputComponent() include the function for binding to the ASC:

// Bind to AbilitySystemComponent
AbilitySystemComponent->BindAbilityActivationToInputComponent(PlayerInputComponent, FGameplayAbilityInputBinds(FString("ConfirmTarget"), FString("CancelTarget"), FString("EGDAbilityInputID"), static_cast<int32>(EGDAbilityInputID::Confirm), static_cast<int32>(EGDAbilityInputID::Cancel)));

If your ASC lives on the PlayerState, there is a potential race condition inside of SetupPlayerInputComponent() where the PlayerState may not have replicated to the client yet. Therefore, I recommend attempting to bind to input in SetupPlayerInputComponent() and OnRep_PlayerState(). OnRep_PlayerState() is not sufficient by itself because there could be a case where the Actor's InputComponent could be null when PlayerState replicates before the PlayerController tells the client to call ClientRestart() which creates the InputComponent. The Sample Project demonstrates attempting to bind in both locations with a boolean gating the process so it only actually binds the input once.

Note: In the Sample Project Confirm and Cancel in the enum don't match the input action names in the project settings (ConfirmTarget and CancelTarget), but we supply the mapping between them in BindAbilityActivationToInputComponent(). These are special since we supply the mapping and they don't have to match, but they can match. All other inputs in the enum must match the input action names in the project settings.

For GameplayAbilities that will only ever be activated by one input (they will always exist in the same "slot" like a MOBA), I prefer to add a variable to my UGameplayAbility subclass where I can define their input. I can then read this from the ClassDefaultObject when granting the ability.

4.6.2.1 Binding to Input without Activating Abilities

If you don't want your GameplayAbilities to automatically activate when an input is pressed but still bind them to input to use with AbilityTasks, you can add a new bool variable to your UGameplayAbility subclass, bActivateOnInput, that defaults to true and override UAbilitySystemComponent::AbilityLocalInputPressed().

void UGSAbilitySystemComponent::AbilityLocalInputPressed(int32 InputID)
{
	// Consume the input if this InputID is overloaded with GenericConfirm/Cancel and the GenericConfim/Cancel callback is bound
	if (IsGenericConfirmInputBound(InputID))
	{
		LocalInputConfirm();
		return;
	}

	if (IsGenericCancelInputBound(InputID))
	{
		LocalInputCancel();
		return;
	}

	// ---------------------------------------------------------

	ABILITYLIST_SCOPE_LOCK();
	for (FGameplayAbilitySpec& Spec : ActivatableAbilities.Items)
	{
		if (Spec.InputID == InputID)
		{
			if (Spec.Ability)
			{
				Spec.InputPressed = true;
				if (Spec.IsActive())
				{
					if (Spec.Ability->bReplicateInputDirectly && IsOwnerActorAuthoritative() == false)
					{
						ServerSetInputPressed(Spec.Handle);
					}

					AbilitySpecInputPressed(Spec);

					// Invoke the InputPressed event. This is not replicated here. If someone is listening, they may replicate the InputPressed event to the server.
					InvokeReplicatedEvent(EAbilityGenericReplicatedEvent::InputPressed, Spec.Handle, Spec.ActivationInfo.GetActivationPredictionKey());
				}
				else
				{
					UGSGameplayAbility* GA = Cast<UGSGameplayAbility>(Spec.Ability);
					if (GA && GA->bActivateOnInput)
					{
						// Ability is not active, so try to activate it
						TryActivateAbility(Spec.Handle);
					}
				}
			}
		}
	}
}

⬆ Back to Top

4.6.3 Granting Abilities

Granting a GameplayAbility to an ASC adds it to the ASC's list of ActivatableAbilities allowing it to activate the GameplayAbility at will if it meets the GameplayTag requirements.

We grant GameplayAbilities on the server which then automatically replicates the GameplayAbilitySpec to the owning client. Other clients / simulated proxies do not receive the GameplayAbilitySpec.

The Sample Project stores a TArray<TSubclassOf<UGDGameplayAbility>> on the Character class that it reads from and grants when the game starts:

void AGDCharacterBase::AddCharacterAbilities()
{
	// Grant abilities, but only on the server	
	if (Role != ROLE_Authority || !AbilitySystemComponent.IsValid() || AbilitySystemComponent->CharacterAbilitiesGiven)
	{
		return;
	}

	for (TSubclassOf<UGDGameplayAbility>& StartupAbility : CharacterAbilities)
	{
		AbilitySystemComponent->GiveAbility(
			FGameplayAbilitySpec(StartupAbility, GetAbilityLevel(StartupAbility.GetDefaultObject()->AbilityID), static_cast<int32>(StartupAbility.GetDefaultObject()->AbilityInputID), this));
	}

	AbilitySystemComponent->CharacterAbilitiesGiven = true;
}

When granting these GameplayAbilities, we're creating GameplayAbilitySpecs with the UGameplayAbility class, the ability level, the input that it is bound to, and the SourceObject or who gave this GameplayAbility to this ASC.

⬆ Back to Top

4.6.4 Activating Abilities

If a GameplayAbility is assigned an input action, it will be automatically activated if the input is pressed and it meets its GameplayTag requirements. This may not always be the desirable way to activate a GameplayAbility. The ASC provides four other methods of activating GameplayAbilities: by GameplayTag, GameplayAbility class, GameplayAbilitySpec handle, and by an event. Activating a GameplayAbility by event allows you to pass in a payload of data with the event.

UFUNCTION(BlueprintCallable, Category = "Abilities")
bool TryActivateAbilitiesByTag(const FGameplayTagContainer& GameplayTagContainer, bool bAllowRemoteActivation = true);

UFUNCTION(BlueprintCallable, Category = "Abilities")
bool TryActivateAbilityByClass(TSubclassOf<UGameplayAbility> InAbilityToActivate, bool bAllowRemoteActivation = true);

bool TryActivateAbility(FGameplayAbilitySpecHandle AbilityToActivate, bool bAllowRemoteActivation = true);

bool TriggerAbilityFromGameplayEvent(FGameplayAbilitySpecHandle AbilityToTrigger, FGameplayAbilityActorInfo* ActorInfo, FGameplayTag Tag, const FGameplayEventData* Payload, UAbilitySystemComponent& Component);

FGameplayAbilitySpecHandle GiveAbilityAndActivateOnce(const FGameplayAbilitySpec& AbilitySpec);

To activate a GameplayAbility by event, the GameplayAbility must have its Triggers set up in the GameplayAbility. Assign a GameplayTag and pick an option for GameplayEvent. To send the event, use the function UAbilitySystemBlueprintLibrary::SendGameplayEventToActor(AActor* Actor, FGameplayTag EventTag, FGameplayEventData Payload). Activating a GameplayAbility by event allows you to pass in a payload with data.

GameplayAbility Triggers also allow you to activate the GameplayAbility when a GameplayTag is added or removed.

Note: When activating a GameplayAbility from event in Blueprint, you must use the ActivateAbilityFromEvent node and the standard ActivateAbility node cannot exist in your graph. If the ActivateAbility node exists, it will always be called over the ActivateAbilityFromEvent node.

Note: Don't forget to call EndAbility() when the GameplayAbility should terminate unless you have a GameplayAbility that will always run like a passive ability.

Activation sequence for locally predicted GameplayAbilities:

  1. Owning client calls TryActivateAbility()
  2. Calls InternalTryActivateAbility()
  3. Calls CanActivateAbility() and returns whether GameplayTag requirements are met, if the ASC can afford the cost, if the GameplayAbility is not on cooldown, and if no other instances are currently active
  4. Calls CallServerTryActivateAbility() and passes it the Prediction Key that it generates
  5. Calls CallActivateAbility()
  6. Calls PreActivate() Epic refers to this as "boilerplate init stuff"
  7. Calls ActivateAbility() finally activating the ability

Server receives CallServerTryActivateAbility()

  1. Calls ServerTryActivateAbility()
  2. Calls InternalServerTryActivateAbility()
  3. Calls InternalTryActivateAbility()
  4. Calls CanActivateAbility() and returns whether GameplayTag requirements are met, if the ASC can afford the cost, if the GameplayAbility is not on cooldown, and if no other instances are currently active
  5. Calls ClientActivateAbilitySucceed() if successful telling it to update its ActivationInfo that its activation was confirmed by the server and broadcasting the OnConfirmDelegate delegate. This is not the same as input confirmation.
  6. Calls CallActivateAbility()
  7. Calls PreActivate() Epic refers to this as "boilerplate init stuff"
  8. Calls ActivateAbility() finally activating the ability

If at any time the server fails to activate, it will call ClientActivateAbilityFailed(), immediately terminating the client's GameplayAbility and undoing any predicted changes.

4.6.4.1 Passive Abilities

To implement passive GameplayAbilities that automatically activate and run continuously, override UGameplayAbility::OnAvatarSet() which is automatically called when a GameplayAbility is granted and the AvatarActor is set and call TryActivateAbility().

I recommend adding a bool to your custom UGameplayAbility class specifying if the GameplayAbility should be activated when granted. The Sample Project does this for its passive armor stacking ability.

Passive GameplayAbilities will typically have a Net Execution Policy of Server Only.

void UGDGameplayAbility::OnAvatarSet(const FGameplayAbilityActorInfo * ActorInfo, const FGameplayAbilitySpec & Spec)
{
	Super::OnAvatarSet(ActorInfo, Spec);

	if (ActivateAbilityOnGranted)
	{
		bool ActivatedAbility = ActorInfo->AbilitySystemComponent->TryActivateAbility(Spec.Handle, false);
	}
}

Epic describes this function as the correct place to initiate passive abilities and to do BeginPlay type things.

⬆ Back to Top

4.6.5 Canceling Abilities

To cancel a GameplayAbility from within, you call CancelAbility(). This will call EndAbility() and set its WasCancelled parameter to true.

To cancel a GameplayAbility externally, the ASC provides a few functions:

/** Cancels the specified ability CDO. */
void CancelAbility(UGameplayAbility* Ability);	

/** Cancels the ability indicated by passed in spec handle. If handle is not found among reactivated abilities nothing happens. */
void CancelAbilityHandle(const FGameplayAbilitySpecHandle& AbilityHandle);

/** Cancel all abilities with the specified tags. Will not cancel the Ignore instance */
void CancelAbilities(const FGameplayTagContainer* WithTags=nullptr, const FGameplayTagContainer* WithoutTags=nullptr, UGameplayAbility* Ignore=nullptr);

/** Cancels all abilities regardless of tags. Will not cancel the ignore instance */
void CancelAllAbilities(UGameplayAbility* Ignore=nullptr);

/** Cancels all abilities and kills any remaining instanced abilities */
virtual void DestroyActiveState();

Note: I have found that CancelAllAbilities doesn't seem to work right if you have a Non-Instanced GameplayAbilities. It seems to hit the Non-Instanced GameplayAbility and give up. CancelAbilities can handle Non-Instanced GameplayAbilities better and that is what the Sample Project uses (Jump is a non-instanced GameplayAbility). Your mileage may vary.

⬆ Back to Top

4.6.6 Getting Active Abilities

Beginners often ask "How can I get the active ability?" perhaps to set variables on it or to cancel it. More than one GameplayAbility can be active at a time so there is no one "active ability". Instead, you must search through an ASC's list of ActivatableAbilities (granted GameplayAbilities that the ASC owns) and find the one matching the Asset or Granted GameplayTag that you are looking for.

UAbilitySystemComponent::GetActivatableAbilities() returns a TArray<FGameplayAbilitySpec> for you to iterate over.

The ASC also has another helper function that takes in a GameplayTagContainer as a parameter to assist in searching instead of manually iterating over the list of GameplayAbilitySpecs. The bOnlyAbilitiesThatSatisfyTagRequirements parameter will only return GameplayAbilitySpecs that satisfy their GameplayTag requirements and could be activated right now. For example, you could have two basic attack GameplayAbilities, one with a weapon and one with bare fists, and the correct one activates depending on if a weapon is equipped setting the GameplayTag requirement. See Epic's comment on the function for more information.

UAbilitySystemComponent::GetActivatableGameplayAbilitySpecsByAllMatchingTags(const FGameplayTagContainer& GameplayTagContainer, TArray < struct FGameplayAbilitySpec* >& MatchingGameplayAbilities, bool bOnlyAbilitiesThatSatisfyTagRequirements = true)

Once you get the FGameplayAbilitySpec that you are looking for, you can call IsActive() on it.

⬆ Back to Top

4.6.7 Instancing Policy

A GameplayAbility's Instancing Policy determines if and how the GameplayAbility is instanced when activated.

Instancing Policy Description Example of when to use
Instanced Per Actor Each ASC only has one instance of the GameplayAbility that is reused between activations. This will probably be the Instancing Policy that you use the most. You can use it for any ability and provides persistence between activations. The designer is responsible for manually resetting any variables between activations that need it.
Instanced Per Execution Every time a GameplayAbility is activated, a new instance of the GameplayAbility is created. The benefit of these GameplayAbilities is that the variables are reset everytime you activate. These provide worse performance than Instanced Per Actor since they will spawn new GameplayAbilities every time they activate. The Sample Project does not use any of these.
Non-Instanced The GameplayAbility operates on its ClassDefaultObject. No instances are created. This has the best performance of the three but is the most restrictive in what can be done with it. Non-Instanced GameplayAbilities cannot store state, meaning no dynamic variables and no binding to AbilityTask delegates. The best place to use them is for frequently used simple abilities like minion basic attacks in a MOBA or RTS. The Sample Project's Jump GameplayAbility is Non-Instanced.

⬆ Back to Top

4.6.8 Net Execution Policy

A GameplayAbility's Net Execution Policy determines who runs the GameplayAbility and in what order.

Net Execution Policy Description
Local Only The GameplayAbility is only run on the owning client. This could be useful for abilities that only make local cosmetic changes. Single player games should use Server Only.
Local Predicted Local Predicted GameplayAbilities activate first on the owning client and then on the server. The server's version will correct anything that the client predicted incorrectly. See Prediction.
Server Only The GameplayAbility is only run on the server. Passive GameplayAbilities will typically be Server Only. Single player games should use this.
Server Initiated Server Initiated GameplayAbilities activate first on the server and then on the owning client. I personally haven't used these much if any.

⬆ Back to Top

4.6.9 Ability Tags

GameplayAbilities come with GameplayTagContainers with built-in logic. None of these GameplayTags are replicated.

GameplayTag Container Description
Ability Tags GameplayTags that the GameplayAbility owns. These are just GameplayTags to describe the GameplayAbility.
Cancel Abilities with Tag Other GameplayAbilities that have these GameplayTags in their Ability Tags will be canceled when this GameplayAbility is activated.
Block Abilities with Tag Other GameplayAbilities that have these GameplayTags in their Ability Tags are blocked from activating while this GameplayAbility is active.
Activation Owned Tags These GameplayTags are given to the GameplayAbility's owner while this GameplayAbility is active. Remember these are not replicated.
Activation Required Tags This GameplayAbility can only be activated if the owner has all of these GameplayTags.
Activation Blocked Tags This GameplayAbility cannot be activated if the owner has any of these GameplayTags.
Source Required Tags This GameplayAbility can only be activated if the Source has all of these GameplayTags. The Source GameplayTags are only set if the GameplayAbility is triggered by an event.
Source Blocked Tags This GameplayAbility cannot be activated if the Source has any of these GameplayTags. The Source GameplayTags are only set if the GameplayAbility is triggered by an event.
Target Required Tags This GameplayAbility can only be activated if the Target has all of these GameplayTags. The Target GameplayTags are only set if the GameplayAbility is triggered by an event.
Target Blocked Tags This GameplayAbility cannot be activated if the Target has any of these GameplayTags. The Target GameplayTags are only set if the GameplayAbility is triggered by an event.

⬆ Back to Top

4.6.10 Gameplay Ability Spec

A GameplayAbilitySpec exists on the ASC after a GameplayAbility is granted and defines the activatable GameplayAbility - GameplayAbility class, level, input bindings, and runtime state that must be kept separate from the GameplayAbility class.

When a GameplayAbility is granted on the server, the server replicates the GameplayAbilitySpec to the owning client so that she may activate it.

Activating a GameplayAbilitySpec will create an instance (or not for Non-Instanced GameplayAbilities) of the GameplayAbility depending on its Instancing Policy.

⬆ Back to Top

4.6.11 Passing Data to Abilities

The general paradigm for GameplayAbilities is Activate->Generate Data->Apply->End. Sometimes you need to act on existing data. GAS provides a few options for getting external data into your GameplayAbilities:

Method Description
Activate GameplayAbility by Event Activate a GameplayAbility with an event containing a payload of data. The event's payload is replicated from client to server for local predicted GameplayAbilities. Use the two Optional Object or the TargetData variables for arbitrary data that does not fit any of the existing variables. The downside to this is that it prevents you from activating the ability with an input bind. To activate a GameplayAbility by event, the GameplayAbility must have its Triggers set up in the GameplayAbility. Assign a GameplayTag and pick an option for GameplayEvent. To send the event, use the function UAbilitySystemBlueprintLibrary::SendGameplayEventToActor(AActor* Actor, FGameplayTag EventTag, FGameplayEventData Payload).
Use WaitGameplayEvent AbilityTask Use the WaitGameplayEvent AbilityTask to tell the GameplayAbility to listen for an event with payload data after it activates. The event payload and the process to send it is the same as activating GameplayAbilities by event. The downside to this is that events are not replicated by the AbilityTask and should only be used for Local Only and Server Only GameplayAbilities. You potentially could write your own AbilityTask that will replicate the event payload.
Use TargetData A custom TargetData struct is a good way to pass arbitrary data between the client and server.
Store Data on the OwnerActor or AvatarActor Use replicated variables stored on the OwnerActor, AvatarActor, or any other object that you can get a reference to. This method is the most flexible and will work with GameplayAbilities activated by input binds. However, it does not guarantee the data will be synchronized from replication at the time of use. You must ensure that ahead of time - meaning if you set a replicated variable and then immediately activate a GameplayAbility there is no guarantee the order that will happen on the receiver due to potential packet loss.

⬆ Back to Top

4.6.12 Ability Cost and Cooldown

GameplayAbilities come with functionality for optional costs and cooldowns. Costs are predefined amounts of Attributes that the ASC must have in order to activate the GameplayAbility implemented with an Instant GameplayEffect (Cost GE). Cooldowns are timers that prevent the reactivation of a GameplayAbility until it expires and is implemented with a Duration GameplayEffect (Cooldown GE).

Before a GameplayAbility calls UGameplayAbility::Activate(), it calls UGameplayAbility::CanActivateAbility(). This function checks if the owning ASC can afford the cost (UGameplayAbility::CheckCost()) and ensures that the GameplayAbility is not on cooldown (UGameplayAbility::CheckCooldown()).

After a GameplayAbility calls Activate(), it can optionally commit the cost and cooldown at any time using UGameplayAbility::CommitAbility() which calls UGameplayAbility::CommitCost() and UGameplayAbility::CommitCooldown(). The designer may choose to call CommitCost() or CommitCooldown() separately if they shouldn't be committed at the same time. Committing cost and cooldown calls CheckCost() and CheckCooldown() one more time and is the last chance for the GameplayAbility to fail related to them. The owning ASC's Attributes could potentially change after a GameplayAbility is activated, failing to meet the cost at time of commit. Committing the cost and cooldown can be locally predicted if the prediction key is valid at the time of commit.

See CostGE and CooldownGE for implementation details.

⬆ Back to Top

4.6.13 Leveling Up Abilities

There are two common methods for leveling up an ability:

Level Up Method Description
Ungrant and Regrant at the New Level Ungrant (remove) the GameplayAbility from the ASC and regrant it back at the next level on the server. This terminates the GameplayAbility if it was active at the time.
Increase the GameplayAbilitySpec's Level On the server, find the GameplayAbilitySpec, increase its level, and mark it dirty so that replicates to the owning client. This method does not terminate the GameplayAbility if it was active at the time.

The main difference between the two methods is if you want active GameplayAbilities to be canceled at the time of level up. You will most likely use both methods depending on your GameplayAbilities. I recommend adding a bool to your UGameplayAbility subclass specifying which method to use.

⬆ Back to Top

4.6.14 Ability Sets

GameplayAbilitySets are convenience UDataAsset classes for holding input bindings and lists of startup GameplayAbilities for Characters with logic to grant the GameplayAbilities. Subclasses can also include extra logic or properties. Paragon had a GameplayAbilitySet per hero that included all of their given GameplayAbilities.

I find this class to be unnecessary at least given what I've seen of it so far. The Sample Project handles all of the functionality of GameplayAbilitySets inside of the GDCharacterBase and its subclasses.

⬆ Back to Top

4.6.15 Ability Batching

Traditional Gameplay Ability lifecycle involves a minimum of two or three RPCs from the client to the server.

  1. CallServerTryActivateAbility()
  2. ServerSetReplicatedTargetData() (Optional)
  3. ServerEndAbility()

If a GameplayAbility performs all of these actions in one atomic grouping in a frame, we can optimize this workflow to batch (combine) all two or three RPCs into one RPC. GAS refers to this RPC optimization as Ability Batching. The common example of when to use Ability Batching is for hitscan guns. Hitscan guns activate, do a line trace, send the TargetData to the server, and end the ability all in one atomic group in one frame. The GASShooter sample project demonstrates this technique for its hitscan guns.

Semi-Automatic guns are the best case scenario and batch the CallServerTryActivateAbility(), ServerSetReplicatedTargetData() (the bullet hit result), and ServerEndAbility() into one RPC instead of three RPCs.

Full-Automatic/Burst guns batch CallServerTryActivateAbility() and ServerSetReplicatedTargetData() for the first bullet into one RPC instead of two RPCs. Each subsequent bullet is its own ServerSetReplicatedTargetData() RPC. Finally, ServerEndAbility() is sent as a separate RPC when the gun stops firing. This is a worst case scenario where we only save one RPC on the first bullet instead of two. This scenario could have also been implemented with activating the ability via a Gameplay Event which would send the bullet's TargetData in with the EventPayload to the server from the client. The downside of the latter approach is that the TargetData would have to be generated externally to the ability whereas the batching approach generates the TargetData inside of the ability.

Ability Batching is disabled by default on the ASC. To enable Ability Batching, override ShouldDoServerAbilityRPCBatch() to return true:

virtual bool ShouldDoServerAbilityRPCBatch() const override { return true; }

Now that Ability Batching is enabled, before activating abilities that you want batched, you must create a FScopedServerAbilityRPCBatcher struct beforehand. This special struct will try to batch any abilities following it within its scope. Once the FScopedServerAbilityRPCBatcher falls out of scope, any abilities activated will not try to batch. FScopedServerAbilityRPCBatcher works by having special code in each of the functions that can be batched that intercepts the call from sending the RPC and instead packs the message into a batch struct. When FScopedServerAbilityRPCBatcher falls out of scope, it automatically RPCs this batch struct to the server in UAbilitySystemComponent::EndServerAbilityRPCBatch(). The server receives the batch RPC in UAbilitySystemComponent::ServerAbilityRPCBatch_Internal(FServerAbilityRPCBatch& BatchInfo). The BatchInfo parameter will contain flags for if the ability should end and if input was pressed at the time of activation and the TargetData if that was included. This is a good function to put a breakpoint on to confirm that your batching is working properly. Alternatively, use the cvar AbilitySystem.ServerRPCBatching.Log 1 to enable special ability batching logging.

This mechanism can only be done in C++ and can only activate abilities by their FGameplayAbilitySpecHandle.

bool UGSAbilitySystemComponent::BatchRPCTryActivateAbility(FGameplayAbilitySpecHandle InAbilityHandle, bool EndAbilityImmediately)
{
	bool AbilityActivated = false;
	if (InAbilityHandle.IsValid())
	{
		FScopedServerAbilityRPCBatcher GSAbilityRPCBatcher(this, InAbilityHandle);
		AbilityActivated = TryActivateAbility(InAbilityHandle, true);

		if (EndAbilityImmediately)
		{
			FGameplayAbilitySpec* AbilitySpec = FindAbilitySpecFromHandle(InAbilityHandle);
			if (AbilitySpec)
			{
				UGSGameplayAbility* GSAbility = Cast<UGSGameplayAbility>(AbilitySpec->GetPrimaryInstance());
				GSAbility->ExternalEndAbility();
			}
		}

		return AbilityActivated;
	}

	return AbilityActivated;
}

GASShooter reuses the same batched GameplayAbility for semi-automatic and full-automatic guns which never directly call EndAbility() (it is handled outside of the ability by a local-only ability that manages player input and the call to the batched ability based on the current firemode). Since all of the RPCs must happen within the scope of the FScopedServerAbilityRPCBatcher, I provide the EndAbilityImmediately parameter so that the controlling/managing local-only can specify whether this ability should batch the EndAbility() call (semi-automatic), or not batch the EndAbility() call (full-automatic) and the EndAbility() call will happen sometime later in its own RPC.

GASShooter exposes a Blueprint node to allow batching abilities which the aforementioned local-only ability uses to trigger the batched ability.

Activate Batched Ability

⬆ Back to Top

4.6.15 Net Security Policy

A GameplayAbility's NetSecurityPolicy determines where should an ability execute on the network. It provides protection from clients attempting to execute restricted abilities.

NetSecurityPolicy Description
ClientOrServer No security requirements. Client or server can trigger execution and termination of this ability freely.
ServerOnlyExecution A client requesting execution of this ability will be ignored by the server. Clients can still request that the server cancel or end this ability.
ServerOnlyTermination A client requesting cancellation or ending of this ability will be ignored by the server. Clients can still request execution of the ability.
ServerOnly Server controls both execution and termination of this ability. A client making any requests will be ignored.

⬆ Back to Top

4.7 Ability Tasks

4.7.1 Ability Task Definition

GameplayAbilities only execute in one frame. This does not allow for much flexibility on its own. To do actions that happen over time or require responding to delegates fired at some point later in time we use latent actions called AbilityTasks.

GAS comes with many AbilityTasks out of the box:

  • Tasks for moving Characters with RootMotionSource
  • A task for playing animation montages
  • Tasks for responding to Attribute changes
  • Tasks for responding to GameplayEffect changes
  • Tasks for responding to player input
  • and more

The UAbilityTask constructor enforces a hardcoded game-wide maximum of 1000 concurrent AbilityTasks running at the same time. Keep this in mind when designing GameplayAbilities for games that can have hundreds of characters in the world at the same time like RTS games.

⬆ Back to Top

4.7.2 Custom Ability Tasks

Often you will be creating your own custom AbilityTasks (in C++). The Sample Project comes with two custom AbilityTasks:

  1. PlayMontageAndWaitForEvent is a combination of the default PlayMontageAndWait and WaitGameplayEvent AbilityTasks. This allows animation montages to send gameplay events from AnimNotifies back to the GameplayAbility that started them. Use this to trigger actions at specific times during animation montages.
  2. WaitReceiveDamage listens for the OwnerActor to receive damage. The passive armor stacks GameplayAbility removes a stack of armor when the hero receives an instance of damage.

AbilityTasks are composed of:

  • A static function that creates new instances of the AbilityTask
  • Delegates that are broadcasted on when the AbilityTask completes its purpose
  • An Activate() function to start its main job, bind to external delegates, etc.
  • An OnDestroy() function for cleanup, including external delegates that it bound to
  • Callback functions for any external delegates that it bound to
  • Member variables and any internal helper functions

Note: AbilityTasks can only declare one type of output delegate. All of your output delegates must be of this type, regardless if they use the parameters or not. Pass default values for unused delegate parameters.

AbilityTasks only run on the Client or Server that is running the owning GameplayAbility; however, AbilityTasks can be set to run on simulated clients by setting bSimulatedTask = true; in the AbilityTask constructor, overriding virtual void InitSimulatedTask(UGameplayTasksComponent& InGameplayTasksComponent);, and setting any member variables to be replicated. This is only useful in rare situations like movement AbilityTasks where you don't want to replicate every movement change but instead simulate the entire movement AbilityTask. All of the RootMotionSource AbilityTasks do this. See AbilityTask_MoveToLocation.h/.cpp as an example.

AbilityTasks can Tick if you set bTickingTask = true; in the AbilityTask constructor and override virtual void TickTask(float DeltaTime);. This is useful when you need to lerp values smoothly across frames. See AbilityTask_MoveToLocation.h/.cpp as an example.

⬆ Back to Top

4.7.3 Using Ability Tasks

To create and activate an AbilityTask in C++ (From GDGA_FireGun.cpp):

UGDAT_PlayMontageAndWaitForEvent* Task = UGDAT_PlayMontageAndWaitForEvent::PlayMontageAndWaitForEvent(this, NAME_None, MontageToPlay, FGameplayTagContainer(), 1.0f, NAME_None, false, 1.0f);
Task->OnBlendOut.AddDynamic(this, &UGDGA_FireGun::OnCompleted);
Task->OnCompleted.AddDynamic(this, &UGDGA_FireGun::OnCompleted);
Task->OnInterrupted.AddDynamic(this, &UGDGA_FireGun::OnCancelled);
Task->OnCancelled.AddDynamic(this, &UGDGA_FireGun::OnCancelled);
Task->EventReceived.AddDynamic(this, &UGDGA_FireGun::EventReceived);
Task->ReadyForActivation();

In Blueprint, we just use the Blueprint node that we create for the AbilityTask. We don't have to call ReadyForActivate(). That is automatically called by Engine/Source/Editor/GameplayTasksEditor/Private/K2Node_LatentGameplayTaskCall.cpp. K2Node_LatentGameplayTaskCall also automatically calls BeginSpawningActor() and FinishSpawningActor() if they exist in your AbilityTask class (see AbilityTask_WaitTargetData). To reiterate, K2Node_LatentGameplayTaskCall only does automagic sorcery for Blueprint. In C++, we have to manually call ReadyForActivation(), BeginSpawningActor(), and FinishSpawningActor().

Blueprint WaitTargetData AbilityTask

To manually cancel an AbilityTask, just call EndTask() on the AbilityTask object in Blueprint (called Async Task Proxy) or in C++.

⬆ Back to Top

4.7.4 Root Motion Source Ability Tasks

GAS comes with AbilityTasks for moving Characters over time for things like knockbacks, complex jumps, pulls, and dashes using Root Motion Sources hooked into the CharacterMovementComponent.

Note: Predicting RootMotionSource AbilityTasks works up to engine version 4.19 and 4.25+. Prediction is bugged for engine versions 4.20-4.24; however, the AbilityTasks still perform their function in multiplayer with minor net corrections and work perfectly in single player. It is possible to cherry pick the prediction fix from 4.25 into a custom 4.20-4.24 engine.

⬆ Back to Top

4.8 Gameplay Cues

4.8.1 Gameplay Cue Definition

GameplayCues (GC) execute non-gameplay related things like sound effects, particle effects, camera shakes, etc. GameplayCues are typically replicated (unless explicitly Executed, Added, or Removed locally) and predicted.

We trigger GameplayCues by sending a corresponding GameplayTag with the mandatory parent name of GameplayCue. and an event type (Execute, Add, or Remove) to the GameplayCueManager via the ASC. GameplayCueNotify objects and other Actors that implement the IGameplayCueInterface can subscribe to these events based on the GameplayCue's GameplayTag (GameplayCueTag).

Note: Just to reiterate, GameplayCue GameplayTags need to start with the parent GameplayTag of GameplayCue. So for example, a valid GameplayCue GameplayTag might be GameplayCue.A.B.C.

There are two classes of GameplayCueNotifies, Static and Actor. They respond to different events and different types of GameplayEffects can trigger them. Override the corresponding event with your logic.

GameplayCue Class Event GameplayEffect Type Description
GameplayCueNotify_Static Execute Instant or Periodic Static GameplayCueNotifies operate on the ClassDefaultObject (meaning no instances) and are perfect for one-off effects like hit impacts.
GameplayCueNotify_Actor Add or Remove Duration or Infinite Actor GameplayCueNotifies spawn a new instance when Added. Because these are instanced, they can do actions over time until they are Removed. These are good for looping sounds and particle effects that will be removed when the backing Duration or Infinite GameplayEffect is removed or by manually calling remove. These also come with options to manage how many are allowed to be Added at the same so that multiple applications of the same effect only start the sounds or particles once.

GameplayCueNotifies technically can respond to any of the events but this is typically how we use them.

Note: When using GameplayCueNotify_Actor, check Auto Destroy on Remove otherwise subsequent calls to Add that GameplayCueTag won't work.

When using an ASC Replication Mode other than Full, Add and Remove GC events will fire twice on Server players (listen server) - once for applying the GE and again from the "Minimal" NetMultiCast to the clients. However, WhileActive events will still only fire once. All events will only fire once on clients.

The Sample Project includes a GameplayCueNotify_Actor for stun and sprint effects. It also has a GameplayCueNotify_Static for the FireGun's projectile impact. These GCs can be optimized further by triggering them locally instead of replicating them through a GE. I opted for showing the beginner way of using them in the Sample Project.

⬆ Back to Top

4.8.2 Triggering Gameplay Cues

From inside of a GameplayEffect when it is successfully applied (not blocked by tags or immunity), fill in the GameplayTags of all the GameplayCues that should be triggered.

GameplayCue Triggered from a GameplayEffect

UGameplayAbility offers Blueprint nodes to Execute, Add, or Remove GameplayCues.

GameplayCue Triggered from a GameplayAbility

In C++, you can call functions directly on the ASC (or expose them to Blueprint in your ASC subclass):

/** GameplayCues can also come on their own. These take an optional effect context to pass through hit result, etc */
void ExecuteGameplayCue(const FGameplayTag GameplayCueTag, FGameplayEffectContextHandle EffectContext = FGameplayEffectContextHandle());
void ExecuteGameplayCue(const FGameplayTag GameplayCueTag, const FGameplayCueParameters& GameplayCueParameters);

/** Add a persistent gameplay cue */
void AddGameplayCue(const FGameplayTag GameplayCueTag, FGameplayEffectContextHandle EffectContext = FGameplayEffectContextHandle());
void AddGameplayCue(const FGameplayTag GameplayCueTag, const FGameplayCueParameters& GameplayCueParameters);

/** Remove a persistent gameplay cue */
void RemoveGameplayCue(const FGameplayTag GameplayCueTag);
	
/** Removes any GameplayCue added on its own, i.e. not as part of a GameplayEffect. */
void RemoveAllGameplayCues();

⬆ Back to Top

4.8.3 Local Gameplay Cues

The exposed functions for firing GameplayCues from GameplayAbilities and the ASC are replicated by default. Each GameplayCue event is a multicast RPC. This can cause a lot of RPCs. GAS also enforces a maximum of two of the same GameplayCue RPCs per net update. We avoid this by using local GameplayCues where we can. Local GameplayCues only Execute, Add, or Remove on the individual client.

Scenarios where we can use local GameplayCues:

  • Projectile impacts
  • Melee collision impacts
  • GameplayCues fired from animation montages

Local GameplayCue functions that you should add to your ASC subclass:

UFUNCTION(BlueprintCallable, Category = "GameplayCue", Meta = (AutoCreateRefTerm = "GameplayCueParameters", GameplayTagFilter = "GameplayCue"))
void ExecuteGameplayCueLocal(const FGameplayTag GameplayCueTag, const FGameplayCueParameters& GameplayCueParameters);

UFUNCTION(BlueprintCallable, Category = "GameplayCue", Meta = (AutoCreateRefTerm = "GameplayCueParameters", GameplayTagFilter = "GameplayCue"))
void AddGameplayCueLocal(const FGameplayTag GameplayCueTag, const FGameplayCueParameters& GameplayCueParameters);

UFUNCTION(BlueprintCallable, Category = "GameplayCue", Meta = (AutoCreateRefTerm = "GameplayCueParameters", GameplayTagFilter = "GameplayCue"))
void RemoveGameplayCueLocal(const FGameplayTag GameplayCueTag, const FGameplayCueParameters& GameplayCueParameters);
void UPAAbilitySystemComponent::ExecuteGameplayCueLocal(const FGameplayTag GameplayCueTag, const FGameplayCueParameters & GameplayCueParameters)
{
	UAbilitySystemGlobals::Get().GetGameplayCueManager()->HandleGameplayCue(GetOwner(), GameplayCueTag, EGameplayCueEvent::Type::Executed, GameplayCueParameters);
}

void UPAAbilitySystemComponent::AddGameplayCueLocal(const FGameplayTag GameplayCueTag, const FGameplayCueParameters & GameplayCueParameters)
{
	UAbilitySystemGlobals::Get().GetGameplayCueManager()->HandleGameplayCue(GetOwner(), GameplayCueTag, EGameplayCueEvent::Type::OnActive, GameplayCueParameters);
	UAbilitySystemGlobals::Get().GetGameplayCueManager()->HandleGameplayCue(GetOwner(), GameplayCueTag, EGameplayCueEvent::Type::WhileActive, GameplayCueParameters);
}

void UPAAbilitySystemComponent::RemoveGameplayCueLocal(const FGameplayTag GameplayCueTag, const FGameplayCueParameters & GameplayCueParameters)
{
	UAbilitySystemGlobals::Get().GetGameplayCueManager()->HandleGameplayCue(GetOwner(), GameplayCueTag, EGameplayCueEvent::Type::Removed, GameplayCueParameters);
}

If a GameplayCue was Added locally, it should be Removed locally. If it was Added via replication, it should be Removed via replication.

⬆ Back to Top

4.8.4 Gameplay Cue Parameters

GameplayCues receive a FGameplayCueParameters structure containing extra information for the GameplayCue as a parameter. If you manually trigger the GameplayCue from a function on the GameplayAbility or the ASC, then you must manually fill in the GameplayCueParameters structure that is passed to the GameplayCue. If the GameplayCue is triggered by a GameplayEffect, then the following variables are automatically filled in on the GameplayCueParameters structure:

  • AggregatedSourceTags
  • AggregatedTargetTags
  • GameplayEffectLevel
  • AbilityLevel
  • EffectContext
  • Magnitude (if the GameplayEffect has an Attribute for magnitude selected in the dropdown above the GameplayCue tag container and a corresponding Modifier that affects that Attribute)

The SourceObject variable in the GameplayCueParameters structure is potentially a good place to pass arbitrary data to the GameplayCue when triggering the GameplayCue manually.

Note: Some of the variables in the parameters structure like Instigator might already exist in the EffectContext. The EffectContext can also contain a FHitResult for location of where to spawn the GameplayCue in the world. Subclassing EffectContext is potentially a good way to pass more data to GameplayCues, especially those triggered by a GameplayEffect.

See the 3 functions in UAbilitySystemGlobals that populate the GameplayCueParameters structure for more information. They are virtual so you can override them to autopopulate more information.

/** Initialize GameplayCue Parameters */
virtual void InitGameplayCueParameters(FGameplayCueParameters& CueParameters, const FGameplayEffectSpecForRPC &Spec);
virtual void InitGameplayCueParameters_GESpec(FGameplayCueParameters& CueParameters, const FGameplayEffectSpec &Spec);
virtual void InitGameplayCueParameters(FGameplayCueParameters& CueParameters, const FGameplayEffectContextHandle& EffectContext);

⬆ Back to Top

4.8.5 Gameplay Cue Manager

By default, the GameplayCueManager will scan the entire game directory for GameplayCueNotifies and load them into memory on play. We can change the path where the GameplayCueManager scans by setting it in the DefaultGame.ini.

[/Script/GameplayAbilities.AbilitySystemGlobals]
GameplayCueNotifyPaths="/Game/GASDocumentation/Characters"

We do want the GameplayCueManager to scan and find all of the GameplayCueNotifies; however, we don't want it to async load every single one on play. This will put every GameplayCueNotify and all of their referenced sounds and particles into memory regardless if they're even used in a level. In a large game like Paragon, this can be hundreds of megabytes of unneeded assets in memory and cause hitching and game freezes on startup.

An alternative to async loading every GameplayCue on startup is to only async load GameplayCues as they're triggered in-game. This mitigates the unnecessary memory usage and potential game hard freezes while async loading every GameplayCue in exchange for potentially delayed effects for the first time that a specific GameplayCue is triggered during play. This potential delay is nonexistent for SSDs. I have not tested on a HDD. If using this option in the UE Editor, there may be slight hitches or freezes during the first load of GameplayCues if the Editor needs to compile particle systems. This is not an issue in builds as the particle systems will already be compiled.

First we must subclass UGameplayCueManager and tell the AbilitySystemGlobals class to use our UGameplayCueManager subclass in DefaultGame.ini.

[/Script/GameplayAbilities.AbilitySystemGlobals]
GlobalGameplayCueManagerClass="/Script/ParagonAssets.PBGameplayCueManager"

In our UGameplayCueManager subclass, override ShouldAsyncLoadRuntimeObjectLibraries().

virtual bool ShouldAsyncLoadRuntimeObjectLibraries() const override
{
	return false;
}

⬆ Back to Top

4.8.6 Prevent Gameplay Cues from Firing

Sometimes we don't want GameplayCues to fire. For example if we block an attack, we may not want to play the hit impact attached to the damage GameplayEffect or play a custom one instead. We can do this inside of GameplayEffectExecutionCalculations by calling OutExecutionOutput.MarkGameplayCuesHandledManually() and then manually sending our GameplayCue event to the Target or Source's ASC.

If you never want any GameplayCues to fire on a specific ASC, you can set AbilitySystemComponent->bSuppressGameplayCues = true;.

⬆ Back to Top

4.8.7 Gameplay Cue Batching

Each GameplayCue triggered is an unreliable NetMulticast RPC. In situations where we fire multiple GCs at the same time, there are a few optimization methods to condense them down into one RPC or save bandwidth by sending less data.

4.8.7.1 Manual RPC

Say you have a shotgun that shoots eight pellets. That's eight trace and impact GameplayCues. GASShooter takes the lazy approach of combining them into one RPC by stashing all of the trace information into the EffectContext as TargetData. While this reduces the RPCs from eight to one, it still sends a lot of data over the network in that one RPC (~500 bytes). A more optimized approach is to send an RPC with a custom struct where you efficiently encode the hit locations or maybe you give it a random seed number to recreate/approximate the impact locations on the receiving side. The clients would then unpack this custom struct and turn back into locally executed GameplayCues.

How this works:

  1. Declare a FScopedGameplayCueSendContext. This suppresses UGameplayCueManager::FlushPendingCues() until it falls out of scope, meaning all GameplayCues will be queued up until the FScopedGameplayCueSendContext falls out of scope.
  2. Override UGameplayCueManager::FlushPendingCues() to merge GameplayCues that can be batched together based on some custom GameplayTag into your custom struct and RPC it to clients.
  3. Clients receive the custom struct and unpack it into locally executed GameplayCues.

This method can also be used when you need specific parameters for your GameplayCues that don't fit with what GameplayCueParameters offer and you don't want to add them to the EffectContext like damage numbers, crit indicator, broken shield indicator, was fatal hit indicator, etc.

https://forums.unrealengine.com/development-discussion/c-gameplay-programming/1711546-fscopedgameplaycuesendcontext-gameplaycuemanager

4.8.7.2 Multiple GCs on one GE

All of the GameplayCues on a GameplayEffect are sent in one RPC already. By default, UGameplayCueManager::InvokeGameplayCueAddedAndWhileActive_FromSpec() will send the whole GameplayEffectSpec (but converted to FGameplayEffectSpecForRPC) in the unreliable NetMulticast regardless of the ASC's Replication Mode. This could potentially be a lot of bandwidth depending on what is in the GameplayEffectSpec. We can potentially optimize this by setting the cvar AbilitySystem.AlwaysConvertGESpecToGCParams 1. This will convert GameplayEffectSpecs to FGameplayCueParameter structures and RPC those instead of the whole FGameplayEffectSpecForRPC. This potentially saves bandwidth but also has less information, depending on how the GESpec is converted to GameplayCueParameters and what your GCs need to know.

⬆ Back to Top

4.8.8 Gameplay Cue Events

GameplayCues respond to specific EGameplayCueEvents:

EGameplayCueEvent Description
OnActive Called when a GameplayCue is activated (added).
WhileActive Called when GameplayCue is active, even if it wasn't actually just applied (Join in progress, etc). This is not Tick! It's called once just like OnActive when a GameplayCueNotify_Actor is added or becomes relevant. If you need Tick(), just use the GameplayCueNotify_Actor's Tick(). It's an AActor after all.
Removed Called when a GameplayCue is removed. The Blueprint GameplayCue function that responds to this event is OnRemove.
Executed Called when a GameplayCue is executed: instant effects or periodic Tick(). The Blueprint GameplayCue function that responds to this event is OnExecute.

Use OnActive for anything in your GameplayCue that happen at the start of the GameplayCue but is okay if late joiners miss. Use WhileActive for ongoing effects in the GameplayCue that you would want late joiners to see. For example, if you have a GameplayCue for a tower structure in a MOBA exploding, you would put the initial explosion particle system and explosion sound in OnActive and you would put any residual ongoing fire particles or sounds in the WhileActive. In this scenario, it wouldn't make sense for late joiners to replay the initial explosion from OnActive, but you would want them to see the persistent, looping fire effects on the ground after the explosion happened from WhileActive. OnRemove should clean up anything added in OnActive and WhileActive. WhileActive will be called every time an Actor enters the relevancy range of a GameplayCueNotify_Actor. OnRemove will be called every time an Actor leaves relevancy range of a GameplayCueNotify_Actor.

⬆ Back to Top

4.8.9 Gameplay Cue Reliability

GameplayCues in general should be considered unreliable and thus unsuited for anything that directly affects gameplay.

Executed GameplayCues: These GameplayCues are applied via unreliable multicasts and are always unreliable.

GameplayCues applied from GameplayEffects:

  • Autonomous proxy reliably receives OnActive, WhileActive, and OnRemove
    FActiveGameplayEffectsContainer::NetDeltaSerialize() calls UAbilitySystemComponent::HandleDeferredGameplayCues() to call OnActive and WhileActive. FActiveGameplayEffectsContainer::RemoveActiveGameplayEffectGrantedTagsAndModifiers() makes the call to OnRemoved.
  • Simulated proxies reliably receive WhileActive and OnRemove
    UAbilitySystemComponent::MinimalReplicationGameplayCues's replication calls WhileActive and OnRemove. The OnActive event is called by an unreliable multicast.

GameplayCues applied without a GameplayEffect:

  • Autonomous proxy reliably receives OnRemove
    The OnActive and WhileActive events are called by an unreliable multicast.
  • Simulated proxies reliably receive WhileActive and OnRemove
    UAbilitySystemComponent::MinimalReplicationGameplayCues's replication calls WhileActive and OnRemove. The OnActive event is called by an unreliable multicast.

If you need something in a GameplayCue to be 'reliable', then apply it from a GameplayEffect and use WhileActive to add the FX and OnRemove to remove the FX.

⬆ Back to Top

4.9 Ability System Globals

The AbilitySystemGlobals class holds global information about GAS. Most of the variables can be set from the DefaultGame.ini. Generally you won't have to interact with this class, but you should be aware of its existence. If you need to subclass things like the GameplayCueManager or the GameplayEffectContext, you have to do that through the AbilitySystemGlobals.

To subclass AbilitySystemGlobals, set the class name in the DefaultGame.ini:

[/Script/GameplayAbilities.AbilitySystemGlobals]
AbilitySystemGlobalsClassName="/Script/ParagonAssets.PAAbilitySystemGlobals"

4.9.1 InitGlobalData()

Starting in UE 4.24, it is now necessary to call UAbilitySystemGlobals::InitGlobalData() to use TargetData, otherwise you will get errors related to ScriptStructCache and clients will be disconnected from the server. This function only needs to be called once in a project. Fortnite calls it from the AssetManager class's start initial loading function and Paragon called it from UEngine::Init(). I find that putting it in UEngineSubsystem::Initialize() is a good place as shown in the Sample Project. I would consider this boilerplate code that you should copy into your project to avoid issues with TargetData.

If you run into a crash while using the AbilitySystemGlobals GlobalAttributeSetDefaultsTableNames, you may need to call UAbilitySystemGlobals::InitGlobalData() later like Fortnite in the AssetManager or in the GameInstance instead of in UEngineSubsystem::Initialize(). This crash is likely due to the order in which the Subsystems are created and the GlobalAttributeDefaultsTables requires the EditorSubsystem to be loaded to bind a delegate in UAbilitySystemGlobals::InitGlobalData().

⬆ Back to Top

4.10 Prediction

GAS comes out of the box with support for client-side prediction; however, it does not predict everything. Client-side prediction in GAS means that the client does not have to wait for the server's permission to activate a GameplayAbility and apply GameplayEffects. It can "predict" the server giving it permission to do this and predict the targets that it would apply GameplayEffects to. The server then runs the GameplayAbility network latency-time after the client activates and tells the client if he was correct or not in his predictions. If the client was wrong in any of his predictions, he will "roll back" his changes from his "mispredictions" to match the server.

The definitive source for GAS-related prediction is GameplayPrediction.h in the plugin source code.

Epic's mindset is to only predict what you "can get away with". For example, Paragon and Fortnite do not predict damage. Most likely they use ExecutionCalculations for their damage which cannot be predicted anyway. This is not to say that you can't try to predict certain things like damage. By all means if you do it and it works well for you then that's great.

... we are also not all in on a "predict everything: seamlessly and automatically" solution. We still feel player prediction is best kept to a minimum (meaning: predict the minimum amount of stuff you can get away with).

Dave Ratti from Epic's comment from the new Network Prediction Plugin

What is predicted:

  • Ability activation
  • Triggered Events
  • GameplayEffect application:
    • Attribute modification (EXCEPTIONS: Executions do not currently predict, only attribute modifiers)
    • GameplayTag modification
  • Gameplay Cue events (both from within predictive gameplay effect and on their own)
  • Montages
  • Movement (built into UE4 UCharacterMovement)

What is not predicted:

  • GameplayEffect removal
  • GameplayEffect periodic effects (dots ticking)

From GameplayPrediction.h

While we can predict GameplayEffect application, we cannot predict GameplayEffect removal. One way that we can work around this limitation is to predict the inverse effect when we want to remove a GameplayEffect. Say we predict a movement speed slow of 40%. We can predictively remove it by applying a movement speed buff of 40%. Then remove both GameplayEffects at the same time. This is not appropriate for every scenario and support for predicting GameplayEffect removal is still needed. Dave Ratti from Epic has expressed desire to add it to a future iteration of GAS.

Because we cannot predict the removal of GameplayEffects, we cannot fully predict GameplayAbility cooldowns and there is no inverse GameplayEffect workaround for them. The server's replicated Cooldown GE will exist on the client and any attempts to bypass this (with Minimal replication mode for example) will be rejected by the server. This means clients with higher latencies take longer to tell the server to go on cooldown and to receive the removal of the server's Cooldown GE. This means players with higher latencies will have a lower rate of fire than players with lower latencies, giving them a disadvantage against lower latency players. Fortnite avoids this issue by using custom bookkeeping instead of Cooldown GEs.

Regarding predicting damage, I personally do not recommend it despite it being one of the first things that most people try when starting with GAS. I especially do not recommend trying to predict death. While you can predict damage, doing so is tricky. If you mispredict applying damage, the player will see the enemy's health jump back up. This can be especially awkward and frustrating if you try to predict death. Say you mispredict a Character's death and it starts ragdolling only to stop ragdolling and continue shooting at you when the server corrects it.

Note: Instant GameplayEffects (like Cost GEs) that change Attributes can be predicted on yourself seamlessly, predicting Instant Attribute changes to other characters will show a brief anomaly or "blip" in their Attributes. Predicted Instant GameplayEffects are actually treated like Infinite GameplayEffects so that they can be rolled back if mispredicted. When the server's GameplayEffect is applied, there potentially exists two of the same GameplayEffect's causing the Modifier to be applied twice or not at all for a brief moment. It will eventually correct itself but sometimes the blip is noticeable to players.

Problems that GAS's prediction implementation is trying to solve:

  1. "Can I do this?" Basic protocol for prediction.
  2. "Undo" How to undo side effects when a prediction fails.
  3. "Redo" How to avoid replaying side effects that we predicted locally but that also get replicated from the server.
  4. "Completeness" How to be sure we /really/ predicted all side effects.
  5. "Dependencies" How to manage dependent prediction and chains of predicted events.
  6. "Override" How to override state predictively that is otherwise replicated/owned by the server.

From GameplayPrediction.h

⬆ Back to Top

4.10.1 Prediction Key

GAS's prediction works on the concept of a Prediction Key which is an integer identifier that the client generates when he activates a GameplayAbility.

  • Client generates a prediction key when it activates a GameplayAbility. This is the Activation Prediction Key`.

  • Client sends this prediction key to the server with CallServerTryActivateAbility().

  • Client adds this prediction key to all GameplayEffects that it applies while the prediction key is valid.

  • Client's prediction key falls out of scope. Further predicted effects in the same GameplayAbility need a new Scoped Prediction Window.

  • Server receives the prediction key from the client.

  • Server adds this prediction key to all GameplayEffects that it applies.

  • Server replicates the prediction key back to the client.

  • Client receives replicated GameplayEffects from the server with the prediction key used to apply them. If any of the replicated GameplayEffects match the GameplayEffects that the client applied with the same prediction key, they were predicted correctly. There will temporarily be two copies of the GameplayEffect on the target until the client removes its predicted one.

  • Client receives the prediction key back from the server. This is the Replicated Prediction Key. This prediction key is now marked stale.

  • Client removes all GameplayEffects that it created with the now stale replicated prediction key. GameplayEffects replicated by the server will persist. Any GameplayEffects that the client added and didn't receive a matching replicated version from the server were mispredicted.

Prediction keys are guaranteed to be valid during an atomic grouping of instructions "window" in GameplayAbilities starting with Activation from the activation prediction key. You can think of this as being only valid during one frame. Any callbacks from latent action AbilityTasks will no longer have a valid prediction key unless the AbilityTask has a built-in Synch Point which generates a new Scoped Prediction Window.

⬆ Back to Top

4.10.2 Creating New Prediction Windows in Abilities

To predict more actions in callbacks from AbilityTasks, we need to create a new Scoped Prediction Window with a new Scoped Prediction Key. This is sometimes referred to as a Synch Point between the client and server. Some AbilityTasks like all of the input related ones come with built-in functionality to create a new scoped prediction window, meaning atomic code in the AbilityTasks' callbacks have a valid scoped prediction key to use. Other tasks like the WaitDelay task do not have built-in code to create a new scoped prediction window for its callback. If you need to predict actions after an AbilityTask that does not have built-in code to create a scoped prediction window like WaitDelay, we must manually do that using the WaitNetSync AbilityTask with the option OnlyServerWait. When the client hits a WaitNetSync with OnlyServerWait, it generates a new scoped prediction key based on the GameplayAbility's activation prediction key, RPCs it to the server, and adds it to any new GameplayEffects that it applies. When the server hits a WaitNetSync with OnlyServerWait, it waits until it receives the new scoped prediction key from the client before continuing. This scoped prediction key does the same dance as activation prediction keys - applied to GameplayEffects and replicated back to clients to be marked stale. The scoped prediction key is valid until it falls out of scope, meaning the scoped prediction window has closed. So again, only atomic operations, nothing latent, can use the new scoped prediction key.

You can create as many scoped prediction windows as you need.

If you would like to add the synch point functionality to your own custom AbilityTasks, look at how the input ones essentially inject the WaitNetSync AbilityTask code into them.

Note: When using WaitNetSync, this does block the server's GameplayAbility from continuing execution until it hears from the client. This could potentially be abused by malicious users who hack the game and intentionally delay sending their new scoped prediction key. While Epic uses the WaitNetSync sparingly, it recommends potentially building a new version of the AbilityTask with a delay that automatically continues without the client if this is a concern for you.

The Sample Project uses WaitNetSync in the Sprint GameplayAbility to create a new scoped prediction window every time we apply the stamina cost so that we can predict it. Ideally we want a valid prediction key when applying costs and cooldowns.

If you have a predicted GameplayEffect that is playing twice on the owning client, your prediction key is stale and you're experiencing the "redo" problem. You can usually solve this by putting a WaitNetSync AbilityTask with OnlyServerWait right before you apply the GameplayEffect to create a new scoped prediction key.

⬆ Back to Top

4.10.3 Predictively Spawning Actors

Spawning Actors predictively on clients is an advanced topic. GAS does not provide functionality to handle this out of the box (the SpawnActor AbilityTask only spawns the Actor on the server). The key concept is to spawn a replicated Actor on both the client and the server.

If the Actor is just cosmetic or doesn't serve any gameplay purpose, the simple solution is to override the Actor's IsNetRelevantFor() function to restrict the server from replicating to the owning client. The owning client would have his locally spawned version and the server and other clients would have the server's replicated version.

bool APAReplicatedActorExceptOwner::IsNetRelevantFor(const AActor * RealViewer, const AActor * ViewTarget, const FVector & SrcLocation) const
{
	return !IsOwnedBy(ViewTarget);
}

If the spawned Actor affects gameplay like a projectile that needs to predict damage, then you need advanced logic that is outside of the scope of this documentation. Look at how UnrealTournament predictively spawns projectiles on Epic Games' GitHub. They have a dummy projectile spawned only on the owning client that synchs up with the server's replicated projectile.

⬆ Back to Top

4.10.4 Future of Prediction in GAS

GameplayPrediction.h states in the future they could potentially add functionality for predicting GameplayEffect removal and periodic GameplayEffects.

Dave Ratti from Epic has expressed interest in fixing the latency reconciliation problem for predicting cooldowns, disadvantaging players with higher latencies versus players with lower latencies.

The new Network Prediction plugin by Epic is expected to be fully interoperable with the GAS like the CharacterMovementComponent was before it.

⬆ Back to Top

4.10.5 Network Prediction Plugin

Epic recently started an initiative to replace the CharacterMovementComponent with a new Network Prediction plugin. This plugin is still in its very early stages but is available to very early access on the Unreal Engine GitHub. It's too soon to tell which future version of the Engine that it will make its experimental beta debut in.

⬆ Back to Top

4.11 Targeting

4.11.1 Target Data

FGameplayAbilityTargetData is a generic structure for targeting data meant to be passed across the network. TargetData will typically hold AActor/UObject references, FHitResults, and other generic location/direction/origin information. However, you can subclass it to put essentially anything that you want inside of them as a simple means to pass data between the client and server in GameplayAbilities. The base struct FGameplayAbilityTargetData is not meant to be used directly but instead subclassed. GAS comes with a few subclassed FGameplayAbilityTargetData structs out of the box located in GameplayAbilityTargetTypes.h.

TargetData is typically produced by Target Actors or created manually and consumed by AbilityTasks and GameplayEffects via the EffectContext. As a result of being in the EffectContext, Executions, MMCs, GameplayCues, and the functions on the backend of the AttributeSet can access the TargetData.

We don't typically pass around the FGameplayAbilityTargetData directly, instead we use a FGameplayAbilityTargetDataHandle which has an internal TArray of pointers to FGameplayAbilityTargetData. This intermediate struct provides support for polymorphism of the TargetData.

⬆ Back to Top

4.11.2 Target Actors

GameplayAbilities spawn TargetActors with the WaitTargetData AbilityTask to visualize and capture targeting information from the world. TargetActors may optionally use GameplayAbilityWorldReticles to display current targets. Upon confirmation, the targeting information is returned as TargetData which can then be passed into GameplayEffects.

TargetActors are based on AActor so they can have any kind of visible component to represent where and how they are targeting such as static meshes or decals. Static meshes may be used to visualize placement of an object that your character will build. Decals may be used to show an area of effect on the ground. The Sample Project uses AGameplayAbilityTargetActor_GroundTrace with a decal on the ground to represent the damage area of effect for the Meteor ability. They also don't need to display anything either. For example it wouldn't make sense to display anything for a hitscan gun that instantly traces a line to its target as used in GASShooter.

They capture targeting information using basic traces or collision overlaps and convert the results as FHitResults or AActor arrays to TargetData depending on the TargetActor implementation. The WaitTargetData AbilityTask determines when the targets are confirmed through its TEnumAsByte<EGameplayTargetingConfirmation::Type> ConfirmationType parameter. When not using TEnumAsByte<EGameplayTargetingConfirmation::Type::Instant, the TargetActor typically performs the trace/overlap on Tick() and updates its location to the FHitResult depending on its implementation. While this performs a trace/overlap on Tick(), it's generally not terrible since it's not replicated and you typically don't have more than one (although you could have more) TargetActor running at a time. Just be aware that it uses Tick() and some complex TargetActors might do a lot on it like the rocket launcher's secondary ability in GASShooter. While tracing on Tick() is very responsive to the client, you may consider lowering the tick rate on the TargetActor if the performance hit is too much. In the case of TEnumAsByte<EGameplayTargetingConfirmation::Type::Instant, the TargetActor immediately spawns, produces TargetData, and destroys. Tick() is never called.

EGameplayTargetingConfirmation::Type When targets are confirmed
Instant The targeting happens instantly without special logic or user input deciding when to 'fire'.
UserConfirmed The targeting happens when the user confirms the targeting when the ability is bound to a Confirm input or by calling UAbilitySystemComponent::TargetConfirm(). The TargetActor will also respond to a bound Cancel input or call to UAbilitySystemComponent::TargetCancel() to cancel targeting.
Custom The GameplayTargeting Ability is responsible for deciding when the targeting data is ready by calling UGameplayAbility::ConfirmTaskByInstanceName(). The TargetActor will also respond to UGameplayAbility::CancelTaskByInstanceName() to cancel targeting.
CustomMulti The GameplayTargeting Ability is responsible for deciding when the targeting data is ready by calling UGameplayAbility::ConfirmTaskByInstanceName(). The TargetActor will also respond to UGameplayAbility::CancelTaskByInstanceName() to cancel targeting. Should not end the AbilityTask upon data production.

Not every EGameplayTargetingConfirmation::Type is supported by every TargetActor. For example, AGameplayAbilityTargetActor_GroundTrace does not support Instant confirmation.

The WaitTargetData AbilityTask takes in a AGameplayAbilityTargetActor class as a parameter and will spawn an instance on each activation of the AbilityTask and will destroy the TargetActor when the AbilityTask ends. The WaitTargetDataUsingActor AbilityTask takes in an already spawned TargetActor, but still destroys it when the AbilityTask ends. Both of these AbilityTasks are inefficient in that they either spawn or require a newly spawned TargetActor for each use. They're great for prototyping, but in production you might explore optimizing it if you have cases where you are constantly producing TargetData like in the case of an automatic rifle. GASShooter has a custom subclass of AGameplayAbilityTargetActor and a new WaitTargetDataWithReusableActor AbilityTask written from scratch that allows you to reuse a TargetActor without destroying it.

TargetActors are not replicated by default; however, they can be made to replicate if that makes sense in your game to show other players where the local player is targeting. They do include default functionality to communicate with the server via RPCs on the WaitTargetData AbilityTask. If the TargetActor's ShouldProduceTargetDataOnServer property is set to false, then the client will RPC its TargetData to the server on confirmation via CallServerSetReplicatedTargetData() in UAbilityTask_WaitTargetData::OnTargetDataReadyCallback(). If ShouldProduceTargetDataOnServer is true, the client will send a generic confirm event, EAbilityGenericReplicatedEvent::GenericConfirm, RPC to the server in UAbilityTask_WaitTargetData::OnTargetDataReadyCallback() and the server will do the trace or overlap check upon receiving the RPC to produce data on the server. If the client cancels the targeting, it will send a generic cancel event, EAbilityGenericReplicatedEvent::GenericCancel, RPC to the server in UAbilityTask_WaitTargetData::OnTargetDataCancelledCallback. As you can see, there are a lot of delegates on both the TargetActor and the WaitTargetData AbilityTask. The TargetActor responds to inputs to produce and broadcast TargetData ready, confirm, or cancel delegates. WaitTargetData listens to the TargetActor's TargetData ready, confirm, and cancel delegates and relays that information back to the GameplayAbility and to the server. If you send TargetData to the server, you may want to do validation on the server to make sure the TargetData looks reasonable to prevent cheating. Producing the TargetData directly on the server avoids this issue entirely, but will potentially lead to mispredictions for the owning client.

Depending on the particular subclass of AGameplayAbilityTargetActor that you use, different ExposeOnSpawn parameters will be exposed on the WaitTargetData AbilityTask node. Some common parameters include:

Common TargetActor Parameters Definition
Debug If true, it will draw debug tracing/overlapping information whenever the TargetActor performs a trace in non-shipping builds. Remember, non-Instant TargetActors will perform a trace on Tick() so these debug draw calls will also happen on Tick().
Filter [Optional] A special struct for filtering out (removing) Actors from the targets when the trace/overlap happens. Typical use cases are to filter out the player's Pawn, require targets be of a specific class. See Target Data Filters for more advanced use cases.
Reticle Class [Optional] Subclass of AGameplayAbilityWorldReticle that the TargetActor will spawn.
Reticle Parameters [Optional] Configure your Reticles. See Reticles.
Start Location A special struct for where tracing should start from. Typically this will be the player's viewpoint, a weapon muzzle, or the Pawn's location.

With the default TargetActor classes, Actors are only valid targets when they are directly in the trace/overlap. If they leave the trace/overlap (they move or you look away), they are no longer valid. If you want the TargetActor to remember the last valid target(s), you will need to add this functionality to a custom TargetActor class. I refer to these as persistent targets as they will persist until the TargetActor receives confirmation or cancellation, the TargetActor finds a new valid target in its trace/overlap, or the target is no longer valid (destroyed). GASShooter uses persistent targets for its rocket launcher's secondary ability's homing rockets targeting.

⬆ Back to Top

4.11.3 Target Data Filters

Using both the Make GameplayTargetDataFilter and Make Filter Handle nodes, you can filter out the player's Pawn or select only a specific class. If you need more advanced filtering, you can subclass FGameplayTargetDataFilter and override the FilterPassesForActor function.

USTRUCT(BlueprintType)
struct GASDOCUMENTATION_API FGDNameTargetDataFilter : public FGameplayTargetDataFilter
{
	GENERATED_BODY()

	/** Returns true if the actor passes the filter and will be targeted */
	virtual bool FilterPassesForActor(const AActor* ActorToBeFiltered) const override;
};

However, this will not work directly into the Wait Target Data node as it requires a FGameplayTargetDataFilterHandle. A new custom Make Filter Handle must be made to accept the subclass:

FGameplayTargetDataFilterHandle UGDTargetDataFilterBlueprintLibrary::MakeGDNameFilterHandle(FGDNameTargetDataFilter Filter, AActor* FilterActor)
{
	FGameplayTargetDataFilter* NewFilter = new FGDNameTargetDataFilter(Filter);
	NewFilter->InitializeFilterContext(FilterActor);

	FGameplayTargetDataFilterHandle FilterHandle;
	FilterHandle.Filter = TSharedPtr<FGameplayTargetDataFilter>(NewFilter);
	return FilterHandle;
}

⬆ Back to Top

4.11.4 Gameplay Ability World Reticles

AGameplayAbilityWorldReticles (Reticles) visualize who you are targeting when targeting with non-Instant confirmed TargetActors. TargetActors are responsible for the spawn and destroy lifetimes for all Reticles. Reticles are AActors so they can use any kind of visual component for representation. A common implementation as seen in GASShooter is to use a WidgetComponent to display a UMG Widget in screen space (always facing the player's camera). Reticles do not know which AActor that they're on, but you could subclass in that functionality on a custom TargetActor. TargetActors will typically update the Reticle's location to the target's location on every Tick().

GASShooter uses Reticles to show locked-on targets for the rocket launcher's secondary ability's homing rockets. The red indicator on the enemy is the Reticle. The similar white image is the rocket launcher's crosshair. Reticles in GASShooter

Reticles come with a handful of BlueprintImplementableEvents for designers (they're intended to be developed in Blueprints):

/** Called whenever bIsTargetValid changes value. */
UFUNCTION(BlueprintImplementableEvent, Category = Reticle)
void OnValidTargetChanged(bool bNewValue);

/** Called whenever bIsTargetAnActor changes value. */
UFUNCTION(BlueprintImplementableEvent, Category = Reticle)
void OnTargetingAnActor(bool bNewValue);

UFUNCTION(BlueprintImplementableEvent, Category = Reticle)
void OnParametersInitialized();

UFUNCTION(BlueprintImplementableEvent, Category = Reticle)
void SetReticleMaterialParamFloat(FName ParamName, float value);

UFUNCTION(BlueprintImplementableEvent, Category = Reticle)
void SetReticleMaterialParamVector(FName ParamName, FVector value);

Reticles can optionally use FWorldReticleParameters provided by the TargetActor for configuration. The default struct only provides one variable FVector AOEScale. While you can technically subclass this struct, the TargetActor will only accept the base struct. It seems a little short-sighted to not allow this to be subclassed with default TargetActors. However, if you make your own custom TargetActor, you can provide your own custom reticle parameters struct and manually pass it to your subclass of AGameplayAbilityWorldReticles when you spawn them.

Reticles are not replicated by default, but can be made replicated if it makes sense for your game to show other players who the local player is targeting.

Reticles will only display on the current valid target with the default TargetActors. For example, if you're using a AGameplayAbilityTargetActor_SingleLineTrace to trace for a target, the Reticle will only appear when the enemy is directly in the trace path. If you look away, the enemy is no longer a valid target and the Reticle will disappear. If you want the Reticle to stay on the last valid target, you will want to customize your TargetActor to remember the last valid target and keep the Reticle on them. I refer to these as persistent targets as they will persist until the TargetActor receives confirmation or cancellation, the TargetActor finds a new valid target in its trace/overlap, or the target is no longer valid (destroyed). GASShooter uses persistent targets for its rocket launcher's secondary ability's homing rockets targeting.

⬆ Back to Top

4.11.5 Gameplay Effect Containers Targeting

GameplayEffectContainers come with an optional, efficient means of producing TargetData. This targeting takes places instantly when the EffectContainer is applied on the client and the server. It's more efficient than TargetActors because it runs on the CDO of the targeting object (no spawning and destroying of Actors), but it lacks player input, happens instantly without needing confirmation, cannot be canceled, and cannot send data from the client to the server (produces data on both). It works well for instant traces and collision overlaps. Epic's Action RPG Sample Project includes two example types of targeting with its containers - target the ability owner and pull TargetData from an event. It also implements one in Blueprint to do instant sphere traces at some offset (set by child Blueprint classes) from the player. You can subclass URPGTargetType in C++ or Blueprint to make your own targeting types.

⬆ Back to Top

5. Commonly Implemented Abilities and Effects

5.1 Stun

Typically with stuns, we want to cancel all of a Character's active GameplayAbilities, prevent new GameplayAbility activations, and prevent movement throughout the duration of the stun. The Sample Project's Meteor GameplayAbility applies a stun on hit targets.

To cancel the target's active GameplayAbilities, we call AbilitySystemComponent->CancelAbilities() when the stun GameplayTag is added.

To prevent new GameplayAbilities from activating while stunned, the GameplayAbilities are given the stun GameplayTag in their Activation Blocked Tags GameplayTagContainer.

To prevent movement while stunned, we override the CharacterMovementComponent's GetMaxSpeed() function to return 0 when the owner has the stun GameplayTag.

⬆ Back to Top

5.2 Sprint

The Sample Project provides an example of how to sprint - run faster while Left Shift is held down.

The faster movement is handled predictively by the CharacterMovementComponent by sending a flag over the network to the server. See GDCharacterMovementComponent.h/cpp for details.

The GA handles responding to the Left Shift input, tells the CMC to begin and stop sprinting, and to predictively charge stamina while Left Shift is pressed. See GA_Sprint_BP for details.

⬆ Back to Top

5.3 Aim Down Sights

The Sample Project handles this the exact same way as sprinting but decreasing the movement speed instead of increasing it.

See GDCharacterMovementComponent.h/cpp for details on predictively decreasing the movement speed.

See GA_AimDownSight_BP for details on handling the input. There is no stamina cost for aiming down sights.

⬆ Back to Top

5.4 Lifesteal

I handle lifesteal inside of the damage ExecutionCalculation. The GameplayEffect will have a GameplayTag on it like Effect.CanLifesteal. The ExecutionCalculation checks if the GameplayEffectSpec has that Effect.CanLifesteal GameplayTag. If the GameplayTag exists, the ExecutionCalculation creates a dynamic Instant GameplayEffect with the amount of health to give as the modifier and applies it back to the Source's ASC.

if (SpecAssetTags.HasTag(FGameplayTag::RequestGameplayTag(FName("Effect.Damage.CanLifesteal"))))
{
	float Lifesteal = Damage * LifestealPercent;

	UGameplayEffect* GELifesteal = NewObject<UGameplayEffect>(GetTransientPackage(), FName(TEXT("Lifesteal")));
	GELifesteal->DurationPolicy = EGameplayEffectDurationType::Instant;

	int32 Idx = GELifesteal->Modifiers.Num();
	GELifesteal->Modifiers.SetNum(Idx + 1);
	FGameplayModifierInfo& Info = GELifesteal->Modifiers[Idx];
	Info.ModifierMagnitude = FScalableFloat(Lifesteal);
	Info.ModifierOp = EGameplayModOp::Additive;
	Info.Attribute = UPAAttributeSetBase::GetHealthAttribute();

	SourceAbilitySystemComponent->ApplyGameplayEffectToSelf(GELifesteal, 1.0f, SourceAbilitySystemComponent->MakeEffectContext());
}

⬆ Back to Top

5.5 Generating a Random Number on Client and Server

Sometimes you need to generate a "random" number inside of a GameplayAbility for things like bullet recoil or spread. The client and the server will both want to generate the same random numbers. To do this, we must set the random seed to be the same at the time of GameplayAbility activation. You will want to set the random seed each time you activate the GameplayAbility in case the client mispredicts activation and its random number sequence becomes out of synch with the server's.

Seed Setting Method Description
Use the activation prediction key The GameplayAbility activation prediction key is an int16 guaranteed to be synchronized and available in both the client and server in the Activation(). You can set this as the random seed on both the client and the server. The downside to this method is that the prediction key always starts at zero each time the game starts and consistently increments the value to use between generating keys. This means each match will have the exact same random number sequence. This may or may not be random enough for your needs.
Send a seed through an event payload when you activate the GameplayAbility Activate your GameplayAbility by event and send the randomly generated seed from the client to the server via the replicated event payload. This allows for more randomness but the client could easily hack their game to only send the same seed value every time. Also activating GameplayAbilities by event will prevent them from activating from the input bind.

If your random deviation is small, most players won't notice that the sequence is the same every game and using the activation prediction key as the random seed should work for you. If you're doing something more complex that needs to be hacker proof, perhaps using a Server Initiated GameplayAbility would work better where the server can create the prediction key or generate the random seed to send via an event payload.

⬆ Back to Top

5.6 Critical Hits

I handle critical hits inside of the damage ExecutionCalculation. The GameplayEffect will have a GameplayTag on it like Effect.CanCrit. The ExecutionCalculation checks if the GameplayEffectSpec has that Effect.CanCrit GameplayTag. If the GameplayTag exists, the ExecutionCalculation generates a random number corresponding to the critical hit chance (Attribute captured from the Source) and adds the critical hit damage (also an Attribute captured from the Source) if it succeeded. Since I don't predict damage, I don't have to worry about synchronizing the random number generators on the client and server since the ExecutionCalculation will only run on the server. If you tried to do this predictively using an MMC to do your damage calculation, you would have to get a reference to the random seed from the GameplayEffectSpec->GameplayEffectContext->GameplayAbilityInstance.

See how GASShooter does headshots. It's the same concept except that it does not rely on a random number for chance and instead checks the FHitResult bone name.

⬆ Back to Top

5.7 Non-Stacking Gameplay Effects but Only the Greatest Magnitude Actually Affects the Target

Slow effects in Paragon did not stack. Each slow instance applied and kept track of their lifetimes as normal, but only the greatest magnitude slow effect actually affected the Character. GAS provides for this scenario out of the box with AggregatorEvaluateMetaData. See AggregatorEvaluateMetaData() for details and implementation.

⬆ Back to Top

5.8 Generate Target Data While Game is Paused

If you need to pause the game while waiting to generate TargetData from a WaitTargetData AbilityTask from your player, I suggest instead of pausing to use slomo 0.

⬆ Back to Top

5.9 One Button Interaction System

GASShooter implements a one button interaction system where the player can press or hold 'E' to interact with interactable objects like reviving a player, opening a weapon chest, and opening or closing a sliding door.

⬆ Back to Top

6. Debugging GAS

Often when debugging GAS related issues, you want to know things like:

  • "What are the values of my attributes?"
  • "What gameplay tags do I have?"
  • "What gameplay effects do I currently have?"
  • "What abilities do I have granted, which ones are running, and which ones are blocked from activating?".

GAS comes with two techniques for answering these questions at runtime - showdebug abilitysystem and hooks in the GameplayDebugger.

Tip: UE4 likes to optimize C++ code which makes it hard to debug some functions. You will encounter this rarely when tracing deep into your code. If setting your Visual Studio solution configuration to DebugGame Editor still prevents tracing code or inspecting variables, you can disable all optimizations by wrapping the optimized function with the PRAGMA_DISABLE_OPTIMIZATION_ACTUAL and PRAGMA_ENABLE_OPTIMIZATION_ACTUAL macros. This cannot be used on the plugin code unless you rebuild the plugin from source. This may or may not work on inline functions depending on what they do and where they are. Be sure to remove the macros when you're done debugging!

PRAGMA_DISABLE_OPTIMIZATION_ACTUAL
void MyClass::MyFunction(int32 MyIntParameter)
{
	// My code
}
PRAGMA_ENABLE_OPTIMIZATION_ACTUAL

⬆ Back to Top

6.1 showdebug abilitysystem

Type showdebug abilitysystem in the in-game console. This feature is split into three "pages". All three pages will show the GameplayTags that you currently have. Type AbilitySystem.Debug.NextCategory into the console to cycle between the pages.

The first page shows the CurrentValue of all of your Attributes: First Page of showdebug abilitysystem

The second page shows all of the Duration and Infinite GameplayEffects on you, their number of stacks, what GameplayTags they give, and what Modifiers they give. Second Page of showdebug abilitysystem

The third page shows all of the GameplayAbilities that have been granted to you, whether they are currently running, whether they are blocked from activating, and the status of currently running AbilityTasks. Third Page of showdebug abilitysystem

While you can cycle between targets with PageUp and PageDown, the pages will only show data for the ASC on your locally controlled Character. However, using AbilitySystem.Debug.NextTarget and AbilitySystem.Debug.PrevTarget will show data for other ASCs, but it will not update the top half of the debug information nor will it update the green targeting rectangular prism so there is no way to know which ASC is currently being targeted. This bug has been reported https://issues.unrealengine.com/issue/UE-90437.

Note: For showdebug abilitysystem to work an actual HUD class must be selected in the GameMode. Otherwise the command is not found and "Unknown Command" is returned.

⬆ Back to Top

6.2 Gameplay Debugger

GAS adds functionality to the Gameplay Debugger. Access the Gameplay Debugger with the Apostrophe (') key. Enable the Abilities category by pressing 3 on your numpad. The category may be different depending on what plugins you have. If your keyboard doesn't have a numpad like a laptop, then you can change the keybindings in the project settings.

Use the Gameplay Debugger when you want to see the GameplayTags, GameplayEffects, and GameplayAbilities on other Characters. Unfortunately it does not show the CurrentValue of the target's Attributes. It will target whatever Character is in the center of your screen. You can change targets by selecting them in the World Outliner in the Editor or by looking at a different Character and press Apostrophe (') again. The currently inspected Character has the largest red circle above it.

Gameplay Debugger

⬆ Back to Top

6.3 GAS Logging

The GAS source code contains a lot of logging statements produced at varying verbosity levels. You will most likely see these as ABILITY_LOG() statements. The default verbosity level is Display. Anything higher will not be displayed in the console by default.

To change the verbosity level of a log category, type into your console:

log [category] [verbosity]

For example, to turn on ABILITY_LOG() statements, you would type into your console:

log LogAbilitySystem VeryVerbose

To reset it back to default, type:

log LogAbilitySystem Display

To display all log categories, type:

log list

Notable GAS related logging categories:

Logging Category Default Verbosity Level
LogAbilitySystem Display
LogAbilitySystemComponent Log
LogGameplayCueDetails Log
LogGameplayCueTranslator Display
LogGameplayEffectDetails Log
LogGameplayEffects Display
LogGameplayTags Log
LogGameplayTasks Log
VLogAbilitySystem Display

See the Wiki on Logging for more information.

⬆ Back to Top

7. Optimizations

7.1 Ability Batching

GameplayAbilities that activate, optionally send TargetData to the server, and end all in one frame can be batched to condense two-three RPCs into one RPC. These types of abilities are commonly used for hitscan guns.

7.2 Gameplay Cue Batching

If you're sending many GameplayCues at the same time, consider batching them into one RPC. The goal is to reduce the number of RPCs (GameplayCues are unreliable NetMulticasts) and send as little data as possible.

7.3 AbilitySystemComponent Replication Mode

By default, the ASC is in Full Replication Mode. This will replicate all GameplayEffects to every client (which is fine for a single player game). In a multiplayer game, set the player owned ASCs to Mixed Replication Mode and AI controlled characters to Minimal Replication Mode. This will replicate GEs applied on a player character to only replicate to the owner of that character and GEs applied on AI controlled characters will never replicate GEs to clients. GameplayTags will still replicate and GameplayCues will still unreliable NetMulticast to all clients, regardless of the Replication Mode. This will cut down on network data from GEs being replicated when all clients don't need to see them.

7.4 Attribute Proxy Replication

In large games with many players like Fortnite Battle Royale (FNBR), there will be a lot of ASCs living on always-relevant PlayerStates replicating a lot of Attributes. To optimize this bottleneck, Fortnite disables the ASC and its AttributeSets from replicating altogether on simulated player-controlled proxies in the PlayerState::ReplicateSubobjects(). Autonomous proxies and AI controlled Pawns still fully replicate according to their Replication Mode. Instead of replicating Attributes on the ASC on the always-relevant PlayerStates, FNBR uses a replicated proxy structure on the player's Pawn. When Attributes change on the server's ASC, they are changed on the proxy struct too. The client receives the replicated Attributes from the proxy struct and pushes the changes back into its local ASC. This allows Attribute replication to use the Pawn's relevancy and NetUpdateFrequency. This proxy struct also replicates a small white-listed set of GameplayTags in a bitmask. This optimization reduces the amount of data over the network and allows us to take advantage of pawn relevancy. AI controlled Pawns have their ASC on the Pawn which already uses its relevancy so this optimization is not needed for them.

I’m not sure if it is still necessary with other server side optimizations that have been done since then (Replication Graph, etc) and it is not the most maintainable pattern.

Dave Ratti from Epic's answer to community questions #3

7.5 ASC Lazy Loading

Fortnite Battle Royale (FNBR) has a lot of damageable AActors (trees, buildings, etc) in the world, each with an ASC. This can add up in memory cost. FNBR optimizes this by lazily loading ASCs only when they're needed (when they first take damage by a player). This reduces overall memory usage since some AActors may never be damaged in a match.

⬆ Back to Top

8. Quality of Life Suggestions

8.1 Gameplay Effect Containers

GameplayEffectContainers combine GameplayEffectSpecs, TargetData, simple targeting, and related functionality into easy to use structures. These are great for transfering GameplayEffectSpecs to projectiles spawned from an ability that will then apply them on collision at a later time.

8.2 Blueprint AsyncTasks to Bind to ASC Delegates

To increase designer-friendly iteration times, especially when designing UMG Widgets for UI, create Blueprint AsyncTasks (in C++) to bind to the common change delegates on the ASC directly from your UMG Blueprint graphs. The only caveat is that they must be manually destroyed (like when the widget is destroyed) otherwise they will live in memory forever. The Sample Project includes three Blueprint AsyncTasks.

Listen for Attribute changes:

Listen for Attributes Changes BP Node

Listen for cooldown changes:

Listen for Cooldown Change BP Node

Listen for GE stack changes:

Listen for GameplayEffect Stack Change BP Node

⬆ Back to Top

9. Troubleshooting

9.1 LogAbilitySystem: Warning: Can't activate LocalOnly or LocalPredicted ability %s when not local!

You need to initialize the ASC on the client.

⬆ Back to Top

9.2 ScriptStructCache errors

You need to call UAbilitySystemGlobals::InitGlobalData().

⬆ Back to Top

9.3 Animation Montages are not replicating to clients

Make sure that you're using the PlayMontageAndWait Blueprint node instead of PlayMontage in your GameplayAbilities. This AbilityTask replicates the montage through the ASC automatically whereas the PlayMontage node does not.

⬆ Back to Top

9.4 Duplicating Blueprint Actors is setting AttributeSets to nullptr

There is a bug in Unreal Engine that will set AttributeSet pointers on your classes to nullptr for Blueprint Actor classes that are duplicated from existing Blueprint Actor classes. There are a few workarounds for this. I've had success not creating bespoke AttributeSet pointers on my classes (no pointer in the .h, not calling CreateDefaultSubobject in the constructor) and instead just directly adding AttributeSets to the ASC in PostInitializeComponents() (not shown in the Sample Project). The replicated AttributeSets will still live in the ASC's SpawnedAttributes array. It would look something like this:

void AGDPlayerState::PostInitializeComponents()
{
	Super::PostInitializeComponents();

	if (AbilitySystemComponent)
	{
		AbilitySystemComponent->AddSet<UGDAttributeSetBase>();
		// ... any other AttributeSets that you may have
	}
}

In this scenario, you would read and set the values in the AttributeSet using the functions on the ASC instead of calling functions on the AttributeSet made from the macros.

/** Returns current (final) value of an attribute */
float GetNumericAttribute(const FGameplayAttribute &Attribute) const;

/** Sets the base value of an attribute. Existing active modifiers are NOT cleared and will act upon the new base value. */
void SetNumericAttributeBase(const FGameplayAttribute &Attribute, float NewBaseValue);

So the GetHealth() would look something like:

float AGDPlayerState::GetHealth() const
{
	if (AbilitySystemComponent)
	{
		return AbilitySystemComponent->GetNumericAttribute(UGDAttributeSetBase::GetHealthAttribute());
	}

	return 0.0f;
}

Setting (initializing) the health Attribute would look something like:

const float NewHealth = 100.0f;
if (AbilitySystemComponent)
{
	AbilitySystemComponent->SetNumericAttributeBase(UGDAttributeSetBase::GetHealthAttribute(), NewHealth);
}

As a reminder, the ASC only ever expects at most one AttributeSet object per AttributeSet class.

⬆ Back to Top

10. Common GAS Acronyms

Name Acronyms
AbilitySystemComponent ASC
AbilityTask AT
Action RPG Sample Project by Epic ARPG, ARPG Sample
CharacterMovementComponent CMC
GameplayAbility GA
GameplayAbilitySystem GAS
GameplayCue GC
GameplayEffect GE
GameplayEffectExecutionCalculation ExecCalc, Execution
GameplayTag Tag, GT
ModifierMagnitudeCalculation ModMagCalc, MMC

⬆ Back to Top

11. Other Resources

⬆ Back to Top

12. GAS Changelog

This is a list of notable changes (fixes, changes, and new features) to GAS compiled from the official Unreal Engine upgrade changelog and from undocumented changes that I've encountered. If you've found something that isn't listed here, please make an issue or pull request.

4.26

  • GAS plugin is no longer flagged as beta.
  • Crash Fix: Fixed a crash when adding a gameplay tag without a valid tag source selection.
  • Crash Fix: Added the path string arg to a message to fix a crash in UGameplayCueManager::VerifyNotifyAssetIsInValidPath.
  • Crash Fix: Fixed an access violation crash in AbilitySystemComponent_Abilities when using a ptr without checking it.
  • Bug Fix: Fixed a bug where stacking GEs that did not reset the duration on additional instances of the effect being applied.
  • Bug Fix: Fixed an issue that caused CancelAllAbilities to only cancel non-instanced abilities.
  • New: Added optional tag parameters to gameplay ability commit functions.
  • New: Added StartTimeSeconds to PlayMontageAndWait ability task and improved comments.
  • New: Added tag container "DynamicAbilityTags" to FGameplayAbilitySpec. These are optional ability tags that are replicated with the spec. They are also captured as source tags by applied gameplay effects.
  • New: GameplayAbility IsLocallyControlled and HasAuthority functions are now callable from Blueprint.
  • New: Visual logger will now only collect and store info about instant GEs if we're currently recording visual logging data.
  • New: Added support for redirectors on gameplay attribute pins in blueprint nodes.
  • New: Added new functionality for when root motion movement related ability tasks end they will return the movement component's movement mode to the movement mode it was in before the task started.

4.25.1

  • Fixed! UE-92787 Crash saving blueprint with a Get Float Attribute node and the attribute pin is set inline
  • Fixed! UE-92810 Crash spawning actor with instance editable gameplay tag property that was changed inline

4.25

  • Fixed prediction of RootMotionSource AbilityTasks
  • GAMEPLAYATTRIBUTE_REPNOTIFY() now additionally takes in the old Attribute value. We must supply that as the optional parameter to our OnRep functions. Previously, it was reading the attribute value to try to get the old value. However, if called from a replication function, the old value had already been discarded before reaching SetBaseAttributeValueFromReplication so we'd get the new value instead.
  • Added NetSecurityPolicy to UGameplayAbility.
  • Crash Fix: Fixed a crash when adding a gameplay tag without a valid tag source selection.
  • Crash Fix: Removed a few ways for attackers to crash a server through the ability system.
  • Crash Fix: We now make sure we have a GameplayEffect definition before checking tag requirements.
  • Bug Fix: Fixed an issue with gameplay tag categories not applying to function parameters in Blueprints if they were part of a function terminator node.
  • Bug Fix: Fixed an issue with gameplay effects' tags not being replicated with multiple viewports.
  • Bug Fix: Fixed a bug where a gameplay ability spec could be invalidated by the InternalTryActivateAbility function while looping through triggered abilities.
  • Bug Fix: Changed how we handle updating gameplay tags inside of tag count containers. When deferring the update of parent tags while removing gameplay tags, we will now call the change-related delegates after the parent tags have updated. This ensures that the tag table is in a consistent state when the delegates broadcast.
  • Bug Fix: We now make a copy of the spawned target actor array before iterating over it inside when confirming targets because some callbacks may modify the array.
  • Bug Fix: Fixed a bug where stacking GameplayEffects that did not reset the duration on additional instances of the effect being applied and with set by caller durations would only have the duration correctly set for the first instance on the stack. All other GE specs in the stack would have a duration of 1 second. Added automation tests to detect this case.
  • Bug Fix: Fixed a bug that could occur if handling gameplay event delegates modified the list of gameplay event delegates.
  • Bug Fix: Fixed a bug causing GiveAbilityAndActivateOnce to behave inconsistently.
  • Bug Fix: Reordered some operations inside FGameplayEffectSpec::Initialize to deal with a potential ordering dependency.
  • New: UGameplayAbility now has an OnRemoveAbility function. It follows the same pattern as OnGiveAbility and is only called on the primary instance of the ability or the class default object.
  • New: When displaying blocked ability tags, the debug text now includes the total number of blocked tags.
  • New: Renamed UAbilitySystemComponent::InternalServerTryActiveAbility to UAbilitySystemComponent::InternalServerTryActivateAbility.Code that was calling InternalServerTryActiveAbility should now call InternalServerTryActivateAbility.
  • New: Continue to use the filter text for displaying gameplay tags when a tag is added or deleted. The previous behavior cleared the filter.
  • New: Don't reset the tag source when we add a new tag in the editor.
  • New: Added the ability to query an ability system component for all active gameplay effects that have a specified set of tags. The new function is called GetActiveEffectsWithAllTags and can be accessed through code or blueprints.
  • New: When root motion movement related ability tasks end they now return the movement component's movement mode to the movement mode it was in before the task started.
  • New: Made SpawnedAttributes transient so it won't save data that can become stale and incorrect. Added null checks to prevent any currently saved stale data from propagating. This prevents problems related to bad data getting stored in SpawnedAttributes.
  • API Change: AddDefaultSubobjectSet has been deprecated. AddAttributeSetSubobject should be used instead.
  • New: Gameplay Abilities can now specify the Anim Instance on which to play a montage.

4.24

  • Fixed blueprint node Attribute variables resetting to None on compile.
  • Need to call UAbilitySystemGlobals::InitGlobalData() to use TargetData otherwise you will get ScriptStructCache errors and clients will be disconnected from the server. My advice is to always call this in every project now whereas before 4.24 it was optional.
  • Fixed crash when copying a GameplayTag setter to a blueprint that didn't have the variable previously defined.
  • UGameplayAbility::MontageStop() function now properly uses the OverrideBlendOutTime parameter.
  • Fixed GameplayTag query variables on components not being modified when edited.
  • Added the ability for GameplayEffectExecutionCalculations to support scoped modifiers against "temporary variables" that aren't required to be backed by an attribute capture.
    • Implementation basically enables GameplayTag-identified aggregators to be created as a means for an execution to expose a temporary value to be manipulated with scoped modifiers; you can now build formulas that want manipulatable values that don't need to be captured from a source or target.
    • To use, an execution has to add a tag to the new member variable ValidTransientAggregatorIdentifiers; those tags will show up in the calculation modifier array of scoped mods at the bottom, marked as temporary variables—with updated details customizations accordingly to support feature
  • Added restricted tag quality-of-life improvements. Removed the default option for restricted GameplayTag source. We no longer reset the source when adding restricted tags to make it easier to add several in a row.
  • APawn::PossessedBy() now sets the owner of the Pawn to the new Controller. Useful because Mixed Replication Mode expects the owner of the Pawn to be the Controller if the ASC lives on the Pawn.
  • Fixed bug with POD (Plain Old Data) in FAttributeSetInitterDiscreteLevels.

⬆ Back to Top

About

My understanding of Unreal Engine 4's GameplayAbilitySystem plugin with a simple multiplayer sample project.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • C++ 98.2%
  • Other 1.8%