I thought I understood Unreal's networking. I had read the docs, I knew the words: authority, replication, RPCs. So I gave myself a weekend to bolt multiplayer onto a little movement prototype, fully expecting to spend the time on polish. Instead I spent it discovering that knowing the words and understanding the model are very different things.
The setup worked beautifully when I ran it as a listen server. Two characters, both moving, picking up the same physics props, looking exactly as I'd imagined. I was, briefly, quite pleased with myself. Then I ran a proper dedicated server with a real client connecting in, and the client's view was a hallucination. Props snapped back when I touched them. Other players juddered. My own character felt fine, because of course it did: locally I had authority and everyone else didn't.
That is the whole lesson, really. On a listen server the host has authority over everything, so nothing is ever actually tested across the wire. Every bug I had was hidden because the machine doing the work and the machine showing the result were the same machine. It is the gamedev equivalent of "works on my machine," except my machine was secretly doing both jobs.
The specific mistakes were dull and instructive. I was mutating gameplay state directly on the client and expecting the server to agree, which it had no reason to do. I had a UPROPERTY I'd marked Replicated but never registered in GetLifetimeReplicatedProps, so it silently did nothing. And I'd reached for a Multicast RPC where the value should have just been a replicated variable, which meant late-joining clients missed the event entirely because an RPC is a moment, not a state.
void AMyPawn::GetLifetimeReplicatedProps(
TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(AMyPawn, HeldItem);
}
One line. The variable had been "replicated" in my head for two hours before I noticed it wasn't replicated anywhere the engine could see.
Once I forced myself to think server-authoritative from the start, the picture cleared. The client predicts, the server decides, the client reconciles when they disagree. Movement gets that for free through the built-in character movement component, which is exactly why my own character felt fine and everything I'd hand-rolled felt awful. The engine was quietly doing the hard part for one system and leaving me to discover how hard it is everywhere else.
I did not ship a polished prototype. I shipped a much shorter prototype where the props stay where you put them, and a far better understanding of why netcode is its own discipline. Worth the weekend. Humbling, but worth it.