A few evenings on from my first encounter with Unreal's replication, I've got the basics working and have promptly walked into the next pit. The replicated properties update, the RepNotifies fire, the healthbar moves. Then I moved the two players far apart in the level and things stopped updating. Nothing had changed in my code. Welcome to relevancy.
relevancy, the thing I'd been ignoring
Unreal doesn't replicate everything to everyone all the time, because that wouldn't scale. Each actor has a notion of relevancy to each connection, and if an actor isn't relevant to a given client, the server simply doesn't bother sending it updates. The default rules lean heavily on distance and visibility, which is sensible for a shooter and surprising for me, standing two players in opposite corners of a test level and wondering why one had gone stale.
The practical upshot: a thing can be perfectly correctly set up to replicate and still appear broken, purely because the engine has decided it's not worth telling that client about it right now. Once I knew the word "relevancy," the fix was a matter of reading which knobs apply. bAlwaysRelevant exists for the handful of actors that genuinely must reach everyone, like a game-state object holding the score, and you should reach for it sparingly rather than slapping it on everything to make the symptom go away.
RPCs, finally
Last time I noted that a client can't just change a replicated variable and have the server care. The mechanism for "client asks server to do something" is a Server RPC, and now I've used one in anger it's less mysterious than it looked.
UFUNCTION(Server, Reliable, WithValidation)
void ServerFire();
void AMyPawn::ServerFire_Implementation()
{
// runs on the server, with authority
}
bool AMyPawn::ServerFire_Validate()
{
return true; // cheap sanity check; reject obvious nonsense here
}
You call ServerFire() from the client, and the engine ships it to the server and runs the _Implementation there, where the code actually has authority to change the world. The three specifiers each earn their place. Server says which way it travels. Reliable says don't drop it, which you want for a gameplay action like firing but emphatically don't want for a thing you send sixty times a second. WithValidation forces you to write a _Validate that can reject malformed calls, which is the engine quietly reminding you that a client is untrusted input and you should treat it like one.
The naming convention tripped me once: you declare ServerFire, but you implement ServerFire_Implementation and ServerFire_Validate, and the engine wires them together. Forget the suffix and you get linker errors that don't obviously point at the cause.
the shape that's emerging
Three evenings in, the mental model that's holding up: the server owns the truth, replicated properties push that truth out to relevant clients, relevancy decides who counts as "relevant," and RPCs are how a client politely asks the authoritative side to change something, with validation because you don't trust the asker. None of it is conceptually hard. It's that each piece fails silently when you get it slightly wrong, so debugging is mostly learning the vocabulary for what's already happening. I'm enjoying it more than I expected, which is dangerous for my evenings.