Client-side smoothing for replicated body rotation

Server-only AddActorWorldRotation (HasAuthority guard) prevents
client/server tug-of-war. Client interpolates toward replicated
rotation via SmoothedBodyYaw (angle-aware FInterpTo at 3x body
speed) to eliminate step artifacts from ~30Hz network updates.
Cascade (DeltaYaw, head, eyes) uses SmoothedBodyYaw on all machines.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
j.foucher 2026-03-03 11:23:44 +01:00
parent 7f92dcab51
commit b5c52c9236
2 changed files with 31 additions and 6 deletions

View File

@ -99,6 +99,7 @@ void UPS_AI_ConvAgent_PostureComponent::BeginPlay()
// Apply the mesh forward offset so "neutral" aligns with where the face points.
OriginalActorYaw = Owner->GetActorRotation().Yaw + MeshForwardYawOffset;
TargetBodyWorldYaw = Owner->GetActorRotation().Yaw;
SmoothedBodyYaw = Owner->GetActorRotation().Yaw;
if (bDebug)
{
@ -179,6 +180,7 @@ void UPS_AI_ConvAgent_PostureComponent::ResetBodyTarget()
if (AActor* Owner = GetOwner())
{
TargetBodyWorldYaw = Owner->GetActorRotation().Yaw;
SmoothedBodyYaw = Owner->GetActorRotation().Yaw;
}
}
@ -328,9 +330,10 @@ void UPS_AI_ConvAgent_PostureComponent::TickComponent(
TargetWorldYaw = HorizontalDir.Rotation().Yaw;
}
// Body smoothly interpolates toward its persistent target
// (only when body tracking is enabled — otherwise only head+eyes move).
if (bEnableBodyTracking)
// Body smoothly interpolates toward its persistent target.
// Server/standalone: directly rotates the actor.
// Client: does NOT rotate — accepts replicated rotation from server.
if (bEnableBodyTracking && Owner->HasAuthority())
{
const float BodyDelta = FMath::FindDeltaAngleDegrees(
Owner->GetActorRotation().Yaw, TargetBodyWorldYaw);
@ -341,12 +344,26 @@ void UPS_AI_ConvAgent_PostureComponent::TickComponent(
}
}
// ── Smoothed body yaw for cascade computations ──────────────────
// Server: direct copy (no lag). Client: interpolate toward replicated
// rotation to avoid step artifacts from discrete ~30Hz network updates.
if (Owner->HasAuthority())
{
SmoothedBodyYaw = Owner->GetActorRotation().Yaw;
}
else
{
const float ReplicatedYaw = Owner->GetActorRotation().Yaw;
const float ClientDelta = FMath::FindDeltaAngleDegrees(SmoothedBodyYaw, ReplicatedYaw);
SmoothedBodyYaw += FMath::FInterpTo(0.0f, ClientDelta, SafeDeltaTime, BodyInterpSpeed * 3.0f);
}
// ── 3. Compute DeltaYaw after body interp ──────────────────────────
float DeltaYaw = 0.0f;
if (!HorizontalDir.IsNearlyZero(1.0f))
{
const float CurrentFacingYaw = Owner->GetActorRotation().Yaw + MeshForwardYawOffset;
const float CurrentFacingYaw = SmoothedBodyYaw + MeshForwardYawOffset;
DeltaYaw = FMath::FindDeltaAngleDegrees(CurrentFacingYaw, TargetWorldYaw);
}
@ -432,6 +449,7 @@ void UPS_AI_ConvAgent_PostureComponent::TickComponent(
// Body: keep current orientation — don't rotate back to original facing.
// The body stays wherever the last tracking left it; only head and eyes reset.
SmoothedBodyYaw = Owner->GetActorRotation().Yaw;
// Head + Eyes: return to center
TargetHeadYaw = 0.0f;
@ -531,20 +549,21 @@ void UPS_AI_ConvAgent_PostureComponent::TickComponent(
DebugFrameCounter++;
if (DebugFrameCounter % 120 == 0)
{
const float FacingYaw = Owner->GetActorRotation().Yaw + MeshForwardYawOffset;
const float FacingYaw = SmoothedBodyYaw + MeshForwardYawOffset;
const FVector TP = TargetActor->GetActorLocation() + TargetOffset;
const FVector Dir = TP - Owner->GetActorLocation();
const float TgtYaw = FVector(Dir.X, Dir.Y, 0.0f).Rotation().Yaw;
const float Delta = FMath::FindDeltaAngleDegrees(FacingYaw, TgtYaw);
UE_LOG(LogPS_AI_ConvAgent_Posture, Log,
TEXT("Posture [%s -> %s]: Delta=%.1f | Head=%.1f/%.1f | Eyes=%.1f/%.1f | Body: enabled=%s TargetYaw=%.1f ActorYaw=%.1f"),
TEXT("Posture [%s -> %s]: Delta=%.1f | Head=%.1f/%.1f | Eyes=%.1f/%.1f | Body: enabled=%s TargetYaw=%.1f SmoothedYaw=%.1f (raw=%.1f)"),
*Owner->GetName(), *TargetActor->GetName(),
Delta,
CurrentHeadYaw, CurrentHeadPitch,
CurrentEyeYaw, CurrentEyePitch,
bEnableBodyTracking ? TEXT("Y") : TEXT("N"),
TargetBodyWorldYaw,
SmoothedBodyYaw,
Owner->GetActorRotation().Yaw);
}
}

View File

@ -321,6 +321,12 @@ private:
* Same sticky pattern as TargetHeadYaw but for the body layer. */
float TargetBodyWorldYaw = 0.0f;
/** Smoothed body yaw for cascade computations.
* On server/standalone: mirrors Owner->GetActorRotation().Yaw directly.
* On client: interpolates toward the replicated rotation to avoid
* step artifacts from discrete network updates (~30Hz replication). */
float SmoothedBodyYaw = 0.0f;
/** Original actor yaw at BeginPlay (for neutral return when TargetActor is null). */
float OriginalActorYaw = 0.0f;