|
|
|
|
@@ -405,11 +405,7 @@ void UPS_AI_ConvAgent_GazeComponent::TickComponent(
|
|
|
|
|
|
|
|
|
|
const FVector ToTarget = TargetPos - EyeOrigin;
|
|
|
|
|
|
|
|
|
|
// ── 2. Body: smooth interp toward sticky target ────────────────────
|
|
|
|
|
//
|
|
|
|
|
// TargetBodyWorldYaw is persistent — only updated when head+eyes
|
|
|
|
|
// can't reach the target. Same sticky pattern as TargetHeadYaw.
|
|
|
|
|
|
|
|
|
|
// ── 1b. Horizontal direction / world yaw to the target ─────────────
|
|
|
|
|
const FVector HorizontalDir = FVector(ToTarget.X, ToTarget.Y, 0.0f);
|
|
|
|
|
float TargetWorldYaw = 0.0f;
|
|
|
|
|
if (!HorizontalDir.IsNearlyZero(1.0f))
|
|
|
|
|
@@ -417,11 +413,21 @@ void UPS_AI_ConvAgent_GazeComponent::TickComponent(
|
|
|
|
|
TargetWorldYaw = HorizontalDir.Rotation().Yaw;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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())
|
|
|
|
|
// ── 2. BODY (authority only): lazy turn ────────────────────────────
|
|
|
|
|
// Rotate the actor to face the target only once head+eyes can no longer
|
|
|
|
|
// cover it. Clients NEVER rotate the body — they receive it replicated.
|
|
|
|
|
// Body turn state (TargetBodyWorldYaw) lives on the authority only.
|
|
|
|
|
if (bEnableBodyTracking && Owner->HasAuthority() && !HorizontalDir.IsNearlyZero(1.0f))
|
|
|
|
|
{
|
|
|
|
|
const float BodyFacingYaw = Owner->GetActorRotation().Yaw + MeshForwardYawOffset;
|
|
|
|
|
const float DeltaToTarget = FMath::FindDeltaAngleDegrees(BodyFacingYaw, TargetWorldYaw);
|
|
|
|
|
|
|
|
|
|
// Commit to facing the target once it leaves the head+eyes range.
|
|
|
|
|
if (FMath::Abs(DeltaToTarget) > MaxHeadYaw + MaxEyeHorizontal)
|
|
|
|
|
{
|
|
|
|
|
TargetBodyWorldYaw = TargetWorldYaw - MeshForwardYawOffset;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const float BodyDelta = FMath::FindDeltaAngleDegrees(
|
|
|
|
|
Owner->GetActorRotation().Yaw, TargetBodyWorldYaw);
|
|
|
|
|
if (FMath::Abs(BodyDelta) > 0.1f)
|
|
|
|
|
@@ -431,122 +437,49 @@ void UPS_AI_ConvAgent_GazeComponent::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.
|
|
|
|
|
// The client also overrides the actor's rotation with the smoothed value
|
|
|
|
|
// so the body mesh doesn't visually jump at network tick rate.
|
|
|
|
|
if (Owner->HasAuthority())
|
|
|
|
|
// Smoothed mirror of the (authoritative/replicated) body yaw — DEBUG
|
|
|
|
|
// only. NEVER written back to the actor: the old client override
|
|
|
|
|
// (SetActorRotation) corrupted the head input and made the client head
|
|
|
|
|
// diverge by up to MaxHeadYaw. The head reads the real actor yaw below.
|
|
|
|
|
{
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
// Override actor rotation with smoothed value so the body mesh
|
|
|
|
|
// shows the interpolated rotation instead of the raw replicated steps.
|
|
|
|
|
// This is local-only — replication will overwrite on next network update,
|
|
|
|
|
// and SmoothedBodyYaw will absorb the correction smoothly.
|
|
|
|
|
FRotator SmoothedRot = Owner->GetActorRotation();
|
|
|
|
|
SmoothedRot.Yaw = SmoothedBodyYaw;
|
|
|
|
|
Owner->SetActorRotation(SmoothedRot);
|
|
|
|
|
const float ActorYaw = Owner->GetActorRotation().Yaw;
|
|
|
|
|
const float YawDelta = FMath::FindDeltaAngleDegrees(SmoothedBodyYaw, ActorYaw);
|
|
|
|
|
SmoothedBodyYaw += FMath::FInterpTo(0.0f, YawDelta, SafeDeltaTime, BodyInterpSpeed * 3.0f);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── 3. Compute DeltaYaw after body interp ──────────────────────────
|
|
|
|
|
// When body tracking is disabled (e.g. proximity gaze during spline patrol),
|
|
|
|
|
// sync SmoothedBodyYaw and TargetBodyWorldYaw to current actor rotation.
|
|
|
|
|
// This ensures the head offset is always relative to the actual body facing,
|
|
|
|
|
// not a stale cached value from a previous frame.
|
|
|
|
|
// When body tracking is off, keep the body target synced (debug only).
|
|
|
|
|
if (!bEnableBodyTracking)
|
|
|
|
|
{
|
|
|
|
|
SmoothedBodyYaw = Owner->GetActorRotation().Yaw;
|
|
|
|
|
TargetBodyWorldYaw = SmoothedBodyYaw;
|
|
|
|
|
TargetBodyWorldYaw = Owner->GetActorRotation().Yaw;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── 3. HEAD + EYES — STATELESS cascade ─────────────────────────────
|
|
|
|
|
// Pure function of the ACTUAL body facing (authoritative on the server,
|
|
|
|
|
// replicated on the client) and the target direction. No sticky /
|
|
|
|
|
// path-dependent state, so server and client converge to the same pose
|
|
|
|
|
// regardless of how the body rotation arrived (smooth vs ~30Hz steps).
|
|
|
|
|
// FInterpTo only eases the visual; it never latches.
|
|
|
|
|
const float BodyFacingYaw = Owner->GetActorRotation().Yaw + MeshForwardYawOffset;
|
|
|
|
|
|
|
|
|
|
float DeltaYaw = 0.0f;
|
|
|
|
|
if (!HorizontalDir.IsNearlyZero(1.0f))
|
|
|
|
|
{
|
|
|
|
|
const float CurrentFacingYaw = SmoothedBodyYaw + MeshForwardYawOffset;
|
|
|
|
|
DeltaYaw = FMath::FindDeltaAngleDegrees(CurrentFacingYaw, TargetWorldYaw);
|
|
|
|
|
DeltaYaw = FMath::FindDeltaAngleDegrees(BodyFacingYaw, TargetWorldYaw);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── 4. Pitch from 3D direction ─────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
const float HorizontalDist = HorizontalDir.Size();
|
|
|
|
|
const float TargetPitch = (HorizontalDist > 1.0f)
|
|
|
|
|
? FMath::RadiansToDegrees(FMath::Atan2(ToTarget.Z, HorizontalDist))
|
|
|
|
|
: 0.0f;
|
|
|
|
|
|
|
|
|
|
// ── 5. BODY overflow: check against persistent TargetBodyWorldYaw ──
|
|
|
|
|
//
|
|
|
|
|
// Same pattern as head: check from body's TARGET position (not current).
|
|
|
|
|
// Body triggers when head+eyes combined range is exceeded.
|
|
|
|
|
|
|
|
|
|
const float BodyTargetFacing = TargetBodyWorldYaw + MeshForwardYawOffset;
|
|
|
|
|
const float DeltaFromBodyTarget = FMath::FindDeltaAngleDegrees(
|
|
|
|
|
BodyTargetFacing, TargetWorldYaw);
|
|
|
|
|
|
|
|
|
|
bool bBodyOverflowed = false;
|
|
|
|
|
if (bEnableBodyTracking && FMath::Abs(DeltaFromBodyTarget) > MaxHeadYaw + MaxEyeHorizontal)
|
|
|
|
|
{
|
|
|
|
|
// Body realigns to face target
|
|
|
|
|
TargetBodyWorldYaw = TargetWorldYaw - MeshForwardYawOffset;
|
|
|
|
|
// Head returns to ~0° since body will face target directly
|
|
|
|
|
TargetHeadYaw = 0.0f;
|
|
|
|
|
bBodyOverflowed = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── 6. HEAD: realign when eyes overflow (check against body TARGET) ──
|
|
|
|
|
//
|
|
|
|
|
// Head overflow is checked relative to where the BODY IS GOING
|
|
|
|
|
// (TargetBodyWorldYaw), not where it currently is. This prevents
|
|
|
|
|
// the head from overcompensating during body interpolation —
|
|
|
|
|
// otherwise the head turns to track while body catches up, then
|
|
|
|
|
// snaps back when body arrives (two-step animation artifact).
|
|
|
|
|
//
|
|
|
|
|
// GUARD: Skip eye overflow check when body just overflowed in this
|
|
|
|
|
// same frame. Body overflow already set TargetHeadYaw=0 (head will
|
|
|
|
|
// recenter as body turns). Allowing eye overflow to also fire would
|
|
|
|
|
// snap TargetHeadYaw to a second value in the same frame, causing
|
|
|
|
|
// a visible 1-2 frame jerk as FInterpTo chases two targets.
|
|
|
|
|
|
|
|
|
|
if (!bBodyOverflowed)
|
|
|
|
|
{
|
|
|
|
|
const float HeadDeltaYaw = FMath::FindDeltaAngleDegrees(
|
|
|
|
|
TargetBodyWorldYaw + MeshForwardYawOffset, TargetWorldYaw);
|
|
|
|
|
|
|
|
|
|
const float EyeDeltaYaw = HeadDeltaYaw - TargetHeadYaw;
|
|
|
|
|
|
|
|
|
|
if (FMath::Abs(EyeDeltaYaw) > MaxEyeHorizontal)
|
|
|
|
|
{
|
|
|
|
|
TargetHeadYaw = FMath::Clamp(HeadDeltaYaw, -MaxHeadYaw, MaxHeadYaw);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Head smoothly interpolates toward its persistent target
|
|
|
|
|
// Head takes as much of the yaw gap as its range allows; eyes the rest.
|
|
|
|
|
// (TargetHeadYaw/Pitch are kept only as debug mirrors — no longer sticky.)
|
|
|
|
|
TargetHeadYaw = FMath::Clamp(DeltaYaw, -MaxHeadYaw, MaxHeadYaw);
|
|
|
|
|
CurrentHeadYaw = FMath::FInterpTo(CurrentHeadYaw, TargetHeadYaw, SafeDeltaTime, HeadInterpSpeed);
|
|
|
|
|
|
|
|
|
|
// Eyes = remaining gap (during transients, eyes may sit at MaxEye while
|
|
|
|
|
// the head catches up — ARKit normalization keeps visual deflection small)
|
|
|
|
|
CurrentEyeYaw = FMath::Clamp(DeltaYaw - CurrentHeadYaw, -MaxEyeHorizontal, MaxEyeHorizontal);
|
|
|
|
|
|
|
|
|
|
// ── 5. PITCH: relative cascade (Eyes → Head, no body pitch) ────────
|
|
|
|
|
|
|
|
|
|
// Same pattern: check against persistent TargetHeadPitch
|
|
|
|
|
const float EyeDeltaPitch = TargetPitch - TargetHeadPitch;
|
|
|
|
|
|
|
|
|
|
if (FMath::Abs(EyeDeltaPitch) > MaxEyeVertical)
|
|
|
|
|
{
|
|
|
|
|
// Eyes overflow → head realigns toward target pitch
|
|
|
|
|
TargetHeadPitch = FMath::Clamp(TargetPitch, -MaxHeadPitch, MaxHeadPitch);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
CurrentHeadPitch = FMath::FInterpTo(CurrentHeadPitch, TargetHeadPitch, SafeDeltaTime, HeadInterpSpeed);
|
|
|
|
|
|
|
|
|
|
// Eyes = remaining pitch gap
|
|
|
|
|
CurrentEyePitch = FMath::Clamp(TargetPitch - CurrentHeadPitch, -MaxEyeVertical, MaxEyeVertical);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
|