Dynamic Effects¶
Most of the time, Gameplay Effects are configured as data-only Blueprint assets and applied as-is. But GAS also supports creating and modifying effects at runtime — adjusting specs before application, adding dynamic tags, and in rare cases, constructing entire GEs from scratch in code.
MakeOutgoingGameplayEffectSpec¶
The standard entry point for working with GE specs at runtime. This function creates an FGameplayEffectSpec from a GE class, capturing source attributes and tags:
FGameplayEffectSpecHandle SpecHandle =
ASC->MakeOutgoingGameplayEffectSpec(MyEffectClass, AbilityLevel);
The returned FGameplayEffectSpecHandle is a shared pointer wrapper around the spec. You can modify the spec before applying it.
What Happens During Spec Creation¶
- The GE definition is read (the CDO)
- Source attributes are captured (snapshotted or linked, as configured)
- Source actor tags are captured
- The level is set
- An
FGameplayEffectContextis created (instigator, effect causer, etc.) - Modifier magnitudes are not computed yet — that happens at application time
Modifying Specs Before Application¶
After creating a spec, you can modify it before applying. This is the primary way to make effects dynamic at runtime.
SetByCaller Magnitudes¶
The most common modification. See SetByCaller for full details:
Dynamic Asset Tags¶
Add tags to the spec that weren't on the original GE definition:
SpecHandle.Data->AddDynamicAssetTag(
FGameplayTag::RequestGameplayTag("Damage.Critical"));
// Or add multiple at once
FGameplayTagContainer ExtraTags;
ExtraTags.AddTag(FGameplayTag::RequestGameplayTag("Damage.Elemental"));
ExtraTags.AddTag(FGameplayTag::RequestGameplayTag("Source.Weapon.Sword"));
SpecHandle.Data->AppendDynamicAssetTags(ExtraTags);
Dynamic asset tags are replicated and show up in immunity/query matching alongside the GE's static asset tags.
Dynamic Granted Tags¶
Add tags that will be granted to the target:
Setting the Level¶
Modifying the Duration¶
Duration Lock
Once SetDuration is called with bLockDuration = true, subsequent calls to SetDuration are ignored. This prevents accidental overrides during the application pipeline.
Effect Context¶
The effect context carries information about who created the effect and how:
FGameplayEffectContextHandle ContextHandle = SpecHandle.Data->GetEffectContext();
ContextHandle.AddHitResult(HitResult);
ContextHandle.AddActors(RelevantActors);
ContextHandle.AddOrigin(SpawnLocation);
// Or replace the entire context
FGameplayEffectContextHandle NewContext =
ASC->MakeEffectContext();
NewContext.AddInstigator(Instigator, EffectCauser);
SpecHandle.Data->SetContext(NewContext);
For passing custom data, subclass FGameplayEffectContext (a common and well-supported pattern).
Stack Count¶
Creating GEs Dynamically at Runtime¶
In rare cases, you might want to construct a UGameplayEffect entirely in code rather than using an asset. This is possible but comes with caveats.
UGameplayEffect* DynamicGE = NewObject<UGameplayEffect>(GetTransientPackage(), FName("DynamicDamageGE"));
DynamicGE->DurationPolicy = EGameplayEffectDurationType::Instant;
// Add a modifier
FGameplayModifierInfo ModifierInfo;
ModifierInfo.Attribute = UMyAttributeSet::GetHealthAttribute();
ModifierInfo.ModifierOp = EGameplayModOp::Additive;
ModifierInfo.ModifierMagnitude = FGameplayEffectModifierMagnitude(FScalableFloat(-50.f));
DynamicGE->Modifiers.Add(ModifierInfo);
// Apply it
FGameplayEffectSpecHandle SpecHandle =
ASC->MakeOutgoingGameplayEffectSpec(DynamicGE->GetClass());
ASC->ApplyGameplayEffectSpecToSelf(*SpecHandle.Data.Get());
Why You Usually Shouldn't Do This
Dynamically created GEs have several drawbacks:
- They don't benefit from editor-based configuration and iteration
- They bypass the component system (no GE Components are set up)
- They can be harder to debug (no asset to inspect)
- They may not garbage collect cleanly if you're not careful about their outer/lifetime
- They don't show up in the GE debugger the same way assets do
The overwhelmingly common pattern is: define GEs as assets, create specs at runtime, and modify specs before application. Only create GEs dynamically when you have a compelling reason (e.g., procedurally generated items with novel stat combinations that can't be expressed with SetByCaller).
The GE Container Pattern¶
For effects that logically group together — like all the stat bonuses from a piece of equipment, or all the effects of a complex ability — a common pattern is to use a container GE that applies additional effects:
GE_EquipSword (Infinite, no modifiers)
├── Component: AdditionalEffectsGEC
│ ├── OnApplication: GE_SwordDamageBonus
│ ├── OnApplication: GE_SwordSpeedBonus
│ └── OnApplication: GE_SwordPassiveAbility
└── Component: TargetTagsGEC
└── Grants: Equipment.Sword
Removing GE_EquipSword triggers the "on premature removal" path, which can clean up the sub-effects. Or better yet, design the sub-effects to have ongoing requirements tied to the Equipment.Sword tag — they auto-inhibit when the sword is unequipped.
The advantage: one handle to track, one removal call to clean everything up.
FGameplayEffectSpecHandle¶
The spec handle is a thin wrapper around TSharedPtr<FGameplayEffectSpec>:
FGameplayEffectSpecHandle SpecHandle = ASC->MakeOutgoingGameplayEffectSpec(GEClass);
// Check validity
if (SpecHandle.IsValid())
{
// Access the spec
FGameplayEffectSpec* Spec = SpecHandle.Data.Get();
// Modify it
Spec->SetSetByCallerMagnitude(Tag, Value);
// Apply it
FActiveGameplayEffectHandle ActiveHandle =
ASC->ApplyGameplayEffectSpecToTarget(*Spec, TargetASC);
}
The handle uses shared pointer semantics — the spec stays alive as long as something holds a reference to the handle. Once all handles go out of scope, the spec is destroyed (unless it was applied and is now part of an FActiveGameplayEffect).
Copying and Forwarding Specs¶
// Copy SetByCaller magnitudes from one spec to another
NewSpecHandle.Data->CopySetByCallerMagnitudes(*OriginalSpecHandle.Data);
// Merge (only adds values that don't already exist)
NewSpecHandle.Data->MergeSetByCallerMagnitudes(OriginalSpec.SetByCallerTagMagnitudes);
// Initialize as a linked spec (preserves context, strips original asset tags)
NewSpecHandle.Data->InitializeFromLinkedSpec(NewGE, *OriginalSpecHandle.Data);
Applying to Self vs. Target¶
// Apply to yourself
FActiveGameplayEffectHandle Handle =
ASC->ApplyGameplayEffectSpecToSelf(*SpecHandle.Data.Get());
// Apply to another target
FActiveGameplayEffectHandle Handle =
ASC->ApplyGameplayEffectSpecToTarget(*SpecHandle.Data.Get(), TargetASC);
Both return an FActiveGameplayEffectHandle for duration/infinite effects (or an invalid handle for instant effects, since they don't persist).
Runtime Workflow Summary¶
Here's the typical runtime flow for dynamic effects:
1. MakeOutgoingGameplayEffectSpec(GEClass, Level)
↓
2. Modify the spec:
- SetSetByCallerMagnitude()
- AddDynamicAssetTag()
- SetContext() / AddHitResult()
- SetDuration()
↓
3. ApplyGameplayEffectSpecToTarget(*Spec, TargetASC)
↓
4. (Optional) Store the ActiveGE handle for later removal
This pattern covers the vast majority of dynamic effect needs. Start here, and only reach for runtime GE construction if spec modification isn't enough.
Tips¶
Spec Reuse
You can apply the same spec to multiple targets. However, be careful — applying a spec captures target attributes, so applying to a second target after the first may use stale target captures. Create a fresh spec per target if target attributes matter.
Duplicate Context
If you need to modify the effect context after initial creation without affecting the original, call SpecHandle.Data->DuplicateEffectContext() to create a deep copy.
Dynamic Tags Are Replicated
Both DynamicAssetTags and DynamicGrantedTags are replicated as part of the spec. Changes made on the server will propagate to clients.