The Games DevProgramming and Game Development. Tips, Tricks and Tutorials.

Adjusting Durations/Cooldowns of Active Gameplay Effects

· Updated Gameplay Ability System

Updated for UE 5.8: the ASC helpers now use the engine’s CheckDurationExpired, the samples were tidied so they compile, and I added notes on ModifyActiveEffectStartTime and the missing duration-changed broadcast.

A common question is: how can I increase/decrease cooldowns (or any duration-based effect) while they are already active? I did some research and came up with what I think is the nicest way to do it, and it uses Gameplay Effects to achieve it. The idea is simple: a dummy attribute holds the “duration modifiers”, and an Execution Calculation walks every active duration effect and re-evaluates its duration against that attribute.

If you haven’t used Execution Calculations before, read my post on understanding Gameplay Effect Execution Calculations first.

The dummy attribute

Put this in any attribute set you like (I recommend something like a PlayerSet or CharacterSet).

UPROPERTY(BlueprintReadOnly, Meta = (HideFromModifiers, AllowPrivateAccess = true))
FKaosGameplayAttributeData ActiveEffectDuration;
ATTRIBUTE_ACCESSORS(ThisClass, ActiveEffectDuration);

HideFromModifiers stops this attribute showing up in the attribute picker of a Gameplay Effect’s modifier list, so it can’t be modified there by accident. (FKaosGameplayAttributeData is my own type; a plain FGameplayAttributeData works just as well.)

ASC helpers

We need a few things exposed from your project-specific ASC. ActiveGameplayEffects is a protected member of UAbilitySystemComponent, so a subclass can reach it.

FActiveGameplayEffect* UKaosAbilitySystemComponent::GetActiveGameplayEffect_Mutable(const FActiveGameplayEffectHandle Handle)
{
	return ActiveGameplayEffects.GetActiveGameplayEffect(Handle);
}

TArray<FActiveGameplayEffectHandle> UKaosAbilitySystemComponent::GetAllActiveEffectHandles() const
{
	return ActiveGameplayEffects.GetAllActiveEffectHandles();
}

void UKaosAbilitySystemComponent::MarkActiveGameplayEffectDirty(FActiveGameplayEffect* ActiveGE)
{
	if (ActiveGE)
	{
		ActiveGameplayEffects.MarkItemDirty(*ActiveGE);
	}
}

void UKaosAbilitySystemComponent::CheckActiveEffectDuration(const FActiveGameplayEffectHandle& Handle)
{
	CheckDurationExpired(Handle);
}

Header:

/** Returns a mutable pointer to the active gameplay effect for the supplied handle, or nullptr if not found. */
FActiveGameplayEffect* GetActiveGameplayEffect_Mutable(FActiveGameplayEffectHandle Handle);

/** Returns the handles of all active gameplay effects. */
TArray<FActiveGameplayEffectHandle> GetAllActiveEffectHandles() const;

/** Marks the active gameplay effect as dirty so the change replicates. */
void MarkActiveGameplayEffectDirty(FActiveGameplayEffect* ActiveGE);

/** Re-checks the duration of the effect, removing it if it has expired and resetting its expiry timer otherwise. */
void CheckActiveEffectDuration(const FActiveGameplayEffectHandle& Handle);

The first returns a mutable pointer to the active effect, the second returns a copy of all active effect handles, the third marks an effect dirty so the change replicates, and the last re-checks the duration after we changed it. That last step is required: it removes the effect if it has now expired, otherwise it resets the expiry timer for the new remaining time. Changing the duration alone isn’t enough.

The Execution Calculation

We need a struct that captures our ActiveEffectDuration attribute.

struct FKaosActiveDurationStatics
{
	FGameplayEffectAttributeCaptureDefinition TargetActiveEffectDurationDef;

	FKaosActiveDurationStatics()
	{
		TargetActiveEffectDurationDef = FGameplayEffectAttributeCaptureDefinition(UKaosPlayerSet::GetActiveEffectDurationAttribute(), EGameplayEffectAttributeCaptureSource::Target, false);
	}
};

