Unreal 5.7 Mover: Crouching teleports the capsule above/below the ground

Hi there!

After updating to 5.7 we noticed some issues with crouching.

I was able to confirm the same issues present themselves also in the contents of the “Mover Examples” Plugin when used on an empty project in an unmodified engine.

At full speed it shows as a small glitch in the camera movement but slowing down the game speed using the slomo cvar or using the rewind debugger it will be pretty clear that when the crouch is toggled the character capsule (and mesh) does the following:

  • c1. capsule teleports up : feet in the air
  • c2. capsule teleports down : feet in below the ground
  • c3. no capsule movement : feet align to the ground

Similarly when un-crouching the situation appears to present itself mirrored

  • u1. capsule teleports down : feet below the ground
  • u2. capsule teleports up : feet above the ground
  • u3. no capsule movement : feet align to the ground

In the attachment a screenshot of each of the above “moments” in the flow captured using the rewind debugger. Each image is named with the matching name above (i.e., c123, u123).

I have not dug deep in the code flow but after a superficial analysis I think that this might be related to the changes to how FStanceModifiers are applied: it seemed to me this was the only relevant change to the crouching flow between 5.6 and 5.7

c123.png(7.53 MB)
u123.png(7.41 MB)

Steps to Reproduce
- Launch Unreal Engine 5.7 from the Epic Launcher

- Create an empty project

- Enable “Mover Examples” Plugin

- Restart the editor

- Load “L_CharacterMovementBasics” map included in the “Mover Examples” Plugin

- Start the map

- Switch to the extended pawn (green) by walking to the platform labelled “Swap for extended Pawn”

- Press C to toggle crouch

Hey Filipe,

Thanks for the bug report! Seeing this on my side too and I’m fairly certain it is coming from a delay in when we adjust the capsule size/visual component and when we teleport the capsule to account for the different sizing (since teleporting the character gets queued with an instant effect). I went ahead and made a ticket for it and will be looking into a fix! If you need a workaround, doing all of the adjustments (capsule size, visual component offset, and capsule location) at the same time in a new instant effect that gets queued from the stance modifier should fix this.

Thanks,

Nate

Hey Nate,

Thanks for your reply, using an instant effect that first resizes the capsule and then teleports it (basically moving the FStanceModifier AdjustCapsule method into the ApplyInstantEffect) fixed the issue!

I also thought about experimenting with independent instant effects queued in order (one for resizing, the other, already existing, for teleporting) but as I plan to scrap the code I wrote once your fix will be live I didn’t explore that direction.

Thanks again,

Filipe

Would you mind sharing some pseudocode for your solution? I tried doing it in a single Effect, but still get jitter teleport on proxies.

bool FStanceAdjustmentEffect::ApplyMovementEffect(FApplyMovementEffectParams& ApplyEffectParams, FMoverSyncState& OutputState)
{
USceneComponent* UpdatedComponent = ApplyEffectParams.UpdatedComponent;
UCapsuleComponent* CapsuleComponent = Cast<UCapsuleComponent>(UpdatedComponent);

if (!CapsuleComponent)
{
return false;
}

AActor* OwnerActor = UpdatedComponent->GetOwner();
if (!OwnerActor)
{
return false;
}

// Resize the capsule
CapsuleComponent->SetCapsuleSize(CapsuleComponent->GetUnscaledCapsuleRadius(), NewCapsuleHalfHeight);

// Update eye height on pawn
if (APawn* OwnerAsPawn = Cast<APawn>(OwnerActor))
{
OwnerAsPawn->BaseEyeHeight = NewEyeHeight;
}

// Calculate the offset for the capsule adjustment
const float HalfHeightDifference = FMath::Abs(NewCapsuleHalfHeight - OldCapsuleHalfHeight);
FVector CapsuleOffset = ApplyEffectParams.MoverComp->GetUpDirection() * (bExpanding ? HalfHeightDifference : -HalfHeightDifference);

// Get the current location and calculate target location with offset
FVector TargetLocation = UpdatedComponent->GetComponentLocation() + CapsuleOffset;

// Teleport to the target location
if (OwnerActor->TeleportTo(TargetLocation, UpdatedComponent->GetComponentRotation()))
{
const FVector UpdatedLocation = UpdatedComponent->GetComponentLocation();

// Update the sync state with the new location and rotation
FMoverDefaultSyncState& OutputSyncState = OutputState.SyncStateCollection.FindOrAddMutableDataByType<FMoverDefaultSyncState>();

// Preserve the velocity from the current state
FVector CurrentVelocity = FVector::ZeroVector;
if (const FMoverDefaultSyncState* CurrentSyncState = ApplyEffectParams.StartState->SyncState.SyncStateCollection.FindDataByType<FMoverDefaultSyncState>())
{
CurrentVelocity = CurrentSyncState->GetVelocity_WorldSpace();
}

OutputSyncState.SetTransforms_WorldSpace(UpdatedLocation,
UpdatedComponent->GetComponentRotation(),
CurrentVelocity,
FVector::ZeroVector,
nullptr); // no movement base

// Invalidate the floor result since we've moved
if (UMoverBlackboard* SimBlackboard = ApplyEffectParams.MoverComp->GetSimBlackboard_Mutable())
{
SimBlackboard->Invalidate(CommonBlackboard::LastFloorResult);
SimBlackboard->Invalidate(CommonBlackboard::LastFoundDynamicMovementBase);
}

return true;
}

return false;
}