If you’re using the experimental Offset Root Bone node and testing on a listen server, you may have noticed a one-frame visual “flick” on remote client characters during sharp rotation changes. The mesh briefly snaps to the new capsule direction, then the offset catches up and pulls it back. It looks like the root bone offset just isn’t there for one frame.
This only affects the listen server’s view of client characters. Standalone, the client’s own view, and clients watching the server character all work fine.
What we tried that DIDN’T fix it
NetworkSmoothingMode = Disabledon the CMCp.NetEnableListenServerSmoothing 0p.DeferCharacterMeshMovement 0- Changing OffsetRootBone mode (Interpolate, Accumulate — same flick)
- Changing rotation halflife and max rotation error values
Root cause
It’s a timing conflict inside MoveAutonomous. When the server processes a client’s movement RPC, the following happens in this order within a single frame:
PerformMovement— capsule snaps to the new rotationTickCharacterPose— animation evaluates and theAnimInstanceProxycaches the new rotationSmoothCorrection— stores the rotation delta for listen server visual smoothing- CMC tick —
SmoothClientPosition_UpdateVisualssets the capsule/mesh back toward the old rotation (for visual smoothing) - Mesh tick —
RefreshBoneTransformsre-evaluates the animation, but the proxy’s cached transform is still the pre-smoothing value from step 2
The renderer uses the post-smoothing transform (step 4) but the bone offset was computed for the pre-smoothing transform (step 2). One-frame mismatch = flick.
On top of this, ACharacter::PossessedBy sets bOnlyAllowAutonomousTickPose = true for remote client characters, which blocks the regular mesh tick from doing a fresh TickPose that would recapture the correct transform.
The fix (requires a C++ CMC subclass)
Unfortunately there’s no Blueprint-only workaround. The fix has two parts:
1. Custom CMC — override TickCharacterPose to skip for listen server remote clients
void UMyCharacterMovementComponent::TickCharacterPose(float DeltaTime)
{
// Skip autonomous pose tick on listen server for remote clients.
// Defers animation evaluation to the regular mesh tick, which runs
// AFTER SmoothClientPosition has settled the capsule rotation.
if (IsNetMode(NM_ListenServer)
&& CharacterOwner
&& !CharacterOwner->bClientUpdating
&& !CharacterOwner->IsPlayingRootMotion()
&& CharacterOwner->GetRemoteRole() == ROLE_AutonomousProxy
&& !CharacterOwner->IsLocallyControlled())
{
return;
}
Super::TickCharacterPose(DeltaTime);
}
2. Character class — clear bOnlyAllowAutonomousTickPose in PossessedBy
void AMyCharacter::PossessedBy(AController* NewController)
{
Super::PossessedBy(NewController);
if (IsNetMode(NM_ListenServer)
&& GetRemoteRole() == ROLE_AutonomousProxy
&& !IsLocallyControlled())
{
if (USkeletalMeshComponent* MeshComp = GetMesh())
{
MeshComp->bOnlyAllowAutonomousTickPose = false;
}
}
}
3. Wire the custom CMC into your character constructor
AMyCharacter::AMyCharacter(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer.SetDefaultSubobjectClass<UMyCharacterMovementComponent>(
ACharacter::CharacterMovementComponentName))
Both parts are needed. Without the skip, the stale proxy cache persists. Without clearing the flag, the mesh tick can’t re-evaluate animation.
Side effects
Minimal. The conditions mirror the base engine’s own gate for calling TickCharacterPose, so root motion, client correction replay, standalone, and dedicated servers are all unaffected. Server-side anim notifies still fire in the same frame, just slightly later in the tick order.
Bug report submitted to Epic. The node is marked experimental so who knows if/when this gets addressed upstream, but the workaround seems to be holding.
Hope this saves someone else the deep-dive.