Dive Into Character Movement Component

Dive Into Character Movement Component

Unreal Engine
Published November 2, 2020
Tianqi Li
Make sure you have read Character Movement Component and A holistic look at replicated movement before read this article. These articles explain the overall flow and prediction/correction system of CMC really well, this article will not repeat that, only focusing on providing new informations.


TickComponent() is where everything starts, it does different things based on the owning character’s net role:
  • If character is local controlled & authority (i.e. character is npc or this is a non-networked game):
    • PerformMovement() called
  • If character is autonomous:
    • ClientUpdatePositionAfterServerUpdate() called - if we’ve received a ClientAdjustPosition() RPC from the server
    • Get jump and acceleration input
    • ReplicateMoveToServer() called which surrounds PerformMovement() with the necessary logic to record movement as the character performs it, then submits the move to the server.
  • If character is a remote of an autonomous:
    • MaybeUpdateBasedMovement() and MaybeSaveBaseLocation() called.
  • If character is simulated:
    • SimulatedTick() called
Note that remote of an autonomous doesn’t use tick to simulate movement, it only simulates movement when receiving ServerMove rpcs from clients.
In the next part of the article I'll go into more detail about PerformMovement() and SimulatedTick().


Let’s recap the description of it from Character Movement Component:
The PerformMovement function is responsible for physically moving the character in the game's world. In a non-networked game, UCharacterMovementComponent calls PerformMovement directly each tick. In a network game, PerformMovement is called by specialized functions for servers and clients to either perform the initial movement on a player's local machine or reproduce that movement on remote machines.
PerformMovement handles the following:
  • Applies external physics, like impulses, forces, and gravity.
  • Calculates movement from animation root motion and root motion sources.
  • Calls StartNewPhysics, which selects a Phys* function based on what movement mode the character is using.
A even more detailed description is the following diagram, this is a 1:1 match of source code:
notion image

Update Velocity

In the next diagram, I will put everything related to Velocity updating together, includes update logic from various Phys* functions. You should read it like read a anim graph. It's just that unlike anim nodes that input and output pose, these nodes input and output Velocity.
notion image
Different Phys* functions work rather similar, the only differences are:
  1. Some Phys* functions have a finer control time steps.
  1. Some Phys* functions have some custom logic before or after CalcVelocity.

Update Location

This is where these red nodes in the diagram. It handles the following:
  • Calculate a new location using the velocity we just calculated, DesiredLocation = CurrentLocation + Velocity * DeltaTime.
  • Move to this desired location. After the move, if we’ve hit something, we resolve it by sliding or stepping up based on the current movement mode. If we now are in the air or went into water, we switch movement modes and then call StartNewPhysics again with the remaining delta time of the move. We cap out the number of different StartNewPhysics calls you can do per tick.
  • After movement is done, recalculate velocity with the final location, Velocity = (NewLocation - OldLocation) / DeltaTime.

Update Rotation

PhysicsRotation is a fix rate rotation. Its rate is controlled by RotationRate. Its destination is either input direction, if bOrientRotationToMovement is true, or AController::GetDesiredRotation (by default control rotation), if bUseControllerDesiredRotation is true.
PhysicsRotation only works if there is no anim root motion, or bAllowPhysicsRotationDuringAnimRootMotion is true.
Why not also check override root motion???
Apply root motion to rotation will only apply anim root motion or override root motion, additive root motion is not considered. Also note rotation here is added to the current rotation, not override. That means PhysicsRotation and root motion can update the rotation altogether.


SimulatedTick’s logic is depend on whether there is root motion playing.
  • If there is anim root motion playing (and root motion mode is RootMotionFromMontagesOnly):
    • TickCharacterPose() called, this is as expected.
    • SimulateRootMotion() and SimulatedRootMotionPositionFixup() called.
  • If there is root motion source playing:
    • If we have RootMotionRepMoves, find the most recent important one and set position/rotation to it.
    • PerformMovement calledSmoothCorrection called.
    • âť“
      Why simulate based on server’s location, doesn’t this mean simulated client will behind server RTT/2.
  • No any root motion playing:
    • If we were simulating root motion, we've been ignoring regular ReplicatedMovement updates. Force us to sync our movement properties.
    • SimulateMovement is called (unless root motion mode is set to RootMotionFromEverything, in which case PerformMovement will be called, but this is very rare since RootMotionFromEverything is not suggest to be used in multiplayer game). It will avoid moving the mesh during movement if SmoothClientPosition will take care of it.
    • If NetworkSmoothingMode is Linear and mesh move was prevented above, we need to know if the capsule rotation changes, and rotate mesh if answer is yes.
At the end of SimulatedTick, it will smooth mesh location by calling SmoothClientPosition.


SimulateRootMotion handles the following:
  • Compute the root motion velocity and set it to Velocity.
  • Call StartNewPhysics to change capsule’s location.
  • Apply root motion rotation.
In short, this is a simplified version of PerformMovement, with only anim root motion and StartNewPhysics left.


