Gameplay Ability System Course Project - Development Blog

Hello again everyone! Welcome to another entry in this on-going blog/development thread for my GAS Course Project. For more context of this project, please see the very first post in this thread, and as usual, you can find my socials here:

Twitch : Twitch
Discord : UE5 - GAS Course Project - JevinsCherries
Tiktok : Devin Sherry (@jevinscherriesgamedev) | TikTok
Youtube : https://youtube.com/@jevinscherries?si=GKt32rDN6z2wn5xV
Twitter : https://twitter.com/jevincherries
Instagram : Jevins Cherries Gamedev (@jevinscherriesgamedev) • Instagram photos and videos

Today we are going to talk about Ability Cooldowns, including showing how to hook in remaining cooldown duration time into the ability UI. For my project, each activatable ability via enhanced input has a cooldown gameplay effect that controls for how long after activation an ability can be activated again. As always, I cannot recommend enough Tranek’s documentation on GAS, and you can find their in-depth documentation of Cooldowns linked here.


What is an Ability Cooldown?

An ability cooldown is a mechanism that can be used within the Gameplay Ability System to prevent abilities from be activated for a specified duration after initial activation. This is a common role-playing game mechanic to prevent ability spamming, and as a path-way for character progression; i.e., shorter cool-downs at higher player level or skill-tree perks to reduce cooldowns.

Classes to research on your own:

UGameplayAbility

Additional Reading:

Adding a Global Cooldown Using GAS

Dynamic Cooldowns

Kaos Spectrum - Adjusting Durations/Cooldowns of Active Gameplay Effects


When trying to activate an ability, the Gameplay Ability class has built in checks to verify that the ability in question can be activated. These two checks include Ability Cost and Ability Cooldown. At the time of this writing, I do not use Ability Cost; however, this is a mechanism I will be using in the future of this project. In short, Ability Cost can represent the amount of mana, or any other gameplay related resource, required to activate an ability.

The functions in question are:

bool UGameplayAbility::CheckCooldown(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, OUT FGameplayTagContainer* OptionalRelevantTags) const
{
	const FGameplayTagContainer* CooldownTags = GetCooldownTags();
	if (CooldownTags)
	{
		if (CooldownTags->Num() > 0)
		{
			UAbilitySystemComponent* const AbilitySystemComponent = ActorInfo->AbilitySystemComponent.Get();
			check(AbilitySystemComponent != nullptr);
			if (AbilitySystemComponent->HasAnyMatchingGameplayTags(*CooldownTags))
			{
				const FGameplayTag& CooldownTag = UAbilitySystemGlobals::Get().ActivateFailCooldownTag;

				if (OptionalRelevantTags && CooldownTag.IsValid())
				{
					OptionalRelevantTags->AddTag(CooldownTag);
				}

				return false;
			}
		}
	}
	return true;
}
bool UGameplayAbility::CheckCost(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, OUT FGameplayTagContainer* OptionalRelevantTags) const
{
	UGameplayEffect* CostGE = GetCostGameplayEffect();
	if (CostGE)
	{
		UAbilitySystemComponent* const AbilitySystemComponent = ActorInfo->AbilitySystemComponent.Get();
		check(AbilitySystemComponent != nullptr);
		if (!AbilitySystemComponent->CanApplyAttributeModifiers(CostGE, GetAbilityLevel(Handle, ActorInfo), MakeEffectContext(Handle, ActorInfo)))
		{
			const FGameplayTag& CostTag = UAbilitySystemGlobals::Get().ActivateFailCostTag;

			if (OptionalRelevantTags && CostTag.IsValid())
			{
				OptionalRelevantTags->AddTag(CostTag);
			}
			return false;
		}
	}
	return true;
}

Note: There are console commands that can be used to ignore both cooldowns and costs. AbilitySystem.IgnoreCooldowns | AbilitySystem.IgnoreCosts

These commands are checked via

AbilitySystemGlobals.ShouldIgnoreCooldowns()

and

