Skip to content

Networking Model

A comprehensive guide to Motion's multiplayer networking architecture, covering replication, prediction, and state synchronization for networked character movement.

Prerequisites

This guide assumes basic familiarity with Unreal Engine's gameplay framework. If you're new to GAS, read the GAS Primer first.

Introduction

Motion is built from the ground up with multiplayer support, leveraging Unreal Engine's robust networking system and the Gameplay Ability System (GAS) for state management. Every movement component in Motion is network-aware, providing smooth, responsive character control in both single-player and multiplayer environments.

Key Networking Features

  • Server-Authoritative Movement: All movement decisions are validated by the server
  • Client Prediction: Responsive controls with predictive movement
  • GAS Integration: Leverages GAS's built-in replication for attributes and effects
  • Minimal Network Overhead: Only essential data is replicated
  • Universal Compatibility: Works with any networked ACharacter

Core Networking Concepts

Server-Client Architecture

Unreal Engine uses a server-client model where the server has ultimate authority over the game state:

Key Principles:

  • Server validates all gameplay decisions
  • Clients predict locally for responsiveness
  • Server corrects client predictions when needed
  • "Never trust the client" - validate everything server-side

Network Roles

Every actor in a networked game has a role that determines its behavior:

RoleDescriptionMotion Example
AuthorityServer version of the actorServer's character instance
Autonomous ProxyLocally controlled client actorYour character on your client
Simulated ProxyRemote client actorOther players on your client
NoneNot networkedUI elements, local effects

Connection Flow

Understanding how players connect and initialize is crucial for Motion's networking:

Very Important

Returning false in a validation function like ServerRequestSprint_Validate will disconnect the client! In PIE this will look like your character is just resetting / respawning, but in a Release build this will kick them out!

Motion's Network Architecture

Component-Based Design

Motion uses a modular component architecture that integrates seamlessly with Unreal's networking:

PlayerState Ownership Pattern

Motion uses MotionPlayerState to own the AbilitySystemComponent, ensuring persistence across respawns:

cpp
// MotionPlayerState.cpp
AMotionPlayerState::AMotionPlayerState(const FObjectInitializer& ObjectInitializer)
{
    // Enable replication
    bReplicates = true;
    
    // Create and configure ASC
    AbilitySystemComponent = CreateDefaultSubobject<UAbilitySystemComponent>(TEXT("ASC"));
    AbilitySystemComponent->SetIsReplicated(true);
    
    // Mixed mode: Server authority with client prediction
    AbilitySystemComponent->SetReplicationMode(EGameplayEffectReplicationMode::Mixed);
}

Why PlayerState?

  • Persists across character respawns
  • Maintains attributes between deaths
  • Simplifies ownership for networking
  • Standard pattern for GAS games

GAS Replication Modes

Motion uses Mixed Replication Mode for optimal performance:

ModeGameplayEffectsGameplayCuesUse Case
FullReplicate to allReplicate to allSingle player
MixedReplicate to owner onlyReplicate to allMotion's choice ✓
MinimalDon't replicateReplicate to allAI/NPCs

Replication in Motion

What Gets Replicated

Motion carefully manages what data is replicated to minimize bandwidth:

Attribute Replication

Motion's attributes replicate automatically through GAS:

cpp
// MotionAttributeSet.cpp
void UMotionAttributeSet::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);
    
    // Replicate movement attributes
	DOREPLIFETIME_CONDITION_NOTIFY(UMotionAttributeSet, WalkSpeed, COND_None, REPNOTIFY_Always);
	DOREPLIFETIME_CONDITION_NOTIFY(UMotionAttributeSet, SprintSpeed, COND_None, REPNOTIFY_Always);
	DOREPLIFETIME_CONDITION_NOTIFY(UMotionAttributeSet, JumpVelocity, COND_None, REPNOTIFY_Always);

	// Replicate stamina attributes
	DOREPLIFETIME_CONDITION_NOTIFY(UMotionAttributeSet, Stamina, COND_None, REPNOTIFY_Always);
	DOREPLIFETIME_CONDITION_NOTIFY(UMotionAttributeSet, MaxStamina, COND_None, REPNOTIFY_Always);
	DOREPLIFETIME_CONDITION_NOTIFY(UMotionAttributeSet, StaminaRegenRate, COND_None, REPNOTIFY_Always);
	DOREPLIFETIME_CONDITION_NOTIFY(UMotionAttributeSet, StaminaDrainRate, COND_None, REPNOTIFY_Always);
}

Tag-Based State Synchronization

Motion uses GameplayTags for state synchronization across the network:

Client-Server Communication

Input Flow Diagram

Motion implements client prediction for responsive movement:

