Why an Actor is really several copies kept in sync, authority and roles, who lives where, replicated variables and RPCs, reliability, prediction, lag compensation, and the gotchas that fail silently.
An Actor is not one thing. It is a set of copies of the same thing, living on different machines, that Unreal works to keep looking similar. In a multiplayer match there is no single Character in the world — there is one copy on the server and one on each client that can currently see it. Same class, same code, each with its own separate copy of every variable.
One copy is the authority (normally the server's). It decides things. The others are proxies: they can run code, play animations, and spawn effects, but anything they decide gets overwritten by the next update from the server.
| Server | Client | |
|---|---|---|
| Authority | Owns game state — health, position, score | Owns local input, cosmetics, UI |
| Trust | Always trusted | Never trusted for game state |
| Actors | Has all actors | Only the relevant ones |
| Role | Meaning |
|---|---|
ROLE_Authority | I am the real one. My values are the truth. |
ROLE_AutonomousProxy | A client's copy of something that client controls (your own pawn). |
ROLE_SimulatedProxy | A client's copy of something someone else controls. |
One Character in a session with a server and two clients has all three roles at once: the server's copy is Authority, the owning player's copy is AutonomousProxy, the other player's copy is SimulatedProxy. Same graph, three behaviors.
Has Authority (BP: Switch Has Authority) asks "am I the truth?" Gates state changes. True on the server and in standalone — the most important check by far.Is Locally Controlled asks "is a human at this machine controlling this pawn?" Gates that player's HUD, camera shake, screen effects.All four combinations occur. A listen server host's pawn is both. A dedicated server's copy is authority but not locally controlled. A client's own pawn is locally controlled but not authority. A client's copy of someone else's pawn is neither.
if (HasAuthority()) { Health -= FinalDamage; CheckForDeath(); }
if (IsLocallyControlled()) { PlayHitVignette(); UpdateHealthWidget(); }
The common pattern is branching on Has Authority in BeginPlay, to split server-only setup from client-only setup.
If an Actor exists on five machines, BeginPlay fires five times and Tick runs on all five, independently and unsynchronized. So Event BeginPlay → Spawn Actor on a server plus four clients runs five times. This is why "spawn on the server only" is a standing rule.
Three things decide whether an actor exists on a given machine:
Replicates flag. Off means the actor is local to whichever machine spawned it.Exception: input events only fire on the machine where a human is pressing the button. Those do not multiply.
| Class | Lives on |
|---|---|
| GameMode | Server only. Clients do not have one at all. |
| GameState | Server plus every client. |
| PlayerState | Server plus every client. One per player, everyone sees everyone's. |
| PlayerController | Server plus ONLY the owning client. |
| Pawn / Character | Server plus every client where relevant. |
| HUD / UMG Widgets | Owning client only. |
| GameInstance | Every machine, one each, never replicated. |
GetGameMode() returns null because the class does not exist there.
The compressed version: GameMode holds the rules. GameState holds the facts those rules produce. PlayerState holds anything that has to survive the pawn dying.
Practical consequence: to display a teammate's ammo or score you cannot read their PlayerController, because you do not have it. It has to live on their PlayerState.
This is the fact underneath the AI Manager architecture in Chapter 5: since clients never run the AI, the manager is pure server-authoritative logic, and only its results need to reach clients.
Blueprint: the variable's Replication dropdown (None / Replicated / RepNotify). C++, both pieces required:
UPROPERTY(ReplicatedUsing = OnRep_Health)
float Health;
void AMyCharacter::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& Out) const
{
Super::GetLifetimeReplicatedProps(Out);
DOREPLIFETIME(AMyCharacter, Health);
}
GetLifetimeReplicatedProps is the most common "why is this not replicating" cause, and it fails silently.
Server to client, never the reverse. If a client sets a replicated variable, the change happens locally, nothing is sent, and the next server update silently overwrites it. No error. Works perfectly on a listen server and fails only on real clients, which is what makes it so hard to catch.
The server sends the current value when it differs from what it last sent. Intermediate values can be skipped: 100 → 80 → 60 in one frame will likely arrive as 100 → 60. So variables are for "what is true right now," RPCs are for "this moment happened." Watching a health variable to detect hits will miss hits.
OnRep_ runs on clients when the value arrives. Cosmetics go here: health bars, shield break effects, ragdoll on death.
OnRep_ does not fire on the server, so on a listen server the host's own UI will not update. Standard fix is for the server to call it manually:
void AMyCharacter::SetHealth(float NewHealth)
{
Health = NewHealth;
OnRep_Health(); // clients get it via replication
}
With DOREPLIFETIME_CONDITION(..., COND_OwnerOnly). Ammo counts, damage numbers, and private state should be owner-only. Bandwidth saving plus a small anti-cheat win, since data you never send cannot be read out of memory.
Variables move state down. RPCs move requests and events around. An RPC is called on one machine and executes on a different one.
| Type | Direction |
|---|---|
| Server | A client asks the server to run something |
| Client | The server tells the owning client to run something |
| NetMulticast | The server tells all clients to run something |
UFUNCTION(Server, Reliable, WithValidation)
void Server_RequestFire(FVector_NetQuantize AimPoint, float ClientTimestamp);
Server RPCs require ownership. The calling client must own the actor, via a chain leading back to that client's PlayerController. Wrong ownership means the call is silently dropped. The _Validate function is the anti-cheat gate; returning false kicks the player. Validate what is physically possible: weapon equipped, fire rate plausible, aim point sane.
The server tells one specific client — UFUNCTION(Client, Reliable). Runs on the owning client only. Correct for hit markers, damage direction, personal damage numbers.
The server tells everyone relevant — UFUNCTION(NetMulticast, Unreliable). Correct for shared cosmetics. Called from a client it just runs locally and goes nowhere. Scales badly by nature since one call becomes N sends.
Blueprint: set Replicates in the function or event Details panel. Note that Blueprint functions cannot be Server RPCs; use a custom event.
Reliable guarantees arrival and ordering by resending until acknowledged. Unreliable is sent once; if it drops it is gone.
Test: if missing one instance is invisible a second later, it is unreliable.
Anything that affects the outcome: damage, health, scoring, spawning, hit registration, the result of an ability.
Must be decided or validated on the server, so every player agrees and cheating is blocked.
Feedback that doesn't change the outcome: muzzle flash, footstep audio, UI animation, particles.
Can run locally for responsiveness. If a client fakes it, there's no competitive impact.
Designing the split well gives you both a responsive feel and fair, consistent outcomes. GAS (Chapter 15) formalizes exactly this, with prediction, server validation, and Gameplay Cues for the cosmetic layer.
Predict freely: muzzle flash, sound, recoil, animation, camera shake, cosmetic tracers, UI press states. All fire locally and immediately.
Never predict: health, ammo, kills, score, objective capture. A wrong prediction about a consequence forces a visible rewind in front of the player, which feels worse than a short delay.
Movement is the exception. The Character Movement Component has built-in client prediction and server correction, because unpredicted movement is unplayable. It is the hardest problem in multiplayer and already solved, hence the advice not to write your own.
Every networked feature decomposes into five steps:
| Step | Where | What |
|---|---|---|
| 1. Input | Local client | Player presses the button |
| 2. Prediction | Local client | Immediate cosmetic feedback |
| 3. Request | Client to server (RPC) | "I would like to do this" |
| 4. Decision | Server, authoritative | Validate, resolve, change real state |
| 5. Propagation | Server to clients | Variables for state, RPCs for moments |
A single shot: client fires, plays local cosmetics immediately, sends Server_RequestFire with a timestamp. Server validates, rewinds hitboxes to that timestamp, traces, resolves damage, changes Health. Health replicates down and OnRep_Health drives health bars. Client_ConfirmHit sends the hit marker to the shooter alone. Multicast_PlayImpactEffect (unreliable) sends sparks to everyone nearby.
Steps 1-2 are feel, 3-4 are truth, 5 is presentation.
Server authority means what you see on screen is the past. With 80ms of latency the enemy is drawn where they were 80ms ago, so by the time your fire message arrives they have moved, and a naive server calls your perfect headshot a miss.
Lag compensation fixes this: the server keeps a short history of hitbox positions, and when a shot arrives it rewinds relevant hitboxes to the shooter's timestamp, re-runs the check against the world the shooter actually saw, then restores everything.
This follows directly: the player rounding a corner sees the defender first, because the defender's screen shows information a round trip old. Every competitive shooter picks a point on this dial, whether by capping the rewind window, tuning it per weapon, or designing map geometry with fewer high-value corners.
Networking bugs are invisible in single-player PIE. Set Number of Players to 2 or 3 and Net Mode to Play As Client, not Listen Server. On a listen server the host is the authority, so every "client tried to change a replicated variable" bug invisibly works and ships. Add latency, because on localhost everything is instant:
Net PktLag=100
Net PktLagVariance=20
showdebug net
GetLifetimeReplicatedProps fails silently.OnRep_ does not fire on the server. Call it manually if the host needs the cosmetic.HasAuthority and IsLocallyControlled are different questions.Answer each out loud before re-reading the chapter.