Ramblings of an aging IT geek
← Ramblings of an aging IT geek
gamedev

the day i realised the server doesn't trust me

My first real encounter with Unreal's replication model, where a variable that looked perfectly fine on my machine simply didn't exist on anyone else's.

A game development scene on a monitor

My first proper run-in with Unreal's replication went like this: I added a health value to an actor, decremented it when the player got hit, watched the number drop perfectly in the editor, and then hopped into a two-player test where one player could see the damage and the other couldn't. Nothing was crashing. The code was, by single-player logic, completely correct. It just wasn't true on more than one machine at once, and that was the whole lesson.

I'd been writing the game as though there was one world. There isn't. There's a server that owns the truth, and there are clients that hold a copy and are, frankly, not trusted with much. My Health variable was a plain member, so each machine had its own private copy, all drifting apart the moment anything happened. The server decremented its copy, the clients never heard about it, and the editor lied to me because in Play-in-Editor the listen server and my view were the same process. Everything looked fine precisely because I was the one machine that did know the truth.

A code editor showing a replicated property declaration

The fix is small and the concept behind it is not. You mark the property Replicated, you register it in GetLifetimeReplicatedProps, and you make sure the value is only ever changed on the server. Then Unreal takes care of pushing the new value out to the clients on its own schedule.

UPROPERTY(ReplicatedUsing = OnRep_Health)
float Health;

void AMyPawn::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& Out) const
{
    Super::GetLifetimeReplicatedProps(Out);
    DOREPLIFETIME(AMyPawn, Health);
}

The bit that took longest to sink in is "only ever changed on the server". My instinct was to change the value wherever it was convenient, which on a client does precisely nothing useful: you scribble on your local copy, the server overwrites it on the next update, and your change evaporates. The mental model that finally clicked is to stop thinking "set the variable" and start thinking "ask the server to set the variable, then react when it tells me it did". That OnRep_Health function is the reacting part. It runs on the clients when the new value arrives, and it's where the health bar actually updates, the hit sound plays, the screen flashes red.

What surprised me most is how much of multiplayer game code is really this one idea wearing different hats. Authority lives on the server. Clients ask and then react. Once I stopped trying to make my single-player code "also work over the network" and instead designed around who is allowed to know what, a lot of confusing behaviour turned into something I could predict. It's slower to write, you say everything twice in a sense, but it's the difference between a game that works on your machine and a game that works.

I've a long way to go with this. Replication has whole subsystems I haven't touched, relevancy, RPCs, the cost of sending too much too often. But the first encounter was worth writing down, because the bug wasn't really a bug. It was me discovering that the server doesn't trust me, and that this is, on reflection, exactly how it should be.