Benefits:

  • Zero perceived latency for local player
  • Smooth movement even with high ping
  • Server can still correct if needed

Movement State Networking

Crouch Network Handling

Crouch uses a dual-layer system combining client prediction with server authority:

cpp
void UMotionCrouchingComponent::HandleCrouchInputStateChange(bool bPressed)
{
    if (!CachedCharacter->IsLocallyControlled())
        return;

    if (bPressed)
    {
        if (CachedCharacter->HasAuthority())
        {
            // Server: Direct authoritative state change
            SetWantsToCrouch(true);
        }
        else
        {
            // Client: Apply loose tag for immediate feedback
            UMotionAbilitySystemHelper::AddGameplayTagToActor(
                CachedCharacter, MotionGameplayTags::Motion_State_WantsToCrouch);
            
            // Send RPC to server
            ServerRequestCrouch(true);
        }
    }
}

void UMotionCrouchingComponent::UpdateCrouchingState(float DeltaTime)
{
    // Local client triggers built-in crouch system
    if (CachedCharacter->IsLocallyControlled())
    {
        if (bWantsToCrouch && !bIsCurrentlyCrouching)
        {
            // Character->Crouch() triggers automatic server RPC
            CachedCharacter->Crouch();
        }
        else if (!bWantsToCrouch && bIsCurrentlyCrouching)
        {
            // Character->UnCrouch() triggers automatic server RPC
            CachedCharacter->UnCrouch();
        }
    }
}

Key Features:

  • Client prediction via loose tags for zero-latency feedback
  • Server validation with rate limiting and collision checks
  • Built-in CharacterMovementComponent handles capsule changes
  • Camera height interpolation managed separately by MotionCameraComponent

Jump Prediction

Jump combines client prediction with custom enhancements for advanced features:

cpp
void UMotionJumpComponent::HandleJumpInputStateChange(bool bPressed)
{
    if (!CachedCharacter->IsLocallyControlled())
        return;

    if (bPressed)
    {
        if (CachedCharacter->HasAuthority())
        {
            // Server: Direct authoritative state change
            SetWantsToJump(true);
        }
        else
        {
            // Client: Apply loose tag for immediate feedback
            UMotionAbilitySystemHelper::AddGameplayTagToActor(
                CachedCharacter, MotionGameplayTags::Motion_State_WantsToJump);
            
            // Send RPC to server
            ServerRequestJump(true);
        }
    }
}

bool UMotionJumpComponent::PerformJump(bool bIsAdditionalJump)
{
    // Custom jump counter avoids Unreal's synchronization bugs
    if (IsInCoyoteTime() && !bUsedCoyoteJump)
    {
        bUsedCoyoteJump = true;
        MotionJumpCount = 1;  // First jump from coyote time
    }
    else if (!IsInCoyoteTime())
    {
        MotionJumpCount++;  // Air jump increment
    }
    
    // Sync Unreal's counter before jumping
    CachedCharacter->JumpCurrentCount = MotionJumpCount - 1;
    
    // Apply custom velocity modifications
    ApplyDirectJumpVelocity(bIsAdditionalJump);
    
    if (bIsAdditionalJump && !IsInCoyoteTime())
    {
        // Directional air jumps with custom velocity
        PerformDirectionalJump();
    }
    else
    {
        // Standard ground/coyote jump - uses built-in prediction
        CachedCharacter->Jump();
    }
    
    return true;
}

Advanced Features:

  • Coyote Time: Grace period for jumping after leaving ground
  • Jump Buffering: Queue jump input before landing
  • Multi-Jump: Air jumps with velocity multipliers
  • Custom Counter: Reliable jump tracking avoiding Unreal's bugs
  • Directional Jumps: Air jumps respect movement input

Stamina Management

Stamina synchronization for sprint limitations:

Testing Multiplayer

Local Testing Setup

Motion supports easy local multiplayer testing:

  1. PIE Multiplayer Settings:
- Test with multiple players: `Play` -> `Number of Players`: 2
- Net Mode: Play As Client
- Use Single Process: Unchecked (for true networking)
  1. Window Arrangement:
Advanced Settings -> Multiplayer Options
- Editor Multiplayer Mode: Play multiple clients
- Create Audio Device for Every Player: Disabled (Default)

Network Simulation

Test with simulated network conditions:

bash
# Console commands for network simulation
NetEmulation.PktLag=100        # 100ms latency
NetEmulation.PktLoss=2         # 2% packet loss
NetEmulation.PktDup=1          # 1% duplicate packets

Alternatively, enable Network Emulation in Editor Preferences:

Debugging Network Issues

Debug Commands

Essential console commands for network debugging:

