How does CharacterMovementComponent handle resubmitting multiple lost moves?

I’m trying to understand how this part of the CharacterMovementComponent works:

The system for buffering saved moves
already ensures that movement
information lost in transit will be
resubmitted and evaluated. This
provides a similar safety net to a
reliable function, but without the
risk of overflowing the reliable RPC
buffer, and with added provisions to
make sure movement data that is too
old gets discarded.

(From CMC docs)


UCharacterMovementComponent::ReplicateMoveToServer appears to find the oldest (unacknowledged) important move and send it alongside NewMove whenever NewMove can no longer be delayed.

Then, in UCharacterMovementComponent::CallServerMove, the old move is sent to the server using ServerMoveOld.

My question is: what happens when there are several old moves which need to be sent? What looks to be happening is that a single old move is sent right before the next new move.

I want to understand what happens when multiple consecutive moves are unacknowledged and if it is possible for there to be a chain of moves like this:

Move 1 (acked) → Move 2 (not acked) → Move 3 (not acked) → Move 4 (acked)

1 Like

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:

  1. 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

  1. 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.

  2. 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.)

  1. If different moves do get backed up, there’s still a chance for old moves to all come in order. Since ReplicateMoveToServer() is called from ControlledCharacterMove(), which is called from TickComponent(), 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 next ReplicateMoveToServer() 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!

That’s a pretty solid interpretation and walkthrough. Well done.

Overall the system is not designed to make bad connections work well. If ping/jitter/loss is all over the place on a users connection, well that’s on them. There is network smoothing to help ease corrections/latent moves. Anything more is bloat and overhead.