Understanding Gameplay Effect Execution Calculations!
Updated for UE 5.8: rewritten with a full walkthrough, two worked examples and a gotchas section.
AttemptCalculateCapturedAttributeMagnitudeWithBaseis now described correctly (the base you pass replaces the attribute’s base value), the sample code is fixed, and scoped modifiers,GetIgnoreHandles, stack count handling and where executions run are covered.
A Gameplay Effect Execution Calculation (I’ll say “execution” from now on) is a class that runs custom code whenever a Gameplay Effect executes, and decides which attributes get modified and by how much. It can read as many attributes as it likes from both the source and the target, look at tags, SetByCaller values and the effect context, and then emit any number of modifiers as its output. Those outputs modify the base value of the attributes on the target, exactly like an instant effect’s normal modifiers would.
Below I’ll explain how they work, how to write one, and go through some complete examples. It’s not exhaustive, but it should help you understand things.
When should I use one?
You have three main tools for working out a number in a Gameplay Effect:
- A plain modifier (with a scalable float, an attribute-based magnitude or SetByCaller). Use this whenever it is enough. It is the cheapest, it is data-driven, and it works for instant, duration and infinite effects.
- A Modifier Magnitude Calculation (MMC,
UGameplayModMagnitudeCalculation). It overridesCalculateBaseMagnitudeand returns one float for one modifier. Because the result is used as a modifier magnitude, the system can re-evaluate it when the attributes it captured change (that’s how duration/infinite effects stay up to date). Use it for “this modifier’s value is a formula”. - An execution (
UGameplayEffectExecutionCalculation). It overridesExecuteand can emit any number of modifiers on any number of attributes, reads captured attributes from source and target, and can trigger conditional effects and control cue/stack handling. But it only runs when the effect executes (see the gotchas), it is not re-evaluated afterwards, and it can’t be predicted. Use it for “this effect does a bunch of things based on a formula”, typically damage, healing, resource conversion.
Rule of thumb: one number in, one modifier out, use an MMC. Multiple outputs, or heavy logic, use an execution.
The moving parts
The class you derive from is UGameplayEffectExecutionCalculation. It is abstract, and lives on the effect under Executions > Calculation Class. The engine only ever uses its Class Default Object, so your execution can’t hold any state (Execute is const for that reason).
On the class:
RelevantAttributesToCapture(inherited fromUGameplayEffectCalculation): the attributes you want the system to capture from the source and/or target for you.InvalidScopedModifierAttributes(editor only): attributes in the capture list that you don’t want designers to be able to add scoped modifiers for (see below).ValidTransientAggregatorIdentifiers(editor only): tags that designers can use as “temporary variables” with scoped modifiers.bRequiresPassedInTags: flags that this execution uses the effect’s Passed In Tags.
On the effect (FGameplayEffectExecutionDefinition), next to the calculation class:
- Passed In Tags: a tag container handed to your execution as is. Great for reusing one execution class with different behaviors.
- Calculation Modifiers: scoped modifiers, applied “in place” only inside this execution. More below.
- Conditional Gameplay Effects: other effects applied to the target if the execution says it was successful.
The main function you override is:
virtual void Execute_Implementation(const FGameplayEffectCustomExecutionParameters& ExecutionParams, FGameplayEffectCustomExecutionOutput& OutExecutionOutput) const override;
ExecutionParams holds everything coming in, and OutExecutionOutput collects everything going out.
ExecutionParams
GetOwningSpec - The (const) spec of the effect running this execution.
GetOwningSpecForPreExecuteMod - Non-const version of the above. Be careful with it.
GetTargetAbilitySystemComponent - The ASC of the actor being affected.
GetSourceAbilitySystemComponent - The ASC of the actor that created the effect (can be null!).
GetPassedInTags - The Passed In Tags from the effect's execution definition.
GetPredictionKey - The prediction key associated with the effect.
GetIgnoreHandles - Active effect handles that should be ignored when evaluating.
On top of that there are the functions that evaluate captured attributes, described next.
Capturing attributes
An execution can only evaluate attributes that were captured. You describe a capture with FGameplayEffectAttributeCaptureDefinition, which has three parts:
- the attribute to capture,
- the source to capture from,
Source(the instigator) orTarget(the recipient), - a snapshot flag.
The source’s attributes are captured when the effect spec is created, the target’s attributes when the effect is applied to the target. With snapshot on, the aggregator is copied at capture time, so later changes to the attribute won’t be seen. With snapshot off, the spec holds a live reference to the attribute’s aggregator, so it sees the current modifiers when you evaluate it. Rule of thumb: snapshot the source (you want the attacker’s stats as they were when the attack was made), don’t snapshot the target (you want their defenses right now).
The engine has helper macros for the definitions:
struct FAresDamageStatics
{
DECLARE_ATTRIBUTE_CAPTUREDEF(AttackPower);
DECLARE_ATTRIBUTE_CAPTUREDEF(CritChance);
DECLARE_ATTRIBUTE_CAPTUREDEF(Armor);
FAresDamageStatics()
{
// Attribute set class, attribute name, source or target, snapshot
DEFINE_ATTRIBUTE_CAPTUREDEF(UAresCombatSet, AttackPower, Source, true);
DEFINE_ATTRIBUTE_CAPTUREDEF(UAresCombatSet, CritChance, Source, true);
DEFINE_ATTRIBUTE_CAPTUREDEF(UAresHealthSet, Armor, Target, false);
}
};
DECLARE_ATTRIBUTE_CAPTUREDEF(AttackPower) declares an AttackPowerProperty and an AttackPowerDef member, and DEFINE_ATTRIBUTE_CAPTUREDEF fills them in. If you prefer to be explicit (I usually am), constructing the definition yourself is the same thing:
OutgoingBaseDamageDef = FGameplayEffectAttributeCaptureDefinition(UAresDamageSet::GetOutgoingBaseDamageAttribute(), EGameplayEffectAttributeCaptureSource::Source, true);
You then create the statics once and register the definitions in your constructor:
static const FAresDamageStatics& AresDamageStatics()
{
static FAresDamageStatics Statics;
return Statics;
}
UAresDamageExecution::UAresDamageExecution()
{
RelevantAttributesToCapture.Add(AresDamageStatics().AttackPowerDef);
RelevantAttributesToCapture.Add(AresDamageStatics().CritChanceDef);
RelevantAttributesToCapture.Add(AresDamageStatics().ArmorDef);
}
Registering them in RelevantAttributesToCapture is what makes the system capture them when the spec is made. If you forget, evaluation will just fail.
Evaluating attributes
FAggregatorEvaluateParameters tells the evaluator what context to evaluate in. The important members are SourceTags and TargetTags (modifiers with tag requirements only count if the tags match), IgnoreHandles, and the AppliedSourceTagFilter / AppliedTargetTagFilter containers. You normally fill in the source and target tags from the spec:
const FGameplayEffectSpec& Spec = ExecutionParams.GetOwningSpec();
FAggregatorEvaluateParameters EvaluateParameters;
EvaluateParameters.SourceTags = Spec.CapturedSourceTags.GetAggregatedTags();
EvaluateParameters.TargetTags = Spec.CapturedTargetTags.GetAggregatedTags();
The aggregated tags are the actor’s tags (ASC owned tags) combined with the tags coming from the spec itself (the effect’s asset tags, granted tags, dynamic tags). This is what makes stuff like “this resistance modifier only applies when the source has DamageType.Elemental.Fire” work (see my damage types post).
Then you evaluate:
float Armor = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(AresDamageStatics().ArmorDef, EvaluateParameters, Armor);
This takes the attribute’s base value from the captured aggregator, runs every qualifying modifier over it (following the normal formula, ((Base + AddBase) * MultiplyAdditive / DivideAdditive * MultiplyCompound) + AddFinal, per evaluation channel) and returns the result: the current value of the attribute, evaluated under your tags. It returns false if the attribute wasn’t captured, and in that case it leaves your output float alone, so always initialize it to something sensible.
There are some siblings worth knowing about:
// Same, but the value you pass in is used INSTEAD OF the attribute's own base value.
ExecutionParams.AttemptCalculateCapturedAttributeMagnitudeWithBase(Def, EvaluateParameters, BaseValue, OutMagnitude);
// Only the attribute's base value, no modifiers.
ExecutionParams.AttemptCalculateCapturedAttributeBaseValue(Def, OutBaseValue);
// Only the bonus contributed by the modifiers, without the base value.
ExecutionParams.AttemptCalculateCapturedAttributeBonusMagnitude(Def, EvaluateParameters, OutBonusMagnitude);
...WithBase is really useful when the “base” of your number comes from somewhere else (the ability, a SetByCaller value, a data table) but you still want the target/source’s modifiers to scale it. An older version of this post said the base was added to the attribute’s base. That’s not what happens: the aggregator is evaluated with your value as its base. For example, if you pass in 30 as the base and a modifier that multiplies by 1.5 exists, you get 45, and whatever the attribute’s own base value is doesn’t matter.
Scoped modifiers
Under Calculation Modifiers on the effect, designers can add modifiers that only exist inside this execution. They can target any captured attribute in RelevantAttributesToCapture (except ones you list in InvalidScopedModifierAttributes), or a “temporary variable” identified by a tag from ValidTransientAggregatorIdentifiers. When you call AttemptCalculateCapturedAttributeMagnitude for an attribute that has scoped modifiers, those modifiers are already included in the result. It is a nice way to let designers tweak an execution per effect without touching code, without changing the real attribute. For transient (tag identified) variables you read them with:
ExecutionParams.AttemptCalculateTransientAggregatorMagnitude(ExecutionTags::Data_Damage_Bonus, EvaluateParameters, Bonus);
Output modifiers
When you’ve calculated your values you tell the system what to modify with:
OutExecutionOutput.AddOutputModifier(FGameplayModifierEvaluatedData(Attribute, EGameplayModOp::Additive, Magnitude));
You can call it as many times as you want, on different attributes. These modifiers are applied to the target (the owner of the effect), one after the other, and for each one the attribute set’s PreGameplayEffectExecute and PostGameplayEffectExecute are called, just like with a normal modifier. If the target doesn’t have the attribute set the attribute belongs to, that modifier is skipped (and logged).
Because of that, the standard pattern is: the execution outputs to a meta attribute (like Damage), and the attribute set turns that into a real change in PostGameplayEffectExecute. That’s where you do clamping and death checks, not in the execution.
The other output functions
// Allow this execution's Conditional Gameplay Effects to fire (once we return).
OutExecutionOutput.MarkConditionalGameplayEffectsToTrigger();
// We handled stacking ourselves, don't scale our output modifiers by the stack count.
OutExecutionOutput.MarkStackCountHandledManually();
// We played the Gameplay Cues ourselves, don't fire the effect's cues after the execution.
OutExecutionOutput.MarkGameplayCuesHandledManually();
If the execution has a calculation class but you never call MarkConditionalGameplayEffectsToTrigger, its conditional effects don’t run. (With no calculation class at all, they always do.)
Example 1: A damage calculation
Let’s put it together. This is a fairly typical damage formula:
- base damage comes from the ability through a SetByCaller value,
- scaled by the attacker’s Attack Power,
- with a crit chance from the attacker (and the crit multiplier from a SetByCaller too),
- reduced by the target’s Armor,
- and an optional “ignore armor” behavior selected with a Passed In Tag on the effect.
The tags used here are native tags, defined as static tags at the top of the execution’s .cpp since nothing else needs them (see my post on native gameplay tags for the other macros):
namespace ExecutionTags
{
UE_DEFINE_GAMEPLAY_TAG_STATIC(Data_Damage_Base, "Data.Damage.Base");
UE_DEFINE_GAMEPLAY_TAG_STATIC(Data_Damage_CritMultiplier, "Data.Damage.CritMultiplier");
UE_DEFINE_GAMEPLAY_TAG_STATIC(Execution_IgnoreArmor, "Execution.IgnoreArmor");
}
struct FAresDamageStatics
{
FGameplayEffectAttributeCaptureDefinition AttackPowerDef;
FGameplayEffectAttributeCaptureDefinition CritChanceDef;
FGameplayEffectAttributeCaptureDefinition ArmorDef;
FAresDamageStatics()
{
// Source stats are snapshotted, we want them as they were when the attack happened.
AttackPowerDef = FGameplayEffectAttributeCaptureDefinition(UAresCombatSet::GetAttackPowerAttribute(), EGameplayEffectAttributeCaptureSource::Source, true);
CritChanceDef = FGameplayEffectAttributeCaptureDefinition(UAresCombatSet::GetCritChanceAttribute(), EGameplayEffectAttributeCaptureSource::Source, true);
// Target defenses are read live.
ArmorDef = FGameplayEffectAttributeCaptureDefinition(UAresHealthSet::GetArmorAttribute(), EGameplayEffectAttributeCaptureSource::Target, false);
}
};
static const FAresDamageStatics& AresDamageStatics()
{
static FAresDamageStatics Statics;
return Statics;
}
UAresDamageExecution::UAresDamageExecution()
{
RelevantAttributesToCapture.Add(AresDamageStatics().AttackPowerDef);
RelevantAttributesToCapture.Add(AresDamageStatics().CritChanceDef);
RelevantAttributesToCapture.Add(AresDamageStatics().ArmorDef);
bRequiresPassedInTags = true;
}
void UAresDamageExecution::Execute_Implementation(const FGameplayEffectCustomExecutionParameters& ExecutionParams, FGameplayEffectCustomExecutionOutput& OutExecutionOutput) const
{
const FAresDamageStatics& Statics = AresDamageStatics();
const FGameplayEffectSpec& Spec = ExecutionParams.GetOwningSpec();
FAggregatorEvaluateParameters EvaluateParameters;
EvaluateParameters.SourceTags = Spec.CapturedSourceTags.GetAggregatedTags();
EvaluateParameters.TargetTags = Spec.CapturedTargetTags.GetAggregatedTags();
// Base damage set by whoever created the spec. Missing values fall back to 0 without spamming the log.
const float BaseDamage = Spec.GetSetByCallerMagnitude(ExecutionTags::Data_Damage_Base, false, 0.f);
if (BaseDamage <= 0.f)
{
return;
}
// Attack power scales the base damage. The base is passed in, so the source's modifiers scale it.
float ScaledDamage = BaseDamage;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitudeWithBase(Statics.AttackPowerDef, EvaluateParameters, BaseDamage, ScaledDamage);
// Crit roll. Executions run on the server, so a plain random roll is fine here.
float CritChance = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(Statics.CritChanceDef, EvaluateParameters, CritChance);
if (FMath::FRand() * 100.f < CritChance)
{
ScaledDamage *= Spec.GetSetByCallerMagnitude(ExecutionTags::Data_Damage_CritMultiplier, false, 1.5f);
}
// Armor reduction, unless the effect asked us to skip it via a passed in tag.
if (!ExecutionParams.GetPassedInTags().HasTag(ExecutionTags::Execution_IgnoreArmor))
{
float Armor = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(Statics.ArmorDef, EvaluateParameters, Armor);
ScaledDamage *= 100.f / (100.f + FMath::Max(Armor, 0.f));
}
if (ScaledDamage > 0.f)
{
OutExecutionOutput.AddOutputModifier(FGameplayModifierEvaluatedData(UAresHealthSet::GetDamageAttribute(), EGameplayModOp::Additive, ScaledDamage));
OutExecutionOutput.MarkConditionalGameplayEffectsToTrigger();
}
}
Why each bit is there:
Spec.GetSetByCallerMagnitude(Tag, false, 0.f): the caller puts the value on the spec withSetSetByCallerMagnitude(Tag, Value)before applying it. Thefalseturns off the error log when it’s missing and0.fis the fallback....WithBase(AttackPowerDef, ..., BaseDamage, ScaledDamage): I’m using the capture to let attack power modifiers scale a value that does not come from the attribute. If you’d rather have plainDamage = Base * AttackPower / 100, use the non-WithBaseversion and do the math yourself. Both are valid, pick the one that matches how your designers think about the stat.ScaledDamageis initialized toBaseDamage, so if the capture fails we still have a sane value.- Passed In Tags: the same execution class can be used for “normal” and “true damage” effects, the designer just adds
Execution.IgnoreArmorto the Passed In Tags of the effect. - The output goes to a
Damagemeta attribute, not to Health directly. In your Health set:
void UAresHealthSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data)
{
Super::PostGameplayEffectExecute(Data);
if (Data.EvaluatedData.Attribute == GetDamageAttribute())
{
const float LocalDamage = GetDamage();
SetDamage(0.f);
if (LocalDamage > 0.f)
{
SetHealth(FMath::Clamp(GetHealth() - LocalDamage, 0.f, GetMaxHealth()));
}
}
}
Getting the ability that caused the damage is also possible via the effect context, for example Spec.GetContext().GetAbilityInstance_NotReplicated() (the ability instance, can be null, and it isn’t replicated so it’s only meaningful on the server), or GetAbility() for the CDO.
Example 2: A shield that absorbs damage first
Now something that reads the target’s state and splits the result across two attributes. The target has a Shield attribute, and incoming damage should drain the shield first, with only the leftover hitting Health. Some damage (say anything tagged Damage.Piercing) ignores the shield completely.
This is a good fit for an execution: it needs to read a live attribute, make a decision, and output modifiers on the target only, which is exactly what executions are for.
namespace ExecutionTags
{
UE_DEFINE_GAMEPLAY_TAG_STATIC(Data_Damage_Base, "Data.Damage.Base");
UE_DEFINE_GAMEPLAY_TAG_STATIC(Damage_Piercing, "Damage.Piercing");
}
UAresShieldedDamageExecution::UAresShieldedDamageExecution()
{
RelevantAttributesToCapture.Add(AresShieldedDamageStatics().ShieldDef);
}
void UAresShieldedDamageExecution::Execute_Implementation(const FGameplayEffectCustomExecutionParameters& ExecutionParams, FGameplayEffectCustomExecutionOutput& OutExecutionOutput) const
{
const FAresShieldedDamageStatics& Statics = AresShieldedDamageStatics();
const FGameplayEffectSpec& Spec = ExecutionParams.GetOwningSpec();
FAggregatorEvaluateParameters EvaluateParameters;
EvaluateParameters.SourceTags = Spec.CapturedSourceTags.GetAggregatedTags();
EvaluateParameters.TargetTags = Spec.CapturedTargetTags.GetAggregatedTags();
const float Damage = Spec.GetSetByCallerMagnitude(ExecutionTags::Data_Damage_Base, false, 0.f);
if (Damage <= 0.f)
{
return;
}
FGameplayTagContainer AssetTags;
Spec.GetAllAssetTags(AssetTags);
const bool bPiercing = AssetTags.HasTag(ExecutionTags::Damage_Piercing);
float Shield = 0.f;
if (!bPiercing)
{
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(Statics.ShieldDef, EvaluateParameters, Shield);
}
const float Absorbed = FMath::Clamp(Shield, 0.f, Damage);
const float Remaining = Damage - Absorbed;
if (Absorbed > 0.f)
{
OutExecutionOutput.AddOutputModifier(FGameplayModifierEvaluatedData(UAresHealthSet::GetShieldAttribute(), EGameplayModOp::Additive, -Absorbed));
}
if (Remaining > 0.f)
{
OutExecutionOutput.AddOutputModifier(FGameplayModifierEvaluatedData(UAresHealthSet::GetDamageAttribute(), EGameplayModOp::Additive, Remaining));
}
}
ShieldDef is a Target capture with snapshot turned off, declared the same way as in the first example.
What’s going on:
- The shield is read live, so if two hits land on the same frame the second one sees what the first left behind.
- We emit up to two modifiers on two different attributes, something a normal modifier or an MMC can’t do in one go. The remainder goes into the
Damagemeta attribute like in the first example, and the attribute set turns it into a Health change. - Everything we emit lands on the target, and we don’t touch the source or apply any effect. The calculation only calculates.
Shieldstill needs clamping to zero inPostGameplayEffectExecute, like any other attribute.
What about lifesteal?
Lifesteal is the classic thing people try to cram into an execution, so it’s worth saying why it doesn’t go there.
- Output modifiers only ever hit the target. An execution can’t modify the source’s attributes.
- Don’t apply Gameplay Effects from inside an execution. You’re already in the middle of applying one. Applying another from within re-enters the ability system while it’s still calculating, which is fragile, and it’s outside what the execution is meant to do. The execution’s job is to produce a number and hand back modifiers.
So do the calculation in the execution, and react to the result afterwards. The natural place is the attribute set, once the damage has actually been applied, because that’s where you know how much health was really lost (overkill doesn’t count):
void UAresHealthSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data)
{
Super::PostGameplayEffectExecute(Data);
if (Data.EvaluatedData.Attribute == GetDamageAttribute())
{
const float LocalDamage = GetDamage();
SetDamage(0.f);
if (LocalDamage > 0.f)
{
const float OldHealth = GetHealth();
SetHealth(FMath::Clamp(OldHealth - LocalDamage, 0.f, GetMaxHealth()));
const float DamageDealt = OldHealth - GetHealth();
UAbilitySystemComponent* SourceASC = Data.EffectSpec.GetContext().GetOriginalInstigatorAbilitySystemComponent();
if (DamageDealt > 0.f && SourceASC && SourceASC != &Data.Target)
{
FGameplayEventData Payload;
Payload.EventTag = AresGameplayTags::GameplayEvent_DamageDealt;
Payload.Instigator = SourceASC->GetAvatarActor();
Payload.Target = Data.Target.GetAvatarActor();
Payload.EventMagnitude = DamageDealt;
SourceASC->HandleGameplayEvent(Payload.EventTag, &Payload);
}
}
}
}
Then a passive ability on the source, triggered by that Gameplay Event (AbilityTriggers with the event tag), reads EventMagnitude, multiplies by the source’s lifesteal, and applies a normal heal Gameplay Effect to itself. Applying effects from an ability is completely normal, and that’s the right home for it. That event tag is shared between the attribute set and the ability, so it’s the one tag here that uses the non-static UE_DECLARE_GAMEPLAY_TAG_EXTERN / UE_DEFINE_GAMEPLAY_TAG pair.
Two notes on this:
HandleGameplayEventruns on the same call stack, so keep the reaction small. If you want it fully decoupled, broadcast a delegate from the attribute set instead and let something else apply the heal a moment later.- The same event is a great hook for other “when I deal damage” things: on-hit procs, combo counters, kill credit and so on, without any of them living inside the calculation.
Gotchas
The ones that have bitten me:
- Only instant and periodic effects run executions. A duration or infinite effect with no period never does. If you want a value that continuously follows attributes, use an MMC.
- They run on the server. Don’t expect your calculation to run on the client. The attribute values that come out of it replicate.
- Don’t keep state on the execution. It’s the CDO. Use locals for per-run data.
- Check that your captures worked.
AttemptCalculate...returnsfalseif the target doesn’t have that attribute set, so initialize your locals and check the return value. The source ASC can also be null. - Snapshot on means frozen, off means live. A Source snapshot is frozen when the spec is created, and a Target snapshot when the effect is applied. Live captures are re-read every time the execution runs.
- You can only change the target. Don’t apply other Gameplay Effects from inside an execution (see “What about lifesteal?”), and do your clamping in the attribute set, not here.
- Stack count is applied for you. With more than one stack, your output modifiers are scaled automatically. If you already account for it, call
MarkStackCountHandledManually().
A few smaller things: conditional effects only trigger if you call MarkConditionalGameplayEffectsToTrigger(), the effect’s Gameplay Cues fire after the executions (call MarkGameplayCuesHandledManually() if you fire them yourself), and when an effect has several executions, a later one can see the results of an earlier one if it reads live captures.
That is a wrap!
Hopefully this gives you a bit of insight into Execution Calculations and how to use them. If you have any issues, check About Me to find out how to contact me!