bash
# Show network debug info
showdebug net
showdebug abilitysystem
stat net
stat game

# Network details
p.NetShowCorrections 1        # Show prediction corrections
net.EnableNetStats 1         # Show connection status

# GAS debugging
showdebug abilitysystem      # Show ability system state
log LogMotionCore Verbose    # Motion-specific logging
log LogAbilitySystem Verbose # GAS logging

# Performance monitoring
stat fps                     # Frame rate
stat unit                    # Frame time breakdown
stat rhi                     # Rendering stats

Common Network Issues

IssueSymptomsSolution
Rubber-bandingCharacter snaps backCheck server validation logic, increase tolerance
Stamina DesyncDifferent stamina valuesEnsure GAS effects replicate properly
Missing MovementOthers don't see movementCheck actor replication settings
Tag MismatchStates out of syncVerify tag replication, check authority

Network Log Analysis

Key log patterns to watch for:

cpp
// Motion network logging
UE_LOG(LogMotionCore, Verbose, TEXT("Sprint RPC: Client->Server"));
UE_LOG(LogMotionCore, Warning, TEXT("Sprint validation failed"));

// GAS replication logging  
UE_LOG(LogAbilitySystem, Verbose, TEXT("Effect applied: %s"), *EffectName);
UE_LOG(LogAbilitySystem, Warning, TEXT("Prediction miss: %s"), *AttributeName);

// Movement logging
UE_LOG(LogCharacterMovement, Verbose, TEXT("Correction delta: %f"), Delta);

Best Practices

Do's ✅

  1. Always validate on server:
cpp
if (HasAuthority())
{
    // Make authoritative decisions here
}
  1. Use GAS for state management:
cpp
// Let GAS handle replication
ApplyGameplayEffectToSelf(SprintEffect);
  1. Implement client prediction:
cpp
// Predict locally, validate on server
PredictMovement();
ServerValidateMovement();
  1. Use Motion's helpers:
cpp
UMotionAbilitySystemHelper::GetAbilitySystemComponentFromActor(Actor);

Don'ts ❌

  1. Don't trust client input:
cpp
// Bad: Direct application
void OnSprintInput() 
{
    StartSprinting(); // No validation!
}

// Good: Server validation
void OnSprintInput()
{
    ServerRequestSprint(true);
}
  1. Don't replicate everything:
cpp
// Bad: Replicate visual-only properties
UPROPERTY(Replicated)
float CameraShakeIntensity;

// Good: Keep visual properties local
UPROPERTY()
float CameraShakeIntensity;
  1. Don't ignore network roles:
cpp
// Bad: Assume authority
ApplyDamage(Target, Amount);

// Good: Check authority
if (HasAuthority())
{
    ApplyDamage(Target, Amount);
}
  1. Don't use reliable RPCs on Tick:
cpp
// Bad: Flooding network
void Tick(float DeltaTime)
{
    ServerUpdatePosition(GetActorLocation()); // Reliable RPC
}

// Good: Use replication
UPROPERTY(ReplicatedUsing=OnRep_Position)
FVector ReplicatedPosition;

Advanced Topics

Custom Replication

Implementing custom replication for Motion components:

cpp
void UMotionCustomComponent::GetLifetimeReplicatedProps(
    TArray<FLifetimeProperty>& OutLifetimeProps) const
{
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);
    
    // Conditional replication
    DOREPLIFETIME_CONDITION(UMotionCustomComponent, CustomProperty, 
                            COND_OwnerOnly);
    
    // Custom replication with callback
    DOREPLIFETIME_CONDITION_NOTIFY(UMotionCustomComponent, ImportantValue,
                                   COND_None, REPNOTIFY_Always);
}

void UMotionCustomComponent::OnRep_ImportantValue()
{
    // Handle replication callback
    OnValueReplicated.Broadcast(ImportantValue);
}

Summary

Motion's networking model provides:

  • Robust multiplayer support through server-authoritative architecture
  • Smooth gameplay via client prediction and interpolation
  • Efficient bandwidth usage with selective replication
  • Easy debugging with comprehensive logging and debug commands
  • Scalability from small co-op to larger multiplayer games

By understanding these networking concepts and Motion's implementation, you can create responsive, fair, and scalable multiplayer experiences.

Next Steps

  • Review the GAS Primer for attribute and effect replication details
  • Test your implementation with the Quick Start Guide
  • Explore individual components for specific networking features
  • Join the Discord community for multiplayer testing partners

Additional Resources

Unreal Documentation

Community Resources


Pro Tip

Start with local multiplayer testing using PIE before moving to dedicated servers. This allows rapid iteration and easier debugging of networking issues.

Motion - Advanced First Person Character Controller