AbilitySystemGlobals.ShouldIgnoreCosts()

respectively.


What is a Cooldown Gameplay Effect?

Gameplay Abilities have a parameter, Cooldown Gameplay Effect class. If this is left empty, it is assumed that the ability does not have a cooldown.

How to make a Cooldown Gameplay Effect?

There are two aspects of a Cooldown Gameplay Effect that are required to be present in order to work within a Gameplay Ability. To be clear, this is just an ordinary UGameplayEffect class; not a special cooldown gameplay effect class.

Duration - A cooldown requires a duration that represents the time between ability activations. A duration policy of instant will be treated as if there is no cooldown at all, and an infinite policy, at least in the context of my project, completely breaks the ability and it cannot be activated again.

Target Tags - A required component of the Gameplay Effect is the Target Tags Gameplay Effect Component, which is used to grant a gameplay tag to the owning ability system component, and is required in order for the cool-down effect to be recognized. Please again refer to the Check Cooldown function and its requirement of CooldownTags:

bool UGameplayAbility::CheckCooldown(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, OUT FGameplayTagContainer* OptionalRelevantTags) const
{
	const FGameplayTagContainer* CooldownTags = GetCooldownTags();
	if (CooldownTags)
	{
		if (CooldownTags->Num() > 0)
		{
			UAbilitySystemComponent* const AbilitySystemComponent = ActorInfo->AbilitySystemComponent.Get();
			check(AbilitySystemComponent != nullptr);
			if (AbilitySystemComponent->HasAnyMatchingGameplayTags(*CooldownTags))
			{
				const FGameplayTag& CooldownTag = UAbilitySystemGlobals::Get().ActivateFailCooldownTag;

				if (OptionalRelevantTags && CooldownTag.IsValid())
				{
					OptionalRelevantTags->AddTag(CooldownTag);
				}

				return false;
			}
		}
	}
	return true;
}

Depending on how you organize your project, and more specifically, your abilities you may want to only create a one global cooldown effect that you update its duration at runtime when activating your ability; rather than creating unique cooldowns effects for each ability. Tranek’s documentation goes over this here: Cooldown Gameplay Effect

In past projects, and most likely in the future of this one, there were attributes used to control both global cooldown modifiers and ability group/type cooldown modifiers; for example, cooldown modifier attributes for shield based abilities. In this case, it would be useful to use a Custom Calculation Class to modify the incoming duration value passed in via attributes that may affect them.


The following are snippets of code that are relevant for when an ability is activated and both ability cooldown & ability cost are checked in order for the ability to activate successfully:

