I've been poking at Unreal Engine 4 in the evenings, and this week I tried to make two players see the same thing at the same time. This is, it turns out, the hard part of multiplayer, and Unreal has opinions about how you do it. My prototype worked beautifully on the server's screen and showed the client a world where objects teleported, health bars lied, and a door I'd opened was somehow still shut. A first encounter with replication, and a humbling one.
the mental model i didn't have
The thing nobody tells you clearly enough at the start: in Unreal's model there is one authority, the server, and everything else is a copy trying to stay in sync. The server owns the truth. Clients receive updates and, mostly, are not allowed to change the world directly. I'd been writing code as though the client could just do things, and the engine was quietly ignoring half of it because I wasn't the authority.
The first concept is Replicated properties. You mark a UPROPERTY as replicated and the server pushes its value out to clients automatically:
UPROPERTY(ReplicatedUsing = OnRep_Health)
float Health;
void AMyCharacter::GetLifetimeReplicatedProps(
TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(AMyCharacter, Health);
}
If you forget the GetLifetimeReplicatedProps registration, the property silently never replicates, which is exactly the trap I fell into. It compiles, it runs, it just never updates the client, and you stare at it convinced you've found an engine bug. You have not. You've forgotten the macro.
ReplicatedUsing gives you a callback, OnRep_Health, that runs on the client when the value arrives. That's where the health bar should update, not in Tick, because the client only knows the value changed when the replication tells it so.
who is allowed to do what
The bit that finally made it click was authority. Anything that changes the game state has to happen on the server. When a client wants to do something, like fire a weapon or open my stubborn door, it doesn't just do it. It asks the server to, via a Server RPC:
UFUNCTION(Server, Reliable, WithValidation)
void Server_OpenDoor();
void AMyDoor::Server_OpenDoor_Implementation()
{
// runs on the server, the authority
bIsOpen = true; // a replicated property
}
bool AMyDoor::Server_OpenDoor_Validate()
{
return true;
}
The client calls Server_OpenDoor(), the engine ships that call to the server, the server runs the _Implementation, flips a replicated bIsOpen, and that change flows back to every client including the one that asked. My door was stuck shut because I'd been setting bIsOpen directly on the client, where it had no authority, so the server never knew and never told anyone else.
The reverse direction, server telling clients to do something cosmetic like play a sound or a muzzle flash, is a Multicast RPC. The split matters: gameplay-affecting state changes go up to the server as authority, presentation effects come down to everyone as multicast. Get those backwards and you either have cheating-by-design or effects that only one person sees.
checking authority before you act
Half my desync bugs came from code running in places I didn't expect, because in Unreal the same actor class runs on both server and client. The guard you reach for constantly is HasAuthority():
if (HasAuthority())
{
// only the server should do this
ApplyDamage(...);
}
Once I started thinking "which machine is this line running on, and is it allowed to", the teleporting stopped. The objects had been getting their position from both a local guess and a server correction, fighting each other every frame. Letting the server own movement and the client merely interpolate towards the replicated transform smoothed it out.
reliable versus unreliable, and why it matters
The other thing I had to unlearn was treating every RPC as if it would always arrive. Unreal lets you mark an RPC Reliable or Unreliable, and the difference is exactly what it sounds like. A Reliable RPC is guaranteed to be delivered and to arrive in order, at the cost of bandwidth and the risk of clogging the channel if you spam them. An Unreliable one is fire-and-forget; it might be dropped, and that's fine for things that are sent constantly and self-correct, like a stream of movement updates where the next packet supersedes the last.
My door-opening RPC is Reliable, because a dropped "open the door" that never retries leaves a player standing at a shut door forever, which is the kind of bug that gets you a one-star review. But the muzzle flash multicast can happily be Unreliable, because if one flash goes missing nobody notices and the next shot sorts it out. Getting this wrong in the expensive direction, making everything Reliable, doesn't break correctness but it does flood your bandwidth, and you won't see it until you test with real players on a real connection rather than two clients on localhost where the network is effectively free.
the localhost trap
Which is the warning I'd give my past self loudest. Everything I built ran flawlessly with two clients on the same machine, because localhost has no latency, no packet loss, and infinite bandwidth. Replication bugs hide there. The moment you put even 80ms of simulated latency between client and server, the cracks I'd plastered over with client-side guesses reappear, and you discover which of your "working" features were quietly relying on the server's correction arriving instantly. Unreal's network emulation settings let you dial in lag and loss in the editor, and turning them on early is the single best habit I've picked up this week. A multiplayer feature that only works on localhost is a single-player feature wearing a disguise.
was it worth the headache
Yes, though it's a genuinely different way to think and I won't pretend the first day wasn't frustrating. The reward is that once the authority model is in your head, Unreal's replication does an enormous amount for free. You mark a property, register it, decide who has authority, and the engine handles the serialisation, the network, the relevancy, all the parts I'd have spent months getting wrong by hand. It's opinionated, but the opinions are the hard-won ones, and fighting them is just delaying the moment you agree with them.
Next I want to understand network relevancy and how Unreal decides which actors a given client even needs to know about, because my test had two players in one room and the real thing won't. But the desync is gone, the door opens for everyone, and the health bar finally tells the truth. A good evening, eventually.