Even though this is an old question, I’d love to know more about this too. I’m still learning Unreal and C++, but here’s what I believe is true:
- Since there’s no guarantee with UDP that the moves will be sent, or will arrive in order, I don’t think this system guarantees every move will eventually make it to the server. Like in
ServerMoveOld_Implementation()
, line 8501, it’s possible for a move to be older than the server’s version of the world, in which case it’s just discarded with a log warning.
Line Numbers are from looking at the code in Unreal 5.1.1, in case there are differences between versions
-
So OldMove is more like an imperfect safety net, giving a move additional chances to be processed, rather than a orderly queue where everything will be processed.
-
In Ryan B’s post on “A holistic look at replicated movement”, Ryan mentions:
Side note: When the server receives a ServerMove, the client has sent it a ClientTimeStamp. This is the value that the server looks at to calculate what deltaTime to process its PerformMovement with. Why? Packets get dropped. Every ServerMove() a client sends isn’t going to get to the server on time, or in order, or ever, so it’s meant to handle those cases. If there’s been a major mess-up with the packets in transition, it’ll likely lead to a correction being sent, since now information will have been lost. If Move 1 you were moving forward, Moves 2-9 were moving to the right, and then Move 10 you moved forward, if the server received Move 1 and then Move 10, it would simulate that entire Move 1-10 time as if you were holding forward the entire time, and be way off. No avoiding that.
Which I believe is why, when an OldMove is selected, the system tries to pick one that’s significantly different from the last acknowledged move (ReplicateMoveToServer()
, line 8141). If moves are similar enough, it doesn’t matter if some of the middle ones don’t get through, since it will apply the move to the whatever time has passed since the previously acknowledged move (such as in ServerMoveOld_Implementation()
line 8501 where it assigns a value to DeltaTime.)
- If different moves do get backed up, there’s still a chance for old moves to all come in order. Since
ReplicateMoveToServer()
is called fromControlledCharacterMove()
, which is called fromTickComponent()
, there’s a chance to submit moves over and over. If an OldMove happens to get to the server and is acknowledged, but the new move happens to be dropped, the system would advance to the next significantly different OldMove and try to send that on the nextReplicateMoveToServer()
call.
This is just my guess, but by looking at this design, I’d think it’d work well if:
- Moves get through much of the time, and only generally need an imperfect safety net to help with dropped packets.
- Consecutive moves are often similar enough that dropping a few doesn’t matter much
- Moves are usually processed quickly, keeping the significantly different moves in the queue short, to avoid the very situation you’re talking about.
Would love to hear from others more familiar than I am!