I have spent a fair chunk of my career on backend systems, so I went into Unreal's networking feeling reasonably smug. Distributed state? Authority? I have done worse. Then I spent the better part of an evening watching a door open perfectly on the server and stay resolutely shut on every client, and the smugness went out of me like air from a tyre.
The mental model that finally made it click: in Unreal there is one authoritative server and the clients are, in a sense, polite approximations of it. The server owns the truth. Clients are told about changes to that truth through replication, but only for the specific things you have explicitly marked as replicated. Nothing happens by magic, and the engine will cheerfully let you change a value locally that never leaves the machine it changed on. Which is exactly what I had done.
the property that went nowhere
My first instinct was a plain replicated property. You mark it in the class and declare it in GetLifetimeReplicatedProps:
UPROPERTY(Replicated)
bool bIsOpen;
void ADoor::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(ADoor, bIsOpen);
}
This does work, in that the value now arrives on the client. But the value arriving is all it does. The client's bIsOpen flips to true, and nothing visual happens, because the code that actually swings the door open ran only on the server, inside the function that set the flag. The client got the new state and no instruction about what to do with it.
RepNotify, or "tell me when it changes"
The fix is ReplicatedUsing, which calls a function on the client the moment the replicated value lands:
UPROPERTY(ReplicatedUsing = OnRep_IsOpen)
bool bIsOpen;
UFUNCTION()
void ADoor::OnRep_IsOpen()
{
PlayDoorAnimation(bIsOpen);
}
Now the server changes bIsOpen, the new value replicates down, and OnRep_IsOpen fires on each client to actually play the animation. The flag is the state, the RepNotify is the reaction to the state changing. Separating those two things in my head was the whole lesson of the evening.
the gotcha that cost me twenty minutes
One detail worth flagging because it bit me: RepNotify does not fire on the server when the server itself sets the value, only on clients receiving the replicated update. So if your visual logic lives entirely in OnRep, the server (which is also a listen-server host, watching the same screen) never sees it. The common pattern is to call the notify function manually on the server after setting the value, so both paths run the same code.
It is a small asymmetry, but it is the sort of thing that has you staring at code that looks completely correct, on a machine where it never gets called. Once you internalise "the server is special and does not get its own notifications", a lot of the confusing behaviour stops being confusing.
I came in thinking I understood distributed state and I did, in the abstract. What I had not internalised was how opinionated Unreal is about it: authority on the server, explicit opt-in on every replicated thing, and a clear seam between the value and the reaction. Annoying for an evening. Genuinely sensible once you stop fighting it.