GAS – Optimisation Approaches

The Constant Hunt for Optimisation
The GAS system project is one of those projects which are challenging due to the nature of the educational resources available. It's a straddle between understanding how C++ can be leveraged to create efficient systems, and wrestling existing framework systems within Blueprint, and the documentation for both. Early stage tests are an attempt to understand how the system works, with future optimisation not being considered. As the project evolves, opportunities to optimise are identified. This post is a discussion about how the ability system has been optimised to reduce the amount of files involved within the abilities - using master cooldown files and setting values dynamically.
The Problem - Excess Duplication
The original version of my abilities all had components, meaning different GAS ability and effect files to make it work. This includes:
- The ability itself
- The cooldown
- The damage it causes
Depending on other elements, we might also have projectiles, particle systems or other bits to make the ability look like a cool ability (animations for example).
The problem with this is that if we have 10 abilities, it's not the end of the world to have 3 GAS files for these, but if we scale this up to 100+, we have a large duplication of code. Most of these GAS files just have simplistic properties - for example:
- Length of cooldown
- Damage to apply
- If it's instant damage, or ticking damage
You can imagine different abilities would likely have similar profiles. A fireball has instant damage, as would a base melee attack. A curse may have a damage over time, as would a scorch fire effect. As such, having almost identical code files repeated per ability isn't the most optimal in terms of project management, although it was easy to implement. So, what's the solution? We need to look at how we can create dynamic instances of the Gameplay Effect files so we can reduce how many we need to create, and even better, control ability properties from a centralised Data Table... Making it easier to make quicker changes and balance abilities without chasing source files! Let's look into how we got this to work...
Solution - Set by Caller Magnitude (GAS Functions)
The GAS system project is one of those projects which are challenging due to the nature of the educational resources available. It's a straddle between understanding how C++ can be leveraged to create efficient systems, and wrestling existing framework systems within Blueprint, and the documentation for both. Early stage tests are an attempt to understand how the system works, with future optimisation not being considered. As the project evolves, opportunities to optimise are identified. This post is a discussion about how the ability system has been optimised to reduce the amount of files involved within the abilities - using master cooldown files and setting values dynamically.
In our new 'Master Cooldown' Gameplay Effect, we keep the duration policy of 'has duration' but we change our Magnitude from a set float for the length of the cooldown, to a 'Set by Caller' and assign a Data tag to this - in this case, the tag is Test.MasterCooldown - it will likely change in the final thing. The tag allows us to directly reference the magnitude, and feed this magnitude value to the Gameplay Effect via Blueprints. As you can see in the Blueprint image below, we use our ability details to set this magnitude. The nodes in this custom function:
- Make Outgoing Gameplay Effect Spec - this references the Master Cooldown Gameplay Ability and creates a spec handle for it to be applied.
- We then Assign Tag Set by Caller Magnitude which expects the Data tag - this is the magnitude we are updating on the spec handle, and we feed in our Magnitude from our Ability Data Table struct.
The good thing about this approach is we've centralised all the information in relation to the cooldown on our Data Table for this ability, making it easier and quicker to update.
By putting this into a Blueprint Function, we can reuse this snippet of code for every ability. There's a few things we'll need to reuse:
- Setting cooldown value/magnitude
- Setting damage numbers dynamically (based off stats like strength, crit chance etc)
- Sending threat information to the primary target
Blueprint functions will help us do all this consistently across all ability blueprints.
Dive into C++: Creating Functions to Manage Tags
To manage cooldown tracking, we need to establish a way to track what cooldown is active at what time. When we had a custom Gameplay Effect for each cooldown, it was easy to get a reference to the class and retrieve the time remaining/length of the duration and use that to update the UI elements, but when you are using a global/parent class of a Gameplay Effect, all applications of this Effect share the same class reference. This creates problems when you want to differentiate each ability cooldown and track them. To help us track them, we want to apply a Gameplay Tag to the Effect so we can get a reference to that effect via that tag. To do this, we need to create some simple C++.
Now, I know a small bit of C++, but not enough to dive into this easily. I often use ChatGPT to help me figure out what I need to do. I don't use it to solve all my problems, but to point me in the right direction - it's a tool which some people see as a cheating tool, but I see it as a good way of helping me get on the right track.
Using ChatGPT, I created a GAS Blueprint Function Library. I originally created this within Unreal Engine, by creating a brand new C++ class of Blueprint Function Library parent so it would help with creating the base syntax and codebase.
The code is quite simple - we want to create a function that will add a provided tag to the spec handle, to essentially label it. We create a blueprint callable UFUNCTION that returns a true/false bool (dependent if it's successful or not) but more importantly, takes in both a Gameplay Effect spec handle (the thing we want to assign a tag to) and the Gameplay Tag (what we want to assign). This is all declared within the .h header file, and looks like this:
#pragma once
#include "Kismet/BlueprintFunctionLibrary.h"
#include "GameplayTagContainer.h"
#include "GameplayEffectTypes.h"
#include "GASBlueprintFunctionLibrary.generated.h"
class UAbilitySystemComponent;
UCLASS()
class GAS_CPP_TEST_API UGASBlueprintFunctionLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
/** Adds a cooldown tag dynamically to a GE Spec */
UFUNCTION(BlueprintCallable, Category = "GAS|Cooldown")
static bool AddCooldownTagToSpec(FGameplayEffectSpecHandle SpecHandle, FGameplayTag CooldownTag);
};
The function that is called checks to see if the spec handle is valid, and if the cooldown tag is valid, and if they both are, it assigns the Cooldown Tag to the Spec handle using the DynamicGrantedTags from the Gameplay Effect codebase. This is very simple, but it allows us to connect the Gameplay Tag to the associated Spec handle, allowing us to query spec handles via tag. The .cpp file looks like this.
#include "GASBlueprintFunctionLibrary.h"
#include "AbilitySystemComponent.h"
#include "GameplayEffect.h"
#include "AbilitySystemBlueprintLibrary.h"
//Function to add a custom cooldown tag to a spec handle, useful for creating global cooldowns instead of creating one for every ability
bool UGASBlueprintFunctionLibrary::AddCooldownTagToSpec(FGameplayEffectSpecHandle SpecHandle, FGameplayTag CooldownTag)
{
if (SpecHandle.IsValid() && CooldownTag.IsValid())
{
SpecHandle.Data->DynamicGrantedTags.AddTag(CooldownTag);
// Double-check it was actually added
return SpecHandle.Data->DynamicGrantedTags.HasTagExact(CooldownTag);
}
// Something was invalid
return false;
}
Updating UI Display - C++ Functions to Query Abilities via Tags
So now we have an ability with a cooldown which is being applied through a 'master' gameplay effect, leveraging the Set Caller by Magnitude process to define the duration, and assigning a Gameplay Tag through our custom C++ function above so we can reference it. The problem with this approach is that we natively cannot use our Get Active Gameplay Effect Remaining and Duration nodes, as it expects a Gameplay Effect Handle as the handle query won't return the specific Effect via Tags via Blueprint. Don't ask me why, I tried for four hours and even ChatGPT says it won't work in Blueprint... but it is possible to do with C++.
For context, this is what the original setup was to query cooldown information, utilising a Gameplay Effect class reference passed through to the UI component (it was stored on an ability Struct within a Data Table...
The new approach combines the GameplayEffectQuery with a Gameplay Tag check to retrieve the TimeRemaining and CooldownDuration directly from the Gameplay Ability System itself within C++. As such, the UFUNCTION we set up has the Ability System Component as an input (to query), and the specific GameplayTagContainer we want to look for - using this, we output both the Cooldown Remaining and Cooldown Duration so we can update the UI. The below code is what the UFUNCTION looks like (Titled Get Cooldown Remaining and Duration) in the .h file.
#pragma once
#include "Kismet/BlueprintFunctionLibrary.h"
#include "GameplayTagContainer.h"
#include "GameplayEffectTypes.h"
#include "GASBlueprintFunctionLibrary.generated.h"
class UAbilitySystemComponent;
UCLASS()
class GAS_CPP_TEST_API UGASBlueprintFunctionLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
/** Returns remaining time and total duration for a cooldown tag */
UFUNCTION(BlueprintCallable, Category = "GAS|Cooldown")
static void GetCooldownRemainingAndDuration(UAbilitySystemComponent* ASC, const FGameplayTagContainer CooldownTags, float& TimeRemaining, float& CooldownDuration);
};
The GetCooldownRemainingAndDuration function matches the provided tag with the active GameplayEffects on the ASC, and for each of those, retrieves matching specs and returns the Duration and the TimeLeft. It's pretty simple in application, however as discussed, this isn't exposed on a Blueprint level already so it needed to be done through this approach. The .cpp code for this is below.
#include "GASBlueprintFunctionLibrary.h"
#include "AbilitySystemComponent.h"
#include "GameplayEffect.h"
#include "AbilitySystemBlueprintLibrary.h"
//Function to retrieve how long left on the cooldown and the overall duration of the cooldown, helps with UI displays
void UGASBlueprintFunctionLibrary::GetCooldownRemainingAndDuration(UAbilitySystemComponent* ASC, const FGameplayTagContainer CooldownTags, float& TimeRemaining, float& CooldownDuration)
{
TimeRemaining = 0.f;
CooldownDuration = 0.f;
if (!ASC || CooldownTags.Num() == 0)
{
return;
}
const FGameplayEffectQuery Query = FGameplayEffectQuery::MakeQuery_MatchAnyOwningTags(CooldownTags);
TArray MatchingEffects = ASC->GetActiveEffects(Query);
float LongestRemaining = 0.f;
float LongestDuration = 0.f;
for (const FActiveGameplayEffectHandle Handle : MatchingEffects)
{
const FActiveGameplayEffect* ActiveEffect = ASC->GetActiveGameplayEffect(Handle);
if (ActiveEffect && ActiveEffect->Spec.GetDuration() > 0.f)
{
float TimeLeft = ActiveEffect->GetTimeRemaining(ASC->GetWorld()->GetTimeSeconds());
if (TimeLeft > LongestRemaining)
{
LongestRemaining = TimeLeft;
LongestDuration = ActiveEffect->Spec.GetDuration();
}
}
}
TimeRemaining = LongestRemaining;
CooldownDuration = LongestDuration;
}
As a result, we've now created a useful function to query the status of the cooldown which we can use to help us display the status of abilities once used, and update elements such as UI accordingly. The optimised UI blueprint looks like this:
Final Comments and Notes
Being able to reduce the amount of Gameplay Effects through using a Master Cooldown means we can reduce the amount of Gameplay Effect files we need to manage, which is a fantastic optimisation approach for this project, especially if we intend to have a range of classes with a range of abilities. We can centralise the abilities and essentially build abilities through Data Tables, rather than pure Blueprint. The next stage for this optimisation pathway is to apply the same approach for the damage profiles, as we could reduce this down to simple profiles such as ticking damage, debuff, instant etc to help us reuse Gameplay Effects for these as well.
This project is frustratingly complex for me, but I'm definitely learning a lot about game design and the Gameplay Ability System...