Skip to content

GAS Primer for Motion

A beginner's guide to understanding the Gameplay Ability System (GAS) and how Motion leverages it for advanced character movement.

Why This Guide?

Motion uses Unreal's Gameplay Ability System for state management, attribute handling, and movement mechanics. This primer explains the foundational GAS concepts you need to understand Motion's architecture.

What is the Gameplay Ability System?

The Gameplay Ability System (GAS) is Unreal Engine's framework for creating abilities, attributes, and gameplay effects. Think of it as a battle-tested system for managing character states, stats, and actions in a networked environment.

Why Motion Uses GAS

Motion leverages GAS for several key reasons:

  • Predictive Networking: GAS handles client prediction for smooth multiplayer movement
  • State Management: GameplayTags provide a robust way to track movement states (sprinting, crouching, etc.)
  • Attribute System: Built-in support for modifying and tracking movement properties
  • Modular Effects: GameplayEffects allow for stackable, temporary movement modifiers
  • Industry Standard: GAS is used by Fortnite, Paragon, and many AAA titles

Core Concepts

The Big Picture

Before diving into details, here's how GAS components work together:

1. AbilitySystemComponent (ASC)

The ASC is the heart of GAS - a component that manages everything related to abilities and attributes.

What it does:

  • Stores and manages character attributes (health, speed, stamina)
  • Applies and tracks active effects
  • Grants and activates abilities
  • Manages gameplay tags
  • Handles replication for multiplayer

In Motion: The ASC lives on MotionPlayerState to persist across respawns:

cpp
// MotionPlayerState owns the ASC
class AMotionPlayerState : public APlayerState
{
    UPROPERTY()
    UAbilitySystemComponent* AbilitySystemComponent;
    
    UPROPERTY()
    UMotionAttributeSet* AttributeSet;
};

2. Attributes and AttributeSets

Attributes are numerical values representing character properties. They have two key values:

BaseValue: The permanent, underlying value CurrentValue: BaseValue + all temporary modifiers

Motion's Attributes:

cpp
// MotionAttributeSet defines movement attributes
UPROPERTY(BlueprintReadOnly, Category = "Movement")
FGameplayAttributeData WalkSpeed;      // Default: 600

UPROPERTY(BlueprintReadOnly, Category = "Movement")  
FGameplayAttributeData SprintSpeed;    // Default: 300 (additive modifier)

UPROPERTY(BlueprintReadOnly, Category = "Movement")
FGameplayAttributeData JumpVelocity;   // Default: 420

UPROPERTY(BlueprintReadOnly, Category = "Stamina")
FGameplayAttributeData Stamina;        // Default: 5.0
FGameplayAttributeData MaxStamina;     // Default: 5.0

3. GameplayEffects

GameplayEffects are vessels for changing attributes and applying tags. They come in three types:

Motion Example - Sprint Effect:

cpp
// GE_Motion_SprintSpeed
Duration: Infinite
Modifiers:
  - Attribute: WalkSpeed
  - Operation: Additive
  - Magnitude: +300
Tags Granted:
  - Motion.State.Sprinting

4. GameplayTags

GameplayTags are hierarchical labels that describe states and categories. They're like enhanced booleans that support parent-child relationships.

Tag Hierarchy Benefits:

  • Motion.State matches all state tags
  • Motion.State.Sprinting is specific to sprinting
  • Tags can represent complex states (e.g., Motion.State.StaminaRegenBlocked prevents regeneration)

5. GameplayAbilities

While Motion doesn't use traditional GameplayAbilities (it uses components instead), understanding them helps grasp the full GAS picture:

How Motion Uses GAS

Motion takes a unique approach by using components instead of traditional abilities, but still leverages GAS for state and attribute management:

Motion's GAS Flow

  1. Initialization:
  1. Sprint State Machine with GAS:

Practical Examples

Example 1: Basic Sprint Implementation

Here's how Motion implements sprinting with GAS:

cpp
// 1. Check if we can start sprinting
bool UMotionSprintingComponent::CanStartSprinting() const
{
    if (!bUseStaminaSystem)
    {
        return true;
    }
    
    // Get stamina from GAS for accurate checking
    const float CurrentGASStamina = GetStaminaFromGAS();
    
    // Direct stamina check - can only sprint if stamina is sufficient
    return CurrentGASStamina >= MinStaminaToSprint;
}

