TutorialSeptember 12, 202614 min read

UE5 Networking: Replication, RPCs, and Multiplayer Fundamentals

Network programming in Unreal can be intimidating. This guide breaks down the core concepts so you can build multiplayer games with confidence.

The Client-Server Model

Unreal Engine uses an authoritative server model. One machine (the server) is the source of truth. All game state that matters lives on the server. Clients send inputs to the server, and the server tells clients what happened.

This model prevents cheating and keeps all players synchronized. The tradeoff is complexity—you need to think carefully about what code runs where and how data flows between machines.

Understanding Network Roles

Every actor has a network role that determines its authority:

// Check network role in C++
ENetRole Role = GetLocalRole();

if (Role == ROLE_Authority)
{
    // This is the server (or standalone)
    // We have authority over this actor
}
else if (Role == ROLE_AutonomousProxy)
{
    // This is a client controlling this actor
    // (e.g., the local player's character)
}
else if (Role == ROLE_SimulatedProxy)
{
    // This is a client seeing another player's actor
    // We just receive updates, no control
}

Understanding these roles is fundamental. Code that should only run on the server must check for authority. Code that handles local input runs on the autonomous proxy.

Replication: Syncing State

Replication is Unreal's system for synchronizing data from server to clients. When a property changes on the server, replication sends that change to connected clients automatically.

Making a Property Replicated

In your header file, mark the property with the Replicated specifier:

UPROPERTY(Replicated)
float CurrentHealth;

UPROPERTY(Replicated)
int32 AmmoCount;

UPROPERTY(Replicated)
bool bIsAlive;

Then, in your cpp file, implement GetLifetimeReplicatedProps:

void AMyCharacter::GetLifetimeReplicatedProps(
    TArray<FLifetimeProperty>& OutLifetimeProps) const
{
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);

    DOREPLIFETIME(AMyCharacter, CurrentHealth);
    DOREPLIFETIME(AMyCharacter, AmmoCount);
    DOREPLIFETIME(AMyCharacter, bIsAlive);
}

Now, whenever these properties change on the server, clients receive the new values automatically.

Conditional Replication

Not all data needs to go to all clients. You can add conditions:

// Only replicate to the owning client
DOREPLIFETIME_CONDITION(AMyCharacter, SecretInventory, COND_OwnerOnly);

// Skip the owner (they already know)
DOREPLIFETIME_CONDITION(AMyCharacter, VisualState, COND_SkipOwner);

// Only replicate initially, never update
DOREPLIFETIME_CONDITION(AMyCharacter, TeamID, COND_InitialOnly);

Conditional replication reduces bandwidth and can prevent information leaking to players who shouldn't have it.

RepNotify: Responding to Changes

Often you need to react when a replicated value changes. Use ReplicatedUsing:

// Header
UPROPERTY(ReplicatedUsing = OnRep_CurrentHealth)
float CurrentHealth;

UFUNCTION()
void OnRep_CurrentHealth();

// Implementation
void AMyCharacter::OnRep_CurrentHealth()
{
    // This runs on clients when CurrentHealth changes
    UpdateHealthBar();

    if (CurrentHealth <= 0.0f)
    {
        PlayDeathAnimation();
    }
}

RepNotify functions are called on clients after the new value arrives. They're perfect for updating UI, playing effects, or triggering animations.

RPCs: Remote Procedure Calls

Replication handles state synchronization. RPCs handle events and commands. When you need to tell another machine to do something, you use an RPC.

Server RPCs

Server RPCs are called on a client but execute on the server:

// Header
UFUNCTION(Server, Reliable)
void Server_Fire();

// Implementation
void AMyCharacter::Server_Fire_Implementation()
{
    // This code runs on the server
    // Validate the request, then do authoritative logic

    if (!CanFire())
    {
        return;
    }

    SpawnProjectile();
    ConsumeAmmo();
}

The client calls Server_Fire(). Unreal sends the request to the server, which executes Server_Fire_Implementation(). Always validate inputs in server RPCs—clients can send anything.

Client RPCs

Client RPCs go from server to a specific client:

// Header
UFUNCTION(Client, Reliable)
void Client_ShowDamageNumber(float Damage, FVector Location);

// Implementation
void AMyCharacter::Client_ShowDamageNumber_Implementation(
    float Damage, FVector Location)
{
    // This runs on the owning client only
    SpawnDamageWidget(Damage, Location);
}

The server calls Client_ShowDamageNumber(). Only the owning client receives and executes it. Use this for player-specific feedback.

Multicast RPCs

Multicast RPCs go from server to all clients:

// Header
UFUNCTION(NetMulticast, Unreliable)
void Multicast_PlayExplosionEffect(FVector Location);

// Implementation
void AMyCharacter::Multicast_PlayExplosionEffect_Implementation(
    FVector Location)
{
    // This runs on the server AND all clients
    SpawnExplosionParticles(Location);
    PlayExplosionSound(Location);
}

Use multicasts for effects everyone should see. Note that multicasts also execute on the server.

Reliable vs Unreliable

RPCs can be Reliable or Unreliable:

  • Reliable: Guaranteed to arrive, in order. Use for important events like firing, abilities, game state changes.
  • Unreliable: May be dropped if the network is congested. Use for frequent, non-critical updates like effects or animations.

Don't make everything reliable. Too many reliable RPCs can cause network congestion and stuttering.

Common Patterns

The Input Pattern

Players press buttons on clients. The server needs to know:

// Client input handler
void AMyCharacter::OnFirePressed()
{
    // Call the server RPC
    Server_Fire();

    // Optionally, do local prediction
    // (play animation immediately, server will correct if wrong)
    PlayFireAnimation();
}