void UGameplayAbility::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData)
{
	if (TriggerEventData && bHasBlueprintActivateFromEvent)
	{
		// A Blueprinted ActivateAbility function must call CommitAbility somewhere in its execution chain.
		K2_ActivateAbilityFromEvent(*TriggerEventData);
	}
	else if (bHasBlueprintActivate)
	{
		// A Blueprinted ActivateAbility function must call CommitAbility somewhere in its execution chain.
		K2_ActivateAbility();
	}
	else if (bHasBlueprintActivateFromEvent)
	{
		UE_LOG(LogAbilitySystem, Warning, TEXT("Ability %s expects event data but none is being supplied. Use 'Activate Ability' instead of 'Activate Ability From Event' in the Blueprint."), *GetName());
		constexpr bool bReplicateEndAbility = false;
		constexpr bool bWasCancelled = true;
		EndAbility(Handle, ActorInfo, ActivationInfo, bReplicateEndAbility, bWasCancelled);
	}
	else
	{
		// Native child classes should override ActivateAbility and call CommitAbility.
		// CommitAbility is used to do one last check for spending resources.
		// Previous versions of this function called CommitAbility but that prevents the callers
		// from knowing the result. Your override should call it and check the result.
		// Here is some starter code:
		
		//	if (!CommitAbility(Handle, ActorInfo, ActivationInfo))
		//	{			
		//		constexpr bool bReplicateEndAbility = true;
		//		constexpr bool bWasCancelled = true;
		//		EndAbility(Handle, ActorInfo, ActivationInfo, bReplicateEndAbility, bWasCancelled);
		//	}
	}
}
bool UGameplayAbility::CommitAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, OUT FGameplayTagContainer* OptionalRelevantTags)
{
	// Last chance to fail (maybe we no longer have resources to commit since we after we started this ability activation)
	if (!CommitCheck(Handle, ActorInfo, ActivationInfo, OptionalRelevantTags))
	{
		return false;
	}

	CommitExecute(Handle, ActorInfo, ActivationInfo);

	// Fixme: Should we always call this or only if it is implemented? A noop may not hurt but could be bad for perf (storing a HasBlueprintCommit per instance isn't good either)
	K2_CommitExecute();

	// Broadcast this commitment
	ActorInfo->AbilitySystemComponent->NotifyAbilityCommit(this);

	return true;
}
bool UGameplayAbility::CommitCheck(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, OUT FGameplayTagContainer* OptionalRelevantTags)
{
	/**
	 *	Checks if we can (still) commit this ability. There are some subtleties here.
	 *		-An ability can start activating, play an animation, wait for a user confirmation/target data, and then actually commit
	 *		-Commit = spend resources/cooldowns. It's possible the source has changed state since it started activation, so a commit may fail.
	 *		-We don't want to just call CanActivateAbility() since right now that also checks things like input inhibition.
	 *			-E.g., its possible the act of starting your ability makes it no longer activatable (CanActivateAbility() may be false if called here).
	 */

	const bool bValidHandle = Handle.IsValid();
	const bool bValidActorInfoPieces = (ActorInfo && (ActorInfo->AbilitySystemComponent != nullptr));
	const bool bValidSpecFound = bValidActorInfoPieces && (ActorInfo->AbilitySystemComponent->FindAbilitySpecFromHandle(Handle) != nullptr);

	// Ensure that the ability spec is even valid before trying to process the commit
	if (!bValidHandle || !bValidActorInfoPieces || !bValidSpecFound)
	{
		ABILITY_LOG(Warning, TEXT("UGameplayAbility::CommitCheck provided an invalid handle or actor info or couldn't find ability spec: %s Handle Valid: %d ActorInfo Valid: %d Spec Not Found: %d"), *GetName(), bValidHandle, bValidActorInfoPieces, bValidSpecFound);
		return false;
	}

	UAbilitySystemGlobals& AbilitySystemGlobals = UAbilitySystemGlobals::Get();

	if (!AbilitySystemGlobals.ShouldIgnoreCooldowns() && !CheckCooldown(Handle, ActorInfo, OptionalRelevantTags))
	{
		return false;
	}

	if (!AbilitySystemGlobals.ShouldIgnoreCosts() && !CheckCost(Handle, ActorInfo, OptionalRelevantTags))
	{
		return false;
	}

	return true;
}
void UGameplayAbility::CommitExecute(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo)
{
	ApplyCooldown(Handle, ActorInfo, ActivationInfo);

	ApplyCost(Handle, ActorInfo, ActivationInfo);
}
void UGameplayAbility::ApplyCooldown(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo) const
{
	UGameplayEffect* CooldownGE = GetCooldownGameplayEffect();
	if (CooldownGE)
	{
		ApplyGameplayEffectToOwner(Handle, ActorInfo, ActivationInfo, CooldownGE, GetAbilityLevel(Handle, ActorInfo));
	}
}

Ability Cooldown UI

In order to get the Ability UI to update based on remaining cooldown after ability activation, I created an ability task that monitors when the corresponding ability cooldown tag is added and then will get the remaining time of the cooldown effect at a specified interval.

This task is based on the code provide in Tranek’s documentation: Get the Cooldown Gameplay Effect’s Remaining Time

Note: I use the same task for checking my Duration-based abilities, and updating their UI; however, I will not be going into details about that aspect.