// 2. Apply sprint effect when starting
void UMotionSprintingComponent::ApplySprintSpeedModification()
{
    if (SprintSpeedEffect)
    {
        FGameplayEffectContextHandle EffectContext = CachedAbilitySystemComponent->MakeEffectContext();
        
        const FGameplayEffectSpecHandle EffectSpecHandle = CachedAbilitySystemComponent->MakeOutgoingSpec(
            SprintSpeedEffect, 1.0f, EffectContext
        );
        
        // Set the magnitude using SetByCaller for dynamic speed values
        EffectSpecHandle.Data.Get()->SetSetByCallerMagnitude(
            MotionGameplayTags::SetByCaller_Magnitude_WalkSpeedModifier, 
            SprintWalkSpeedModifier);
        
        ActiveSprintSpeedEffectHandle = CachedAbilitySystemComponent->ApplyGameplayEffectSpecToSelf(*EffectSpecHandle.Data.Get());
    }
}

// 3. The effect modifies movement speed
// GE_Motion_SprintSpeed configuration:
{
    "Duration": "Infinite",
    "Modifiers": [{
        "Attribute": "MotionAttributeSet.WalkSpeed",
        "ModifierOp": "Additive",
        "ModifierMagnitude": "SetByCaller.Magnitude.WalkSpeedModifier"
    }],
    "GrantedTags": ["Motion.State.Sprinting"]
}

Example 2: Stamina System

Motion's stamina system demonstrates attribute modification over time:

cpp
// Stamina drain while sprinting
void UMotionSprintingComponent::ApplyStaminaDrain()
{
    if (!StaminaDrainEffect) return;
    
    // Create effect that reduces stamina over time
    FGameplayEffectSpecHandle DrainSpec = ASC->MakeOutgoingSpec(
        StaminaDrainEffect, 1, ASC->MakeEffectContext());
    
    // This effect continuously reduces Stamina attribute
    ActiveDrainHandle = ASC->ApplyGameplayEffectSpecToSelf(*DrainSpec.Data.Get());
}

// GE_Motion_StaminaDrain configuration:
{
    "Duration": "Infinite",
    "Period": 0.1,  // Tick every 100ms
    "Modifiers": [{
        "Attribute": "MotionAttributeSet.Stamina",
        "ModifierOp": "Additive",
        "ModifierMagnitude": -2  // Lose 2 stamina per tick
    }]
}

// Listen for stamina depletion
void UMotionSprintingComponent::OnStaminaChanged(const FOnAttributeChangeData& Data)
{
    if (Data.NewValue <= 0)
    {
        // Force stop sprinting when exhausted
        StopSprinting();
        
        // Apply exhaustion effect (blocks sprint for X seconds)
        ApplyExhaustionEffect();
    }
}

Example 3: Input Binding with GAS

Motion provides helpers to bind Enhanced Input to GAS:

cpp
// In your character setup
void AMyCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
    UEnhancedInputComponent* EnhancedInput = Cast<UEnhancedInputComponent>(PlayerInputComponent);
    
    // Create input action for sprinting
    UInputAction* SprintAction = LoadObject<UInputAction>(nullptr, 
        TEXT("/MotionCore/Input/IA_Sprint"));
    
    // Bind the input to the sprint component
    if (MotionSprintComponent)
    {
        EnhancedInput->BindAction(SprintAction, ETriggerEvent::Started, 
            MotionSprintComponent, &UMotionSprintingComponent::OnSprintInputStarted);
        
        EnhancedInput->BindAction(SprintAction, ETriggerEvent::Completed, 
            MotionSprintComponent, &UMotionSprintingComponent::OnSprintInputStopped);
    }
}

Common Patterns

Pattern 1: State Checking with Tags

cpp
// Check if character is in a specific state
bool IsCharacterSprinting()
{
    return UMotionAbilitySystemHelper::ActorHasGameplayTag(Character,
        MotionGameplayTags::Motion_State_Sprinting);
}

// Check stamina depletion state
bool IsStaminaDepleted()
{
    return UMotionAbilitySystemHelper::ActorHasGameplayTag(Character,
        MotionGameplayTags::Motion_State_StaminaDepleted);
}

Pattern 2: Temporary Attribute Modification

cpp
// Apply a temporary speed boost
void ApplySpeedBoost(float Duration, float SpeedIncrease)
{
    UGameplayEffect* TempEffect = NewObject<UGameplayEffect>();
    TempEffect->DurationPolicy = EGameplayEffectDurationType::HasDuration;
    TempEffect->DurationMagnitude = FScalableFloat(Duration);
    
    // Add speed modifier
    FGameplayModifierInfo SpeedMod;
    SpeedMod.Attribute = UMotionAttributeSet::GetWalkSpeedAttribute();
    SpeedMod.ModifierOp = EGameplayModOp::Additive;
    SpeedMod.ModifierMagnitude = FScalableFloat(SpeedIncrease);
    
    TempEffect->Modifiers.Add(SpeedMod);
    
    // Apply the effect
    ASC->ApplyGameplayEffectToSelf(TempEffect, 1.0f, ASC->MakeEffectContext());
}

