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
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 aClientAdjustPosition()
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()
andMaybeSaveBaseLocation()
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()
.PerformMovement
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:
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
.Different Phys* functions work rather similar, the only differences are:
- Some Phys* functions have a finer control time steps.
- 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 differentStartNewPhysics
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
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()
andSimulatedRootMotionPositionFixup()
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 toRootMotionFromEverything
, in which casePerformMovement
will be called, but this is very rare sinceRootMotionFromEverything
is not suggest to be used in multiplayer game). It will avoid moving the mesh during movement ifSmoothClientPosition
will take care of it.- If
NetworkSmoothingMode
isLinear
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
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.SimulatedRootMotionPositionFixup
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 toSimulateRootMotion
is client and server’s montage position difference.
- Smooth out error in position by calling
SmoothCorrection
.
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
SimulateMovement
is another simplified version of PerformMovement
. It handles the following:HandlePendingLaunch
called.
MaybeUpdateBasedMovement
called.
MoveSmooth
called, which is a extremally simplified version ofPhysWalking
+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
?SmoothClientPosition
Smooth mesh location for network interpolation, based on values set up by SmoothCorrection. Internally this simply calls
SmoothClientPosition_Interpolate()
then SmoothClientPosition_UpdateVisuals()
.