In general, this task works for my needs, but I don’t like having to specify an interval value of 0.025f in order to get a smooth progression of the UI progress bar, and its something I would want to possibly revisit in the future.

Here is the code:

AbilityTask_WaitForDurationEffectChange.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "Kismet/BlueprintAsyncActionBase.h"
#include "AbilitySystemComponent.h"
#include "GameplayTagContainer.h"
#include "AbilityTask_WaitForDurationEffectChange.generated.h"


DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FOnDurationChanged, FGameplayTag, DurationTag, float, TimeRemaining, float, Duration);

/**
 * 
 */

UCLASS(Abstract)
class GASCOURSE_API UAbilityTask_WaitForDurationEffectChange: public UBlueprintAsyncActionBase
{

	GENERATED_BODY()
	
public:

	UFUNCTION(BlueprintCallable)
	void EndTask();
	
	UPROPERTY(BlueprintAssignable)
	FOnDurationChanged OnDurationBegin;

	UPROPERTY(BlueprintAssignable)
	FOnDurationChanged OnDurationEnd;

	UPROPERTY(BlueprintAssignable)
	FOnDurationChanged OnDurationTimeUpdated;

protected:

	UPROPERTY()
	UAbilitySystemComponent* ASC;

	FGameplayTagContainer DurationTags;
	float DurationInterval = 0.1f;
	bool bUseServerCooldown;
	const UObject* WorldContext;

	/**
	 * This method is invoked when a gameplay effect is added to the target ability system component (ASC).
	 * It retrieves the asset tags and granted tags from the applied gameplay effect specification (SpecApplied)
	 * and checks if any of them match the duration tags (DurationTagArray) specified.
	 * If a match is found, it determines the remaining time and total duration for the duration tag, and broadcasts
	 * the OnDurationBegin event with the duration tag, time remaining, and duration as parameters.
	 *
	 * Here are the possible scenarios for broadcasting the OnDurationBegin event:
	 * - If the owner of the ASC is the server, it broadcasts the event with the duration tag, time remaining, and duration as parameters.
	 * - If bUseServerCooldown is false and the applied effect is not replicated, it broadcasts the event with the duration tag, time remaining, and duration as parameters.
	 * - If bUseServerCooldown is true and the applied effect is not replicated, it broadcasts the event with the duration tag, time remaining, and duration as parameters.
	 * - If bUseServerCooldown is true and the applied effect is replicated, it broadcasts the event with the duration tag, -1.0f, and -1.0f as parameters.
	 *
	 * Additionally, if the WorldContext is not null, it sets a timer that calls the OnDurationUpdate method periodically
	 * with a specified frequency (DurationInterval).
	 *
	 * @param InTargetASC The target ability system component to which the gameplay effect is added.
	 * @param InSpecApplied The gameplay effect specification that was applied.
	 * @param ActiveHandle The handle of the active gameplay effect.
	 */
	void OnActiveGameplayEffectAddedCallback(UAbilitySystemComponent* InTargetASC, const FGameplayEffectSpec& InSpecApplied, FActiveGameplayEffectHandle ActiveHandle);

	/**
	 * \brief Notifies when the duration tag of a duration effect has changed.
	 *
	 * This method is called when the duration tag of a duration effect has changed to a new count.
	 * If the new count is zero, it means that the duration has ended.
	 *
	 * \param InDurationTag The gameplay tag of the duration effect.
	 * \param InNewCount The new count of the duration effect.
	 */
	void DurationTagChanged(const FGameplayTag InDurationTag, int32 InNewCount);

	/**
	 * Retrieves the remaining time and total duration of the longest active cooldown with the specified tags.
	 *
	 * @param InDurationTags The tags used to identify the cooldowns.
	 * @param TimeRemaining (out) The remaining time of the longest active cooldown.
	 * @param InDuration (out) The total duration of the longest active cooldown.
	 *
	 * @return True if a cooldown with the specified tags is found, false otherwise.
	 */
	bool GetCooldownRemainingForTag(const FGameplayTagContainer& InDurationTags, float& TimeRemaining, float& InDuration) const;