Pattern 3: Reacting to Attribute Changes

cpp
// Subscribe to attribute changes
void UMotionSprintingComponent::BeginPlay()
{
    Super::BeginPlay();
    
    if (ASC)
    {
        // Listen for stamina changes
        ASC->GetGameplayAttributeValueChangeDelegate(
            UMotionAttributeSet::GetStaminaAttribute()
        ).AddUObject(this, &UMotionSprintingComponent::OnStaminaChanged);
    }
}

void UMotionSprintingComponent::OnStaminaChanged(const FOnAttributeChangeData& Data)
{
    float StaminaPercent = Data.NewValue / GetMaxStamina();
    
    // Update UI or trigger events based on stamina
    OnStaminaUpdated.Broadcast(StaminaPercent);
    
    if (StaminaPercent <= 0.2f)
    {
        // Low stamina warning
        PlayLowStaminaEffects();
    }
}

Debugging GAS in Motion

Console Commands

bash
# Show active gameplay effects
showdebug abilitysystem

Common Issues and Solutions

IssueCauseSolution
Attributes not changingASC not initializedEnsure MotionPlayerState is set in GameMode
Effects not applyingMissing effect classCheck effect references in component properties
Tags not workingTag not registeredAdd tags to DefaultGameplayTags.ini
Multiplayer desyncWrong replication modeUse Mixed mode for player characters
Sprint not workingInsufficient staminaCheck stamina level and MinStaminaToSprint setting

Visual Debugging

cpp
// Draw debug info on screen
void DebugDrawGAS()
{
    if (GEngine && ASC)
    {
        // Show current speed
        float CurrentSpeed = ASC->GetNumericAttribute(
            UMotionAttributeSet::GetWalkSpeedAttribute());
        GEngine->AddOnScreenDebugMessage(-1, 0.0f, FColor::Green, 
            FString::Printf(TEXT("Speed: %.0f"), CurrentSpeed));
        
        // Show active tags
        FGameplayTagContainer ActiveTags;
        ASC->GetOwnedGameplayTags(ActiveTags);
        GEngine->AddOnScreenDebugMessage(-1, 0.0f, FColor::Yellow,
            FString::Printf(TEXT("Tags: %s"), *ActiveTags.ToString()));
    }
}

Best Practices for Motion + GAS

1. Use Tags for State Management

cpp
// Good: Use Motion helpers for tag checking
if (UMotionAbilitySystemHelper::ActorHasGameplayTag(Character, MotionGameplayTags::Motion_State_Sprinting))
{
    // Character is sprinting
}

// Avoid: Manual boolean tracking
// Exception: Client-side prediction to avoid delayed response
if (bIsSprinting)  // Can desync with GAS
{
    // Less reliable, but sometimes needed for responsiveness
}

2. Let GAS Handle Attribute Changes

cpp
// Good: Apply effect to change speed using Motion patterns
if (SprintSpeedEffect)
{
    FGameplayEffectSpecHandle SpecHandle = ASC->MakeOutgoingSpec(
        SprintSpeedEffect, 1.0f, ASC->MakeEffectContext());
    SpecHandle.Data.Get()->SetSetByCallerMagnitude(
        MotionGameplayTags::SetByCaller_Magnitude_WalkSpeedModifier, 
        SprintWalkSpeedModifier);
    ASC->ApplyGameplayEffectSpecToSelf(*SpecHandle.Data.Get());
}

// Avoid: Direct modification
CharacterMovement->MaxWalkSpeed = 900;  // Bypasses GAS

3. Use Motion's Helper Functions

cpp
// Good: Use Motion helpers
UMotionAbilitySystemHelper::GetAbilitySystemComponentFromActor(Character);

// More complex: Manual ASC lookup
// ... lots of null checking and casting

4. Configure Effects in Blueprint/Data

  • Define effects as Blueprint assets for easy tweaking
  • Use data tables for attribute initialization
  • Keep magic numbers out of C++ code

5. Profile Network Traffic

  • Use Mixed replication mode for players
  • Minimal mode for AI characters
  • Monitor with stat net command

Next Steps

Now that you understand GAS fundamentals:

  1. Explore Motion Components: See how each movement component uses GAS

  2. Review Core Classes:

  3. Try the Examples:

    • Open B_MotionCharacter to see GAS integration
    • Modify gameplay effects in Content/Motion/Blueprints/GameplayEffects/
    • Create custom movement states with tags

Additional Resources

Official Documentation

Motion-Specific

Community Resources


Remember

GAS might seem complex at first, but Motion abstracts much of the complexity. Start with the pre-made components, understand how they use GAS, then customize as needed. The system's power becomes apparent as you build more complex movement mechanics.

Motion - Advanced First Person Character Controller