Before introducing SimulatedRootMotionPositionFixup will need to add some context.
Server regularly replicates RepRootMotion to clients, which includes character’s location, rotation, montage, montage’s position, FRootMotionSourceGroup, etc.
void ACharacter::PreReplication( IRepChangedPropertyTracker & ChangedPropertyTracker ) { Super::PreReplication( ChangedPropertyTracker ); if (CharacterMovement->CurrentRootMotion.HasActiveRootMotionSources() || IsPlayingNetworkedRootMotionMontage()) { const FAnimMontageInstance* RootMotionMontageInstance = GetRootMotionAnimMontageInstance(); RepRootMotion.bIsActive = true; // Is position stored in local space? RepRootMotion.bRelativePosition = BasedMovement.HasRelativeLocation(); RepRootMotion.bRelativeRotation = BasedMovement.HasRelativeRotation(); RepRootMotion.Location = RepRootMotion.bRelativePosition ? BasedMovement.Location : FRepMovement::RebaseOntoZeroOrigin(GetActorLocation(), GetWorld()->OriginLocation); RepRootMotion.Rotation = RepRootMotion.bRelativeRotation ? BasedMovement.Rotation : GetActorRotation(); RepRootMotion.MovementBase = BasedMovement.MovementBase; RepRootMotion.MovementBaseBoneName = BasedMovement.BoneName; if (RootMotionMontageInstance) { RepRootMotion.AnimMontage = RootMotionMontageInstance->Montage; RepRootMotion.Position = RootMotionMontageInstance->GetPosition(); } else { RepRootMotion.AnimMontage = nullptr; } RepRootMotion.AuthoritativeRootMotion = CharacterMovement->CurrentRootMotion; RepRootMotion.Acceleration = CharacterMovement->GetCurrentAcceleration(); RepRootMotion.LinearVelocity = CharacterMovement->Velocity; DOREPLIFETIME_ACTIVE_OVERRIDE( ACharacter, RepRootMotion, true ); } else { RepRootMotion.Clear(); DOREPLIFETIME_ACTIVE_OVERRIDE( ACharacter, RepRootMotion, false ); } ... }
After received it on a simulated client, client will add it to a queue - RootMotionRepMoves.
void ACharacter::OnRep_RootMotion() { if (CharacterMovement && (CharacterMovement->NetworkSmoothingMode == ENetworkSmoothingMode::Replay)) { return; } if (GetLocalRole() == ROLE_SimulatedProxy) { UE_LOG(LogRootMotion, Log, TEXT("ACharacter::OnRep_RootMotion")); // Save received move in queue, we'll try to use it during Tick(). if( RepRootMotion.bIsActive ) { // Add new move RootMotionRepMoves.AddZeroed(1); FSimulatedRootMotionReplicatedMove& NewMove = RootMotionRepMoves.Last(); NewMove.RootMotion = RepRootMotion; NewMove.Time = GetWorld()->GetTimeSeconds(); } else { // Clear saved moves. RootMotionRepMoves.Empty(); } if (CharacterMovement) { CharacterMovement->bNetworkUpdateReceived = true; } } }
What SimulatedRootMotionPositionFixup does is using this queue to update client’s position.
It will find the last RepRootMotion matches client’s montage instance, matching conditions includes same montage, same section, section is not looped and server’s montage playing position is not ahead of client’s. If a valid RepRootMotion found:
  • It will move character back to position of that RepRootMotion . (server replicated position).
  • SimulateRootMotion again to get back to where we were on the client, delta time passed to SimulateRootMotion is client and server’s montage position difference.
  • Smooth out error in position by calling SmoothCorrection.


SmoothCorrection’s logic is depend on NetworkSmoothingMode:
  • if NetworkSmoothingMode == ENetworkSmoothingMode::Disabled:
    • move both the capsule and the mesh.
  • if NetworkSmoothingMode == ENetworkSmoothingMode::Linear:
    • move the capsule, but not the mesh. and only move capsule’s location, rotation is changed in SmoothClientPosition.
  • everything else:
    • move the capsule, but not the mesh.
SmoothCorrection will also update GetPredictionData_Client_Character() if NetworkSmoothingMode is not ENetworkSmoothingMode::Disabled.


SimulateMovement is another simplified version of PerformMovement. It handles the following:
  • HandlePendingLaunch called.
  • MaybeUpdateBasedMovement called.
  • MoveSmooth called, which is a extremally simplified version of PhysWalking + PhysFalling + PhysFly.
  • Check if falling, change movement mode to falling and update falling Velocity if answer is yes.
Acceleration is not currently replicated for simulated movement, that should be why CalcVelocity is not called here.
Why not also call ApplyAccumulatedForces before HandlePendingLaunch ?


Smooth mesh location for network interpolation, based on values set up by SmoothCorrection. Internally this simply calls SmoothClientPosition_Interpolate() then SmoothClientPosition_UpdateVisuals().