	/**
	 * @brief The OnDurationUpdate method is called when the duration of a particular effect is updated.
	 *
	 * This method calculates the time remaining and total duration of the effect and broadcasts these values
	 * using the OnDurationTimeUpdated event.
	 *
	 * @see UAbilityTask_WaitForDurationEffectChange::OnDurationTimeUpdated
	 */
	UFUNCTION()
	void OnDurationUpdate();

private:

	
	FTimerHandle DurationTimeUpdateTimerHandle;
};

UCLASS(BlueprintType, meta = (ExposedAsyncProxy = AsyncTask))
class GASCOURSE_API UAbilityTask_WaitOnDurationChange : public UAbilityTask_WaitForDurationEffectChange
{
	GENERATED_BODY()
	
public:
	
	/**
	 * WaitOnDurationChange is a static method that creates and returns an instance of UAbilityTask_WaitOnDurationChange.
	 * This task listens for changes in the duration of active gameplay effects with the specified duration tags.
	 *
	 * @param InAbilitySystemComponent The ability system component to listen for duration changes on.
	 * @param InDurationTags The gameplay tag container specifying which duration tags to listen for changes on.
	 * @param InDurationInterval The interval (in seconds) at which to check for duration changes.
	 * @param bInUseServerCooldown Determines if server cooldown should be used for duration changes.
	 * @return A new instance of UAbilityTask_WaitOnDurationChange that is set up to listen for duration changes.
	 */
	UFUNCTION(BlueprintCallable, Category="GASCourse|Ability|Tasks", meta=(BlueprintInternalUseOnly = "true"))
	static UAbilityTask_WaitOnDurationChange* WaitOnDurationChange(UAbilitySystemComponent* InAbilitySystemComponent,FGameplayTagContainer InDurationTags, float InDurationInterval=0.05f, bool bInUseServerCooldown=true);
	
};

UCLASS(BlueprintType, meta = (ExposedAsyncProxy = AsyncTask))
class GASCOURSE_API UAbilityTask_WaitOnCooldownChange : public UAbilityTask_WaitForDurationEffectChange
{
	GENERATED_BODY()
	
public:
	/**
	 * Waits for the cooldown status of ability to change.
	 *
	 * @param InAbilitySystemComponent The ability system component to wait on cooldown change.
	 * @param InCooldownTags The cooldown tags to listen for changes.
	 * @param InDurationInterval The duration interval to check for cooldown changes.
	 * @param bInUseServerCooldown Determines if server cooldown should be used.
	 * @return The instance of UAbilityTask_WaitOnCooldownChange.
	 */
	UFUNCTION(BlueprintCallable, Category="GASCourse|Ability|Tasks", meta=(BlueprintInternalUseOnly = "true"))
	static UAbilityTask_WaitOnCooldownChange* WaitOnCooldownChange(UAbilitySystemComponent* InAbilitySystemComponent,FGameplayTagContainer InCooldownTags, float InDurationInterval = 1.0f, bool bInUseServerCooldown=true);
	
};

AbilityTaskWaitForDurationEffectChange.cpp

// Fill out your copyright notice in the Description page of Project Settings.


#include "Game/GameplayAbilitySystem/Tasks/AbilityTask_WaitForDurationEffectChange.h"

