I built a tiny two-player prototype this weekend, the sort of thing that's meant to take an evening and instead ate the whole of Saturday. The idea was simple: two players, a ball, scores that go up. The lesson was simpler still. The server is the only thing allowed to be right, and I spent hours pretending otherwise.
My mistake was the usual one. I changed state on the client because it felt responsive, then waited for replication to magically agree with me. It does not. In Unreal you set a property Replicated, write to it on the server, and let the clients receive it. The moment I tried to write the score locally "just to be quick" the two machines drifted apart, and the cheap way to notice was that one player won and the other didn't.
UPROPERTY(ReplicatedUsing=OnRep_Score)
int32 Score = 0;
void AScoreActor::AddPoint()
{
if (!HasAuthority()) return;
Score++;
}
That HasAuthority() guard is the whole post, really. Once I stopped letting clients have opinions about facts, everything calmed down. RPCs for intent, replication for truth, and a healthy respect for the fact that the network is not a free function call. I'm not good at this yet, but I'm wrong in more interesting ways now, which I'll take.