Article written by Alex K.
When using Unreal’s replication system, you may find yourself concerned with the performance characteristics of using replicated properties vs. using RPCs, and it can be unclear which option is the best choice for certain situations. This article seeks to provide an in-depth look at using replicated properties vs. RPCs and to explore when and how they’re best used.
Generally speaking, performance concerns are almost never a good reason (on their own) to choose between RPCs and property replication. To better determine which method to use, there are a few questions you should ask yourself:
- Do I actually need discrete events for every time these values change, or do I just care about its most up to date value?
- Do I need this data to be restored if an actor becomes relevant, if a player joins in progress, or for replay recording / scrubbing, or am I OK with it being "gone forever?”
- Will these values rely on the state of other properties within my Actor, or are they actionable by themselves?
Generally, property replication is best suited for cases where you need to have the most up to date data, but don’t necessarily care about the in-between steps. Because properties will only be sent if they’ve actually changed, and because we may not replicate every actor every frame to every connection, you can save yourself a bit of work and be confident that you’ll get the correct values. Further, although property data is transmitted “unreliably,” we will redundantly send property changes to clients until we have received acknowledgment. There is no such guarantee with unreliable RPCs.
You can guarantee you receive events with reliable RPCs, but those require more overhead and should be used very infrequently. Additionally, property replication just works for the relevancy, join in progress, and replay scrubbing cases. Whenever the first time an actor is replicated to a given connection, you’re guaranteed that the properties relevant to that connection will make it to that connection. Again, RPCs offer no such guarantee.
From the docs for RPCs: “The primary use case for these features are to do unreliable gameplay events that are transient or cosmetic in nature.” This means that there is no eventual consistency for RPCs. If the packet is lost, or the RPC isn’t sent to a specific client, it’s just never going to reach that client. You’ll have no guarantee that your Actor state will be consistent in the join in progress, relevancy, and replay scrubbing cases. However, it also means that if you do really just want to shoot something out immediately and don’t care if it doesn’t reach the clients, then you won’t waste resources comparing properties every frame or transmitting the data redundantly. It’s also important to note that RPCs are the only way to send data from the client to the server.
Still, it can be unclear if using an unreliable RPC will be faster than using a replicated property or vice versa. Again, this is really hard to quantify and depends on how the rest of your system is set up. RPCs eventually need to be routed through Actors (specifically, an actor that is owned by your UNetConnection), and all actors have a base set of replicated properties. Both cases will transmit data over the wire, and in more or less the same format. However, how the data gets routed and applied is a bit different.
For starters, it’s important to note that serialization is going to be nearly identical. For example, both RPCs and property replication rely on replication layouts (see Engine\Source\Runtime\Engine\Public\Net\RepLayout.h). There will be one RepLayout generated per class and one RepLayout generated per RPC, so there will be slightly less memory overhead for just adding a new property than adding a new RPC. Of course, that can easily be offset by the number of instances of the class you have. If there are a lot of instances, eventually the amount of memory the property takes up on each of those instances would surpass the memory for creating / tracking the function RepLayout.
Performance-wise, RPCs end up invoking a lot more virtual function calls and lookups than property replication, and these need to happen every time an RPC is invoked. Further, these calls will happen in the frame right where the RPC is called as opposed to batched at the end of the frame (like with property replication), so it’s more likely that you’ll have cache misses for one off RPCs.
The flow of an RPC looks like this:
- You call your RPC function.
- The parameters get copied into a parameter struct (which is generated by Unreal Header Tool).
- Next, we search for the UFunction object that represents the RPC (by name through a map lookup), and pass that and the params to UObject::ProcessEvent (virtual call).
- In ProcessEvent, we need to figure out the callspace of the function (virtual call to determine if it’s Local, Remote, Both, or Suppressed).
- If it is remote we’ll call UObject::CallRemoteFunction (again, another virtual).
- Then we need to iterate over every available NetDriver associated with the world (usually there’s only 1 anyway) and call ProcessRemoteFunction (another virtual).
- There’s then a ton of lookups to resolve: the actor’s connection, the actor’s channel (and may result in actor channel creation if one doesn’t exist), the actual function being called, and the RepLayout for the function (again, creation may occur if one doesn’t exist).
- Next, we’ll serialize the properties.
- At this point, if you’re using an unreliable multicast RPC or if we’re forcing the RPC to be queued, that data will be cached off and then sent with replicated property data later anyway. If we’re not queueing, we’ll go ahead and try to send it immediately.
The flow of Property Data looks like this (note that this is for standard Net Driver replication, not using RepGraph):
At the end of the frame, the server will figure out all actors that may need to replicate.
The server will then prioritize those actors.
Each connection will then process the prioritized actors, replicating what they need to.
The first time in a frame an actor is replicated (or if we are forcing net comparisons), we will iterate over all replicated properties to generate a changelist (literally, a list of handles of properties that changed), unless push model is enabled, in which case only properties marked as dirty will be checked.
Then, the server will send just the changed properties.
Again, it’s important to note that the steps for RPCs happen every time an RPC is invoked, but the steps for Property Replication will happen every frame regardless. So, if there’s a single new property that you want to add, that property isn’t immensely expensive to compare, and the actor/object is already replicating other properties, then there will be significantly less overhead to just adding a new property. However, if the properties are extremely expensive to compare or you know you only need them to be updated rarely, using RPCs might be faster, and again, RPCs are the only way to get data back to the server from the client.
Going back to that section from the RPC docs: “The primary use case for these features are to do unreliable gameplay events that are transient or cosmetic in nature. These could include events that do things such as play sounds, spawn particles, or do other temporary effects that are not crucial to the Actor functioning. Previously these types of events would often be replicated via Actor properties.” Again, the cases described here aren’t for performance reasons, but rather for gameplay reasons. Let’s look at the example of playing sounds. If you had sounds that were triggered due to properties and an actor became relevant again (or if someone joined in progress, etc.), then you could end up in a scenario where those sounds were retriggered.
So generally speaking, RPCs should be used for effect cues and similar non-critical network messaging, and properties for everything else. Property replication is going to happen on your actors regardless, and there are plenty of optimizations you can start to use if they become a bottleneck. If you have a case where you truly only need to fire a one off event and you don’t care if that event gets dropped, or you need to send data from the client to the server, then you can certainly use RPCs.