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 rarelyLet 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.