UAbilityTask_WaitOnDurationChange* UAbilityTask_WaitOnDurationChange::WaitOnDurationChange(UAbilitySystemComponent* InAbilitySystemComponent,FGameplayTagContainer InDurationTags, float InDurationInterval, bool bInUseServerCooldown)
{
	UAbilityTask_WaitOnDurationChange* MyObj = NewObject<UAbilityTask_WaitOnDurationChange>();
	MyObj->WorldContext = GEngine->GetWorldFromContextObjectChecked(InAbilitySystemComponent);
	MyObj->ASC = InAbilitySystemComponent;
	MyObj->DurationTags = InDurationTags;
	MyObj->DurationInterval = InDurationInterval;
	MyObj->bUseServerCooldown = bInUseServerCooldown;


	if(!IsValid(InAbilitySystemComponent) || InDurationTags.Num() < 1)
	{
		MyObj->EndTask();
		return nullptr;
	}

	InAbilitySystemComponent->OnActiveGameplayEffectAddedDelegateToSelf.AddUObject(MyObj, &UAbilityTask_WaitForDurationEffectChange::OnActiveGameplayEffectAddedCallback);

	TArray<FGameplayTag> DurationTagArray;
	InDurationTags.GetGameplayTagArray(DurationTagArray);

	for(const FGameplayTag DurationTag : DurationTagArray)
	{
		InAbilitySystemComponent->RegisterGameplayTagEvent(DurationTag, EGameplayTagEventType::NewOrRemoved).AddUObject(MyObj, &UAbilityTask_WaitForDurationEffectChange::DurationTagChanged);
	}
	
	return MyObj;
}

UAbilityTask_WaitOnCooldownChange* UAbilityTask_WaitOnCooldownChange::WaitOnCooldownChange(
	UAbilitySystemComponent* InAbilitySystemComponent, FGameplayTagContainer InCooldownTags, float InDurationInterval,
	bool bInUseServerCooldown)
{
	UAbilityTask_WaitOnCooldownChange* MyObj = NewObject<UAbilityTask_WaitOnCooldownChange>();
	MyObj->WorldContext = GEngine->GetWorldFromContextObjectChecked(InAbilitySystemComponent);
	MyObj->ASC = InAbilitySystemComponent;
	MyObj->DurationTags = InCooldownTags;
	MyObj->DurationInterval = InDurationInterval;
	MyObj->bUseServerCooldown = bInUseServerCooldown;


	if(!IsValid(InAbilitySystemComponent) || InCooldownTags.Num() < 1)
	{
		MyObj->EndTask();
		return nullptr;
	}

	InAbilitySystemComponent->OnActiveGameplayEffectAddedDelegateToSelf.AddUObject(MyObj, &UAbilityTask_WaitForDurationEffectChange::OnActiveGameplayEffectAddedCallback);

	TArray<FGameplayTag> DurationTagArray;
	InCooldownTags.GetGameplayTagArray(DurationTagArray);

	for(const FGameplayTag DurationTag : DurationTagArray)
	{
		InAbilitySystemComponent->RegisterGameplayTagEvent(DurationTag, EGameplayTagEventType::NewOrRemoved).AddUObject(MyObj, &UAbilityTask_WaitForDurationEffectChange::DurationTagChanged);
	}
	
	return MyObj;
}

void UAbilityTask_WaitForDurationEffectChange::EndTask()
{
	if(IsValid(ASC))
	{
		ASC->OnActiveGameplayEffectAddedDelegateToSelf.RemoveAll(this);

		TArray<FGameplayTag> DurationTagArray;
		DurationTags.GetGameplayTagArray(DurationTagArray);

		for(const FGameplayTag DurationTag :DurationTagArray)
		{
			ASC->RegisterGameplayTagEvent(DurationTag, EGameplayTagEventType::NewOrRemoved).RemoveAll(this);
		}
	}

	SetReadyToDestroy();
	MarkAsGarbage();
}

