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. // Apply the mesh forward offset so "neutral" aligns with where the face points.
OriginalActorYaw = Owner->GetActorRotation().Yaw + MeshForwardYawOffset; OriginalActorYaw = Owner->GetActorRotation().Yaw + MeshForwardYawOffset;
TargetBodyWorldYaw = Owner->GetActorRotation().Yaw; TargetBodyWorldYaw = Owner->GetActorRotation().Yaw;
SmoothedBodyYaw = Owner->GetActorRotation().Yaw;
if (bDebug) if (bDebug)
{ {
@ -179,6 +180,7 @@ void UPS_AI_ConvAgent_PostureComponent::ResetBodyTarget()
if (AActor* Owner = GetOwner()) if (AActor* Owner = GetOwner())
{ {
TargetBodyWorldYaw = Owner->GetActorRotation().Yaw; TargetBodyWorldYaw = Owner->GetActorRotation().Yaw;
SmoothedBodyYaw = Owner->GetActorRotation().Yaw;
} }
} }
@ -328,9 +330,10 @@ void UPS_AI_ConvAgent_PostureComponent::TickComponent(
TargetWorldYaw = HorizontalDir.Rotation().Yaw; TargetWorldYaw = HorizontalDir.Rotation().Yaw;
} }
// Body smoothly interpolates toward its persistent target // Body smoothly interpolates toward its persistent target.
// (only when body tracking is enabled — otherwise only head+eyes move). // Server/standalone: directly rotates the actor.
if (bEnableBodyTracking) // Client: does NOT rotate — accepts replicated rotation from server.
if (bEnableBodyTracking && Owner->HasAuthority())
{ {
const float BodyDelta = FMath::FindDeltaAngleDegrees( const float BodyDelta = FMath::FindDeltaAngleDegrees(
Owner->GetActorRotation().Yaw, TargetBodyWorldYaw); 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 ────────────────────────── // ── 3. Compute DeltaYaw after body interp ──────────────────────────
float DeltaYaw = 0.0f; float DeltaYaw = 0.0f;
if (!HorizontalDir.IsNearlyZero(1.0f)) if (!HorizontalDir.IsNearlyZero(1.0f))
{ {
const float CurrentFacingYaw = Owner->GetActorRotation().Yaw + MeshForwardYawOffset; const float CurrentFacingYaw = SmoothedBodyYaw + MeshForwardYawOffset;
DeltaYaw = FMath::FindDeltaAngleDegrees(CurrentFacingYaw, TargetWorldYaw); 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. // 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. // The body stays wherever the last tracking left it; only head and eyes reset.
SmoothedBodyYaw = Owner->GetActorRotation().Yaw;
// Head + Eyes: return to center // Head + Eyes: return to center
TargetHeadYaw = 0.0f; TargetHeadYaw = 0.0f;
@ -531,20 +549,21 @@ void UPS_AI_ConvAgent_PostureComponent::TickComponent(
DebugFrameCounter++; DebugFrameCounter++;
if (DebugFrameCounter % 120 == 0) if (DebugFrameCounter % 120 == 0)
{ {
const float FacingYaw = Owner->GetActorRotation().Yaw + MeshForwardYawOffset; const float FacingYaw = SmoothedBodyYaw + MeshForwardYawOffset;
const FVector TP = TargetActor->GetActorLocation() + TargetOffset; const FVector TP = TargetActor->GetActorLocation() + TargetOffset;
const FVector Dir = TP - Owner->GetActorLocation(); const FVector Dir = TP - Owner->GetActorLocation();
const float TgtYaw = FVector(Dir.X, Dir.Y, 0.0f).Rotation().Yaw; const float TgtYaw = FVector(Dir.X, Dir.Y, 0.0f).Rotation().Yaw;
const float Delta = FMath::FindDeltaAngleDegrees(FacingYaw, TgtYaw); const float Delta = FMath::FindDeltaAngleDegrees(FacingYaw, TgtYaw);
UE_LOG(LogPS_AI_ConvAgent_Posture, Log, 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(), *Owner->GetName(), *TargetActor->GetName(),
Delta, Delta,
CurrentHeadYaw, CurrentHeadPitch, CurrentHeadYaw, CurrentHeadPitch,
CurrentEyeYaw, CurrentEyePitch, CurrentEyeYaw, CurrentEyePitch,
bEnableBodyTracking ? TEXT("Y") : TEXT("N"), bEnableBodyTracking ? TEXT("Y") : TEXT("N"),
TargetBodyWorldYaw, TargetBodyWorldYaw,
SmoothedBodyYaw,
Owner->GetActorRotation().Yaw); Owner->GetActorRotation().Yaw);
} }
} }

View File

@ -321,6 +321,12 @@ private:
* Same sticky pattern as TargetHeadYaw but for the body layer. */ * Same sticky pattern as TargetHeadYaw but for the body layer. */
float TargetBodyWorldYaw = 0.0f; 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). */ /** Original actor yaw at BeginPlay (for neutral return when TargetActor is null). */
float OriginalActorYaw = 0.0f; float OriginalActorYaw = 0.0f;