OK after a lot of debugging and spelunking through code I think I have some answers, I’ll try not to get too specific because **I am still trying to grok the code and likely to get something wrong, if what I am saying here is wrong please reply to set the record straight **
Max Packet size 1024
Max Bunch size = ~952 bytes (7620 bits this is the max packet size minus the size of bunch headers)
Send
An Actor is serialized into one or more bunches in a single call to UActorChannel::ReplicateActor() which
- serializes the replicated properties of the actor
- calls DoSubObjectReplication to traverse the replicated components and serialize replicated properties from each component
During ReplicateActor() the actor is written into either a “bunch” but because these have a size limit it might be broken up “partial” bunches. When a bunch is being written to a packet it first checks to see if there is room, if there isn’t room in the packet that packet is flushed and a new packet is started.
Receive
Receiving is driven via a specific NetDriver, in my testing I am using the Ip Net Driver and the BSD Socket Implementation so the reads occur in UIpNetDriver::TickDispatch. I’m tempted to go down the rabbit hole here on the low level details of the receive but will fight that urge!
Partials are not processed (they are not applied to an actor) until all of the partial bunches are received and are re-assembled.
Once the entire bunch is received (again this could be one, or it could be a bunch of bunches which are tagged as partials) we will call UActorChannel::ProcessBunch( FInBunch & Bunch ) which does the heavy lifting and should be the spot you look at if you need to.
- The actor is spawned locally and deserialized from the bunch(es) if it is a new actor
- The properties are processed by calling ReceiveProperties_r
- we will compare the serialized value to the current value for each property and if they are different we will add an item to an Array called RepNotifies.
- after we have processed all of the data from all of the bunches, we will enter a for loop over the ReplicationMap and will invoke FObjectReplicator::PostReceivedBunch(), UObject::PostNetReceive(), and call CallRepNotifies(true) which will traverse the array of RepNotifies and call all of the items we queued.
- if this was a newly spawned actor we will now call Actor->PostNetInit() which is how BeginPlay() gets called for a new actor.
Now that we have that huge wall of text we can answer some of the below questions.
Q: If two Properties change on an actor in a single server frame is the client guaranteed to get both of these changes at the same time?
Yes two properties in the same actor which change enough to be replicated on the server in the same server frame should be guaranteed to be received together because there is no point where we ever partially serialize an actor and then apply a partial serialization to client data.
Q: If two Properties in the same component change on an actor in a single server frame is the client guaranteed to get both of these changes at the same time?
Same answer as above, yes see above we don’t apply partially serialized actor data to the client’s data.
Q: If a property in the actor and a property in one of the Actor’s components change in a single server frame is the client guaranteed to get both of these changes at the same time?
Same answer as above, yes see above we don’t apply partially serialized actor data to the client’s data.
Q: If 2 properties in 2 different components on a single actor change in a single server frame is the client guaranteed to get both of these changes at the same time?
Same answer as above, yes see above we don’t apply partially serialized actor data to the client’s data.
Q: Are callbacks for replicated fields called as the data for that field is assigned or after all fields are assigned for the incoming bunch?
They are queues and called AFTER all fields are updated. This means during any OnRep call if we had a dependency on another field we are free to inspect that field and it should have the latest data, there is no order issues with the Rep Notifies.
One problem
One issue I noticed is this if I am running a high rate server and my client thread stalls it can lead to cases where the client can have multiple versions for the same Actor so imagine version 5 of an Actor, version 6, version 7, etc… When this occurs it can lead to a hit in client processing time to call the OnRep calls for every version of the actor that has arrived on the client. This can then cause the client frame to run long which again leads to more backed up packets.
A solution
Ideally this code should be changed to queue up every actor’s RepNotifies until we are done processing all packets (or the timer has expired) then we can fire callbacks once for each actor instead of once per version received, we just need to “OR” together the RepNotifies between the versions to ensure we call the RepNotifies once per field even if we received the Actor with 10 different values for a field. I’ll look into this when I get time and see if I can whip up a fix.
Thanks for your time and if there are mistakes here please reply, having correct info is more important than egos.