void UAbilityTask_WaitForDurationEffectChange::OnActiveGameplayEffectAddedCallback(UAbilitySystemComponent* InTargetASC,
	const FGameplayEffectSpec& InSpecApplied, FActiveGameplayEffectHandle ActiveHandle)
{
	FGameplayTagContainer AssetTags;
	InSpecApplied.GetAllAssetTags(AssetTags);

	FGameplayTagContainer GrantedTags;
	InSpecApplied.GetAllGrantedTags(GrantedTags);

	TArray<FGameplayTag> DurationTagArray;
	DurationTags.GetGameplayTagArray(DurationTagArray);

	for(FGameplayTag DurationTag : DurationTagArray)
	{
		if(AssetTags.HasTagExact(DurationTag) || GrantedTags.HasTagExact(DurationTag))
		{
			float TimeRemaining = 0.0f;
			float Duration = 0.0f;

			const FGameplayTagContainer DurationTagContainer(GrantedTags.GetByIndex(0));
			GetCooldownRemainingForTag(DurationTagContainer, TimeRemaining, Duration);

			if (ASC->GetOwnerRole() == ROLE_Authority)
			{
				// Player is Server
				OnDurationBegin.Broadcast(DurationTag, TimeRemaining, Duration);
			}
			else if (!bUseServerCooldown && InSpecApplied.GetContext().GetAbilityInstance_NotReplicated())
			{
				// Client using predicted cooldown
				OnDurationBegin.Broadcast(DurationTag, TimeRemaining, Duration);
			}
			else if (bUseServerCooldown && InSpecApplied.GetContext().GetAbilityInstance_NotReplicated() == nullptr)
			{
				// Client using Server's cooldown. This is Server's corrective cooldown GE.
				OnDurationBegin.Broadcast(DurationTag, TimeRemaining, Duration);
			}
			else if (bUseServerCooldown && InSpecApplied.GetContext().GetAbilityInstance_NotReplicated())
			{
				// Client using Server's cooldown but this is predicted cooldown GE.
				// This can be useful to gray out abilities until Server's cooldown comes in.
				OnDurationBegin.Broadcast(DurationTag, -1.0f, -1.0f);
			}
			
			if(WorldContext)
			{
				WorldContext->GetWorld()->GetTimerManager().SetTimer(DurationTimeUpdateTimerHandle, this, &UAbilityTask_WaitForDurationEffectChange::OnDurationUpdate, DurationInterval, true);
			}

		}
	}
}

void UAbilityTask_WaitForDurationEffectChange::DurationTagChanged(const FGameplayTag InDurationTag, int32 InNewCount)
{
	if(InNewCount == 0)
	{
		OnDurationEnd.Broadcast(InDurationTag, -1.0f, -1.0f);
		if(WorldContext)
		{
			WorldContext->GetWorld()->GetTimerManager().ClearTimer(DurationTimeUpdateTimerHandle);
			WorldContext->GetWorld()->GetTimerManager().ClearAllTimersForObject(this);
		}
	}
}

bool UAbilityTask_WaitForDurationEffectChange::GetCooldownRemainingForTag(const FGameplayTagContainer& InDurationTags,
	float& TimeRemaining, float& InDuration) const
{
	if(IsValid(ASC) && InDurationTags.Num() > 0)
	{
		TimeRemaining = 0.0f;
		InDuration = 0.0f;

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

			TimeRemaining = DurationAndTimeRemaining[BestIndex].Key;
			InDuration = DurationAndTimeRemaining[BestIndex].Value;

			return true;
		}
	}

	return false;
}

void UAbilityTask_WaitForDurationEffectChange::OnDurationUpdate()
{
	float TimeRemaining = 0.0f;
	float Duration = 0.0f;
	GetCooldownRemainingForTag(DurationTags, TimeRemaining, Duration);
	OnDurationTimeUpdated.Broadcast(DurationTags.GetByIndex(0), TimeRemaining, Duration);
}

Here is the Blueprint logic:

Finally, here is the current results:


References:

Mahadi - Unreal Engine 5 Ability Cooldown And Smooth Progress Bar

Ryan Laley - Unreal Engine 5 Tutorial - Action RPG Part 8: Cooldown UI

Reddit - u/lexical-decoherence - How to setup costs/cooldowns for abilities in gameplay ability system?


Thank you for taking the time to read this post, and I hope you were able to take something away from it. Please let me know if I got any information wrong, or explained something incorrectly! Also add any thoughts or questions or code review feedback so we can all learn together :slight_smile: I will be taking a break from making additional posts in this topic for the holidays, but I’ll be back in a few weeks with more!


Next Blog Post Topic:

Attribute Sets & Initialization

1 Like