I've been poking at Unreal in the evenings, and this week I decided to do the thing that separates a toy from a game: make two players see the same world. Networking. Replication. The part everyone tells you is hard, which you nod along to without really believing until you try it.
The mental model that finally clicked is that the server is the truth and the clients are guesses. Unreal's whole replication system is built around authority. The server owns the canonical state of a replicated actor, and clients receive updates about it. A client can ask the server to do something, but it cannot simply decide that its health is now 100 and have everyone agree. If you let clients be authoritative you have built a cheating machine, so the engine makes the server-knows-best model the default and you work within it.
Where I tripped, repeatedly, is that marking a variable for replication does not make it two-way. You flag a property with Replicated, register it in GetLifetimeReplicatedProps, and now when the server changes it, clients get told. That's it. A client changing the same variable locally just changes its own copy, which the next replication update from the server will cheerfully stomp on. I spent a baffled half-hour watching a value "not save" before realising the client was scribbling on a number the server didn't know about.
To actually make something happen, you need an RPC, and the RPCs come with a direction baked into their names. A Server function is called on a client and runs on the server. A Client function is called on the server and runs on a specific client. A NetMulticast runs on the server and on everyone. Getting the direction wrong gives you the special kind of bug where nothing errors and nothing happens, which is the worst kind, because there's nothing to grep for.
UPROPERTY(ReplicatedUsing = OnRep_Health)
float Health;
UFUNCTION(Server, Reliable, WithValidation)
void ServerApplyDamage(float Amount);
void AMyPawn::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& Out) const
{
Super::GetLifetimeReplicatedProps(Out);
DOREPLIFETIME(AMyPawn, Health);
}
The ReplicatedUsing bit is the part I wish I'd understood sooner. Instead of just having the value updated silently, you nominate a function that fires on the client whenever the new value arrives. That's where the visible stuff goes: play the hit reaction, update the health bar, flash the screen red. The variable replicating and the player seeing the consequence are two separate jobs, and conflating them is why my first attempt updated the number but never the UI.
The other thing that bit me was testing alone. A single PIE window hides everything, because the local player is both client and server and authority confusion never surfaces. The moment I switched to running two clients with a dedicated server, half my "working" code fell over, because it had been quietly relying on being authoritative everywhere. Run two clients from the start. The pain you feel early is the pain you'd otherwise feel later, in front of someone else.
None of this is hard once the model is in your head: server is truth, replication is one-way notification, RPCs carry intent across the boundary, and a RepNotify is where the client reacts. But that model is not what the gentle tutorials imply, where you tick a box and multiplayer happens. It's a discipline about who is allowed to decide what, and the engine enforces it whether you've understood it yet or not. I have a tiny prototype where two capsules can shoot each other and the damage agrees on both screens, which after this week feels like a genuine achievement rather than the trivial thing it sounds.