Why GAS Exists
Every action game needs a combat system. You could build one from scratch with custom C++ or Blueprints, but you'd eventually reinvent:
- An ability activation system with cooldowns and costs
- A stat/attribute system with modifiers
- A buff/debuff system with stacking rules
- A tag-based state management system
- Multiplayer replication for all of the above
That's exactly what the Gameplay Ability System (GAS) provides. It's Epic's production-tested framework, used in Fortnite, Paragon, and many other shipped titles. The learning curve is steep — but the payoff is a combat architecture that scales from a simple hack-and-slash to a complex MMORPG.
The Four Pillars of GAS
1. Ability System Component (ASC)
The ASC is the central hub. Every actor that uses GAS needs one. It manages:
- Which abilities the actor has
- Which effects are active
- Current attribute values
- Gameplay tag state
// In your character header
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Abilities")
UAbilitySystemComponent* AbilitySystemComponent;
Typically, the ASC lives on the PlayerState (for players) or the Character/Pawn (for AI). The PlayerState approach persists abilities across respawns.
2. Gameplay Abilities (GA)
Abilities are the actions — Fireball, Dash, Block, Heal. Each ability is a UGameplayAbility subclass with:
- Activation requirements: Tags, cooldowns, costs
- Execution logic: What happens when activated
- Cancellation rules: What can interrupt this ability
- Replication policy: How it syncs in multiplayer
UCLASS()
class UGA_Fireball : public UGameplayAbility
{
GENERATED_BODY()
public:
UGA_Fireball();
virtual bool CanActivateAbility(const FGameplayAbilitySpecHandle Handle,
const FGameplayAbilityActorInfo* ActorInfo,
const FGameplayTagContainer* SourceTags,
const FGameplayTagContainer* TargetTags,
FGameplayTagContainer* OptionalRelevantTags) const override;
virtual void ActivateAbility(const FGameplayAbilitySpecHandle Handle,
const FGameplayAbilityActorInfo* ActorInfo,
const FGameplayAbilityActivationInfo ActivationInfo,
const FGameplayEventData* TriggerEventData) override;
};
3. Gameplay Effects (GE)
Effects modify attributes and apply tags. They're data-driven (configured in Blueprints, not code):
- Instant: Apply once (heal 50 HP, deal 30 damage)
- Duration: Active for a time period (speed boost for 5 seconds)
- Infinite: Persist until removed (passive stat buff, status ailment)
Effects support:
- Modifiers: Add, multiply, or override attribute values
- Stacking: How multiple applications of the same effect combine
- Tags: Granted tags while active (e.g., "Status.Burning")
- Conditional removal: Remove when specific tags change
4. Attribute Sets
Attributes are the numerical stats — Health, Mana, Strength, Armor. They live in UAttributeSet subclasses:
UCLASS()
class UBaseAttributeSet : public UAttributeSet
{
GENERATED_BODY()
public:
UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_Health, Category = "Attributes")
FGameplayAttributeData Health;
ATTRIBUTE_ACCESSORS(UBaseAttributeSet, Health)
UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_MaxHealth, Category = "Attributes")
FGameplayAttributeData MaxHealth;
ATTRIBUTE_ACCESSORS(UBaseAttributeSet, MaxHealth)
UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_Mana, Category = "Attributes")
FGameplayAttributeData Mana;
ATTRIBUTE_ACCESSORS(UBaseAttributeSet, Mana)
// Called before an attribute is modified
virtual void PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue) override;
// Called after a gameplay effect modifies an attribute
virtual void PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data) override;
};
The ATTRIBUTE_ACCESSORS macro generates getter/setter functions. PreAttributeChange lets you clamp values (health can't go below 0 or above max).
Gameplay Tags: The Glue
Gameplay Tags are hierarchical identifiers that GAS uses for everything:
State.Dead
State.Stunned
Ability.Skill.Fireball
Ability.Skill.Dash
Status.Burning
Status.Frozen
Cooldown.Ability.Fireball
Damage.Type.Fire
Damage.Type.Physical
Tags control:
- Ability activation: "Can only use Fireball if NOT State.Stunned"
- Effect application: "Burning effect grants Status.Burning tag"
- Ability blocking: "While State.Dead, block all abilities"
- Effect immunity: "If Status.FireResistant, immune to Damage.Type.Fire"
Define your tag hierarchy in Project Settings → Gameplay Tags, or in a DefaultGameplayTags.ini data table.
Building a Basic Combat System
Let's walk through implementing a simple combat system with GAS.
Step 1: Set Up the ASC
Create a base character class with an ASC:
ABaseCharacter::ABaseCharacter()
{
AbilitySystemComponent = CreateDefaultSubobject<UAbilitySystemComponent>(TEXT("AbilitySystem"));
AbilitySystemComponent->SetIsReplicated(true);
AbilitySystemComponent->SetReplicationMode(EGameplayEffectReplicationMode::Mixed);
AttributeSet = CreateDefaultSubobject<UBaseAttributeSet>(TEXT("AttributeSet"));
}
void ABaseCharacter::BeginPlay()
{
Super::BeginPlay();
if (AbilitySystemComponent)
{
AbilitySystemComponent->InitAbilityActorInfo(this, this);
// Grant default abilities
for (TSubclassOf<UGameplayAbility>& Ability : DefaultAbilities)
{
AbilitySystemComponent->GiveAbility(FGameplayAbilitySpec(Ability, 1, INDEX_NONE, this));
}
// Apply default effects (base stats)
for (TSubclassOf<UGameplayEffect>& Effect : DefaultEffects)
{
FGameplayEffectContextHandle Context = AbilitySystemComponent->MakeEffectContext();
AbilitySystemComponent->ApplyGameplayEffectToSelf(Effect.GetDefaultObject(), 1.0f, Context);
}
}
}
Step 2: Create a Damage Ability
void UGA_MeleeAttack::ActivateAbility(...)
{
// Play attack montage
UAbilityTask_PlayMontageAndWait* MontageTask = UAbilityTask_PlayMontageAndWait::CreatePlayMontageAndWaitProxy(
this, NAME_None, AttackMontage, 1.0f);
MontageTask->OnCompleted.AddDynamic(this, &UGA_MeleeAttack::OnMontageCompleted);
MontageTask->ReadyForActivation();
// Wait for event from anim notify (hit detection)
UAbilityTask_WaitGameplayEvent* EventTask = UAbilityTask_WaitGameplayEvent::WaitGameplayEvent(
this, FGameplayTag::RequestGameplayTag("Event.Montage.Hit"));
EventTask->EventReceived.AddDynamic(this, &UGA_MeleeAttack::OnHitEvent);
EventTask->ReadyForActivation();
}
void UGA_MeleeAttack::OnHitEvent(FGameplayEventData Payload)
{
// Apply damage effect to target
if (AActor* Target = Payload.Target.Get())
{
FGameplayEffectSpecHandle DamageSpec = MakeOutgoingGameplayEffectSpec(DamageEffect, GetAbilityLevel());
ApplyGameplayEffectSpecToTarget(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, DamageSpec,
UAbilitySystemBlueprintLibrary::AbilityTargetDataFromActor(Target));
}
}
Step 3: Create a Damage Effect (in Blueprint)
Create a Gameplay Effect Blueprint:
- Duration Policy: Instant
- Modifiers: Health, Add, -30 (or use a scalable float for level-based damage)
- Tags: GameplayEffectAssetTag = "Damage.Type.Physical"
Step 4: Handle Death
In your Attribute Set:
void UBaseAttributeSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data)
{
if (Data.EvaluatedData.Attribute == GetHealthAttribute())
{
float NewHealth = FMath::Clamp(GetHealth(), 0.0f, GetMaxHealth());
SetHealth(NewHealth);
if (NewHealth <= 0.0f)
{
// Broadcast death event
FGameplayEventData EventData;
UAbilitySystemBlueprintLibrary::SendGameplayEventToActor(
Data.Target.GetOwnerActor(),
FGameplayTag::RequestGameplayTag("Event.Death"),
EventData);
}
}
}
Common GAS Patterns
Cooldowns
Apply a Gameplay Effect with a Duration that grants a cooldown tag:
- Effect Duration: 3 seconds
- Granted Tag: Cooldown.Ability.Fireball
- Ability's Cooldown Tag: Cooldown.Ability.Fireball (checked before activation)
Mana/Resource Costs
Create a "cost" Gameplay Effect that's applied when the ability activates:
- Modifier: Mana, Add, -25
- Set as the ability's Cost Gameplay Effect
GAS automatically checks if the actor has enough of the resource before allowing activation.
Buff Stacking
Configure stacking rules on the Gameplay Effect:
- Aggregate by Source: Each caster's buff stacks independently
- Aggregate by Target: Only the strongest buff applies
- Stack Limit: Maximum number of stacks (e.g., 5 stacks of Poison)
- Stack Duration Refresh: Whether new stacks reset the timer
Passive Abilities
Abilities that activate once and persist:
- Activation Policy: On Granted (activates when given to the actor)
- Never ends (no EndAbility call)
- Can listen for events and respond continuously
Multiplayer Considerations
GAS was designed for multiplayer from the start:
- ASC Replication Mode:
Mixedfor player characters (predict locally, replicate results),Minimalfor AI - Ability Prediction: Client predicts ability activation, server confirms or rolls back
- Effect Replication: Effects replicate automatically based on configuration
- Attribute Replication: Use
DOREPLIFETIMEandOnRep_functions for smooth sync
The key multiplayer rule: the server is authoritative for all gameplay effects and attribute changes. Clients predict for responsiveness, but the server's calculation is always the final truth.
Common Mistakes
Putting logic in the Attribute Set that belongs in abilities: The Attribute Set should only clamp values and broadcast events. Ability logic (spawning projectiles, playing effects) belongs in abilities.
Too many Gameplay Tags: Start with a minimal tag hierarchy and expand as needed. Over-engineering tags early leads to a confusing, hard-to-maintain system.
Ignoring Ability Tasks: Ability Tasks are GAS's async primitives. Use them for montages, waits, and event listening instead of timers or tick functions.
Not using the Gameplay Debugger: Press ' (apostrophe) in PIE to open the Gameplay Debugger. It shows active abilities, effects, attributes, and tags in real-time. Essential for debugging GAS issues.
GAS has a reputation for being complex, and it is — but it's complex because combat systems are complex. The framework handles the hard problems (networking, prediction, state management) so you can focus on the fun parts: designing abilities, tuning balance, and creating engaging combat.