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:
// 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:
// 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.03. GameplayEffects
GameplayEffects are vessels for changing attributes and applying tags. They come in three types:
Motion Example - Sprint Effect:
// GE_Motion_SprintSpeed
Duration: Infinite
Modifiers:
- Attribute: WalkSpeed
- Operation: Additive
- Magnitude: +300
Tags Granted:
- Motion.State.Sprinting4. GameplayTags
GameplayTags are hierarchical labels that describe states and categories. They're like enhanced booleans that support parent-child relationships.
Tag Hierarchy Benefits:
Motion.Statematches all state tagsMotion.State.Sprintingis specific to sprinting- Tags can represent complex states (e.g.,
Motion.State.StaminaRegenBlockedprevents 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
- Initialization:
- Sprint State Machine with GAS:
Practical Examples
Example 1: Basic Sprint Implementation
Here's how Motion implements sprinting with GAS:
// 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:
// 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:
// 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
// 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
// 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
// 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
# Show active gameplay effects
showdebug abilitysystemCommon Issues and Solutions
| Issue | Cause | Solution |
|---|---|---|
| Attributes not changing | ASC not initialized | Ensure MotionPlayerState is set in GameMode |
| Effects not applying | Missing effect class | Check effect references in component properties |
| Tags not working | Tag not registered | Add tags to DefaultGameplayTags.ini |
| Multiplayer desync | Wrong replication mode | Use Mixed mode for player characters |
| Sprint not working | Insufficient stamina | Check stamina level and MinStaminaToSprint setting |
Visual Debugging
// 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
// 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
// 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 GAS3. Use Motion's Helper Functions
// Good: Use Motion helpers
UMotionAbilitySystemHelper::GetAbilitySystemComponentFromActor(Character);
// More complex: Manual ASC lookup
// ... lots of null checking and casting4. 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 netcommand
Next Steps
Now that you understand GAS fundamentals:
Explore Motion Components: See how each movement component uses GAS
Review Core Classes:
- MotionPlayerState - ASC ownership
- MotionAttributeSet - Movement attributes
- MotionAbilitySystemHelper - Utility functions
Try the Examples:
- Open
B_MotionCharacterto see GAS integration - Modify gameplay effects in
Content/Motion/Blueprints/GameplayEffects/ - Create custom movement states with tags
- Open
Additional Resources
Official Documentation
Motion-Specific
- Motion Architecture - Overall system design
- Quick Start Guide - Get started with Motion
Community Resources
- GAS Companion Plugin
- Unreal Slackers Discord - #gameplay-ability-system channel
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.