I've spent twenty years building distributed systems professionally. I know about consistency, about latency, about the lies the network tells you. So when I decided to build a small multiplayer prototype in Unreal Engine 5 over a few evenings, I assumed the networking would be the part I breezed through and the gameplay would be where I struggled. It was, of course, exactly the other way round, and the lesson was a useful slap.
The prototype was nothing ambitious: a handful of players running around a level, picking up objects, and a simple scoreboard. The kind of thing a tutorial knocks out in an afternoon. And the happy path genuinely did come together quickly, because Unreal's replication system is properly good. You mark a property as replicated, the engine syncs it from server to clients, and for a while it feels like magic.
the magic has rules, and I'd ignored them
The first thing that humbled me was the authority model. In Unreal's default setup the server is authoritative: clients don't get to just decide things and have them be true. I knew this in the abstract. I did not know it in my fingers. So my first instinct, predict locally and let it sync, produced a prototype where the player who hosted saw one reality and everyone else saw a slightly different, slightly delayed one, and the scoreboard couldn't agree with itself.
The fix is the pattern Unreal pushes you towards whether you like it or not: clients ask the server to do things via a Server RPC, the server validates and applies, and the result replicates back. Something like:
UFUNCTION(Server, Reliable, WithValidation)
void ServerPickup(AItem* Item);
bool AMyPawn::ServerPickup_Validate(AItem* Item)
{
// reject nonsense before it touches game state
return IsValid(Item) && Item->IsPickupable();
}
void AMyPawn::ServerPickup_Implementation(AItem* Item)
{
if (!HasAuthority()) return;
Item->SetOwner(this);
Score += Item->GetValue(); // replicated, fans out to clients
}
The _Validate half is the part I'd have skipped if the framework had let me, and it's the part that matters. It runs on the server before the implementation, and a client failing validation gets disconnected. That's not paranoia, that's the entire trust model: never let the client assert game state, only request it.
prediction is where the real work lives
The thing I'd smugly assumed would be easy, making it feel responsive, is where the genuine difficulty lives, and it's the same difficulty as in any distributed system: the client and server are separated by time, and you have to paper over that gap convincingly. If a client waits for a server round-trip before showing any response to a button press, the game feels like wading through treacle. So you predict locally, show the player the likely outcome immediately, and then reconcile when the authoritative answer arrives.
When the server agrees, lovely, nobody notices. When the server disagrees, you have to un-show the thing the player already saw, and do it without it looking like a glitch. This is rollback, and it's the same problem as optimistic UI in a web app, except a web app can usually get away with a spinner and a player cannot. I have new respect for anyone who's shipped a fighting game.
the bugs that don't exist on your own machine
The cruellest part of multiplayer prototyping is that almost everything works perfectly when you test it alone with two windows on one machine, because there's no real latency and no real packet loss. Everything is fast and lossless and lying to you. Unreal has a Network Emulation feature that lets you inject latency and packet loss in the editor, and turning it on is like turning the lights on in a room you thought was tidy:
Net PktLag=120 PktLagVariance=40 PktLoss=5
Suddenly the pickup that felt instant has a visible hitch. The scoreboard that always agreed now briefly disagrees and corrects itself. The smooth movement gets a faint rubber-banding under loss. None of that was visible at zero latency, and all of it is exactly what a real player on real wifi would have hit on day one. I'd built something that worked beautifully in the one environment that doesn't exist.
what I actually took away
The humbling bit wasn't any single mechanic. It was realising that my distributed-systems instincts were correct but pitched at the wrong layer. I think about eventual consistency in terms of databases reconciling over seconds. Multiplayer games solve a harder version of the same problem in tens of milliseconds, every frame, while also being visually convincing, because the consistency boundary is a human eye rather than a retry loop. The maths I knew. The deadline I did not respect.
I'm keeping the prototype, partly because it's fun and partly as a reminder. Twenty years of one kind of distributed system does not transfer for free to another, and the moment you assume it does is the moment the framework, gently and via a disconnected client, teaches you otherwise. Unreal's replication is excellent, by the way. I just had to be wrong a few times before I'd let it be right.