static FKaosActiveDurationStatics& KaosActiveDurationStatics()
{
	static FKaosActiveDurationStatics Statics;
	return Statics;
}

I used a non-snapshot capture here (the last parameter), so we read the live value from the target rather than a copy taken earlier.

Now the calculation itself.

UKaosActiveEffectDurationExecution::UKaosActiveEffectDurationExecution()
{
	RelevantAttributesToCapture.Add(KaosActiveDurationStatics().TargetActiveEffectDurationDef);
}

void UKaosActiveEffectDurationExecution::Execute_Implementation(const FGameplayEffectCustomExecutionParameters& ExecutionParams, FGameplayEffectCustomExecutionOutput& OutExecutionOutput) const
{
#if WITH_SERVER_CODE
	UKaosAbilitySystemComponent* TargetASC = Cast<UKaosAbilitySystemComponent>(ExecutionParams.GetTargetAbilitySystemComponent());
	if (!TargetASC)
	{
		return;
	}

	FAggregatorEvaluateParameters EvaluateParameters;

	for (const FActiveGameplayEffectHandle& Handle : TargetASC->GetAllActiveEffectHandles())
	{
		FActiveGameplayEffect* ActiveGE = TargetASC->GetActiveGameplayEffect_Mutable(Handle);

		// Only effects with a fixed duration can be adjusted.
		if (!ActiveGE || !ActiveGE->Spec.Def || ActiveGE->Spec.Def->DurationPolicy != EGameplayEffectDurationType::HasDuration)
		{
			continue;
		}

		// Use the effect's asset tags as the target tags, so modifiers can be limited to e.g. cooldown effects.
		FGameplayTagContainer AssetTags;
		ActiveGE->Spec.GetAllAssetTags(AssetTags);
		EvaluateParameters.TargetTags = &AssetTags;

		// The current duration is the base value, the modifiers on our attribute then scale it.
		float NewDuration = 0.f;
		if (!ExecutionParams.AttemptCalculateCapturedAttributeMagnitudeWithBase(KaosActiveDurationStatics().TargetActiveEffectDurationDef, EvaluateParameters, ActiveGE->GetDuration(), NewDuration))
		{
			continue;
		}

		// Never let the duration reach 0, which would make it an instant effect.
		ActiveGE->Spec.Duration = FMath::Max(NewDuration, UE_SMALL_NUMBER);

		TargetASC->MarkActiveGameplayEffectDirty(ActiveGE);

		// Removes the effect if it has expired and refreshes the expiry timer otherwise.
		TargetASC->CheckActiveEffectDuration(Handle);
	}
#endif
}

In short: iterate all active effects, keep the ones with a duration, calculate the new duration, mark it dirty and then check the duration (to see if it should expire).

Here is an example GE that will reduce cooldown by 25% when applied.

Notes and gotchas

  • AttemptCalculateCapturedAttributeMagnitudeWithBase uses the value you pass in instead of the attribute’s own base value (the engine evaluates the aggregator with that base). So passing the current duration in, with a multiply-style modifier of 0.75, gives you 75% of the duration.
  • Each run re-scales the current duration of every matching effect, so running it twice compounds. That is fine for a one-off “reduce by 25%”, but don’t use it as a “sync durations to an attribute” tick.
  • Writing Spec.Duration directly does not fire the ASC’s duration-changed callback (OnGameplayEffectDurationChange), because the container function that broadcasts it is private. UI that only listens to that delegate won’t update by itself. If you just want to shift an effect’s timing by some seconds, the ASC already has ModifyActiveEffectStartTime(Handle, StartTimeDiff), which moves the start time, checks for expiry, broadcasts the duration change and marks the effect dirty for you. A negative diff shortens the remaining time, so for a flat “reduce cooldown by 2 seconds” that is the simpler option.
  • Instant effects never become active, and infinite effects have no duration to scale, which is why only HasDuration effects are touched.

Hope this helps. For any issues or problems, check out my Discord in About Me, or join Unreal Slackers.