I've been poking at a small multiplayer prototype in Unreal in the evenings, mostly to find out how much I don't know. The answer, predictably, is "a lot," and the first wall I hit was replication. I had a value that changed on the server, I could see it change with a log line, and the client just sat there with the old number like nothing had happened. This is the post I wish I'd read before I lost an evening to it.
the mental model nobody handed me up front
The thing that finally made replication make sense was getting the authority model straight in my head. In Unreal's networking, the server is authoritative. It owns the truth. Clients are, broadly, told what's happening; they don't decide it. Replication is the machinery by which the server's truth is pushed out to the clients that need it.
So a variable doesn't "sync" in some symmetric, peer-to-peer way. The server changes it, and if you've asked nicely, the engine sends that change down to the clients. The reverse does not happen for free. A client cannot just change a replicated variable and expect the server to notice; that's what RPCs are for, and that's a separate rabbit hole I'll save for another evening.
The reason my value wasn't moving was embarrassingly simple once I understood this: I was changing it on the client. Of course it changed locally. It just had no authority, so nothing propagated, and the next server update would have overwritten it anyway. Lesson one: check where your code is actually running. HasAuthority() is your friend, and GetNetMode() in a log line will save you a lot of confusion.
the two lines you actually need
Making a property replicate is not hard, it's just easy to get half-right. You mark the property with a Replicated specifier, and then, crucially, you register it in GetLifetimeReplicatedProps. Miss the second part and nothing moves, with no error to tell you why.
UPROPERTY(Replicated)
int32 Health;
void AMyActor::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(AMyActor, Health);
}
There's also a flag on the actor itself, bReplicates, which has to be true, and which I had forgotten to set on the very first attempt. So that's three things, and missing any one of them produces the same symptom: server changes the value, client never hears about it. The silence is the worst part. There's no compile error and no runtime warning, just a number that won't budge.
RepNotify, the bit that made it useful
Plain replication updates the value, but often you want to react when it arrives: play a sound when health drops, update a healthbar, flash the screen red. For that there's ReplicatedUsing, which nominates a function to call on the client whenever the new value lands.
UPROPERTY(ReplicatedUsing=OnRep_Health)
int32 Health;
void AMyActor::OnRep_Health()
{
// runs on clients when Health replicates in
UpdateHealthBar();
}
The gotcha here, and it caught me, is that OnRep_Health runs on clients but not on the server that set the value. The server changed Health directly, so it never sees its own RepNotify fire. If you want the same reaction on the server, you call the function yourself after changing the value. It feels asymmetric because it is, and once I stopped expecting symmetry the whole thing got easier to reason about.
where I've got to
The prototype now has a value that changes on the server and a client that actually reflects it, complete with a healthbar that updates when it should. Modest, but it took a genuine shift in how I think about state to get there. The summary I've pinned in my notes: the server owns the truth, replication is a one-way push of that truth to clients, you have to opt every property in twice (the specifier and the lifetime registration), and RepNotify is how you turn a replicated value into a reaction. Clients asking the server to change things is a different mechanism entirely, and I'll find out how badly I misunderstand that next weekend.