// Server validates and executes
void AMyCharacter::Server_Fire_Implementation()
{
    if (!CanFire())
    {
        return;
    }

    // Authoritative logic
    AProjectile* Projectile = SpawnProjectile();
    ConsumeAmmo();

    // Tell all clients about the fire event
    Multicast_OnFired();
}

// All clients play effects
void AMyCharacter::Multicast_OnFired_Implementation()
{
    PlayFireEffects();
}

The State Pattern

State changes happen on the server and replicate to clients:

// Server-only function
void AMyCharacter::ApplyDamage(float Damage)
{
    if (!HasAuthority())
    {
        return; // Only server can apply damage
    }

    CurrentHealth -= Damage;
    // CurrentHealth is replicated, clients will receive the new value
    // OnRep_CurrentHealth will be called on clients

    if (CurrentHealth <= 0.0f)
    {
        Die();
    }
}

The Ownership Pattern

Some data should only be visible to specific players:

// Only the owning player can see their inventory
UPROPERTY(ReplicatedUsing = OnRep_Inventory)
TArray<FInventoryItem> Inventory;

void AMyCharacter::GetLifetimeReplicatedProps(
    TArray<FLifetimeProperty>& OutLifetimeProps) const
{
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);

    // Only replicate to owner
    DOREPLIFETIME_CONDITION(AMyCharacter, Inventory, COND_OwnerOnly);
}

Common Pitfalls

Pitfall 1: Running Authority Code on Clients

This is the most common networking bug:

// WRONG - runs on all machines
void AMyCharacter::TakeDamage(float Damage)
{
    CurrentHealth -= Damage; // Clients modify their own copy
}

// RIGHT - only server modifies state
void AMyCharacter::TakeDamage(float Damage)
{
    if (!HasAuthority())
    {
        return;
    }

    CurrentHealth -= Damage; // Only server modifies
}

Always ask: "Should only the server run this code?" If yes, check HasAuthority().

Pitfall 2: Trusting Client Data

Never trust data from clients:

// WRONG - trusts client damage value
void AMyCharacter::Server_DealDamage_Implementation(
    AActor* Target, float Damage)
{
    Target->TakeDamage(Damage); // Client could send Damage = 999999
}

// RIGHT - server calculates damage
void AMyCharacter::Server_RequestAttack_Implementation(
    AActor* Target)
{
    if (!IsValidTarget(Target))
    {
        return;
    }

    float Damage = CalculateDamage(); // Server calculates
    Target->TakeDamage(Damage);
}

Clients send requests, not commands. The server decides what actually happens.

Pitfall 3: Forgetting Initial Replication

When a client joins mid-game, they need current state:

// Properties replicate current values to new clients automatically
UPROPERTY(Replicated)
float CurrentHealth; // New clients get the current value

// But RPCs are not replayed!
// If you fired earlier, new clients won't see the effect

// Solution: Use replicated state for persistent things
UPROPERTY(Replicated)
bool bIsFiring; // New clients can see this

void AMyCharacter::OnRep_IsFiring()
{
    if (bIsFiring)
    {
        StartFiringEffects();
    }
    else
    {
        StopFiringEffects();
    }
}

Pitfall 4: Replicating Too Much

Every replicated property costs bandwidth:

// WRONG - replicates every tick
UPROPERTY(Replicated)
FVector ExactPosition; // Changes every frame

// RIGHT - use movement replication
// Character movement is already optimized for networking
// Only replicate additional state that changes infrequently

UPROPERTY(Replicated)
EMovementMode CurrentMode; // Changes rarely

Let built-in systems handle movement. Only replicate state that changes infrequently.

Pitfall 5: Wrong RPC Direction

Calling RPCs on the wrong machine does nothing:

// WRONG - calling Server RPC on server does nothing useful
void AMyCharacter::ServerOnlyFunction()
{
    Server_DoSomething(); // We're already on server!
}

// RIGHT - check before calling
void AMyCharacter::TryDoSomething()
{
    if (IsLocallyControlled())
    {
        Server_DoSomething(); // Client asks server
    }
    else if (HasAuthority())
    {
        DoSomething(); // Server just does it
    }
}

Blueprint Networking

All these concepts apply in Blueprints too. Use the Switch Has Authority node to check ownership. RPC functions can be marked in the function details panel.

Key settings in Blueprint:

  • Replicates checkbox on variables
  • Rep Notify dropdown for OnRep functions
  • Replication dropdown on functions (Not Replicated, Server, Client, Multicast)
  • Reliable checkbox on replicated functions

Testing Multiplayer

Use Play-In-Editor with multiple clients:

  • Set Number of Players to 2 or more
  • Enable "Run Dedicated Server" to test true client-server
  • Use the network emulation settings to simulate lag and packet loss

Always test with simulated bad network conditions. Code that works on localhost often fails on real networks.

Performance Tips

  • Net Update Frequency: Reduce how often actors send updates. Not everything needs to update 60 times per second.
  • Relevancy: Actors far from a player don't need to replicate to them. Use net relevancy settings.
  • Dormancy: Actors that aren't changing can go dormant, stopping all replication until they wake up.
  • Quantization: Reduce precision on replicated floats and vectors when full precision isn't needed.

Next Steps

Networking is deep. This guide covers fundamentals, but production games need more:

  • Client-side prediction and reconciliation
  • Lag compensation for shooting games
  • Actor replication channels and priorities
  • Custom network serialization

Start with the basics here. Get simple replication working. Then add complexity as your game requires it.

Speed Up Multiplayer Development

UnrealPilot understands UE5 networking. Ask it to generate replicated properties, create RPC stubs, or explain why your networking code isn't working. Request early access to try it.

Request beta access →