Senior Multiplayer programmer here - here is a good rule of thumb:
- If you are changing the State of an actor, use a replicated variable. Use an OnRep callback if you want to respond to changes.
- If you need a one-time Event, use a multicast.
This design eliminates any interference from Network Relevancy and handles Join-In-Progress etc seamlessly. Personally, I don’t often find much use for Multicasts in gameplay code - they’re more useful for say, broadcasting a server message to players etc.
However - sometimes it can make sense to use replicated variables for events which occur very often. A good example is ShooterGame’s ‘BurstCounter’ - which in that case is just simple integer value on the weapon class, which increments and uses an OnRep to trigger firing effects on clients.
You don’t want to be clogging up your RPC buffer with unreliable multicasts for simple cosmetic events like this, especially as they occur so often. Leave the RPC buffer free for other more important things. Variable replication is also considerably more efficient here, because you can benefit from network prioritisation and update rate optimisations - you get no such benefit with RPC’s.
There is a myth that OnRep callbacks are not reliable, but they absolutely are. If you receive a value from the Server and it differs from the Clients’ current value, the OnRep WILL be called. I have worked on several games that rely on this behaviour, and they simply wouldn’t work if it didn’t.
In addition, OnRep’s have some other useful tools you can use. You can force the OnRep to be called even if the Clients’ current state matches the received state by adding the following flags in GetLifetimeReplicatedProps:
DOREPLIFETIME_CONDITION_NOTIFY(AMyActor, MyReplicatedProperty, COND_Custom, REPNOTIFY_Always);
You can also optionally add a “Previous Value” argument to the OnRep callback, and Unreal will pass the variables previous client value to it. This is now used by Epic quite extensively as part of the Gameplay Abilities system, but it has always been a feature, and you can use it in regular gamecode too. Here’s an example from one of my projects:
/* Used to drive firing simulation effects (muzzle flashes, audio etc.) */
UPROPERTY(Transient, DuplicateTransient, ReplicatedUsing = "OnRep_BurstCounter")
/* Replication function. */
UFUNCTION() void OnRep_BurstCounter(const FHT_RepBurstInfo& PreviousValue);
The Server will ONLY send values to clients if it thinks that the client has a different value currently (the server maintains a list of “acked” property states for every property).
I think this is where people get confused and assume it to not be working properly. You will not receive all updates made to a variable, you are only garaunteed to receive the eventual state. It’s up to you to make the code resilient to this.