Add gaze bridge: behavior-driven look-at via ConvAgent GazeComponent
New BTService_UpdateGaze bridges PS_AI_Behavior to PS_AI_ConvAgent's GazeComponent via runtime reflection (zero compile dependency). Priority system: - Combat/TakingCover: gaze disabled (aim animation handles it) - Alerted: look at ThreatActor (head + eyes, no body) - Conversation: skip (ConvAgent manages) - Proximity: glance at nearest perceived actor within radius Proximity gaze features: - Lock on target until release (no jumping) - Configurable duration, cooldown, radius on AIController - Front-facing only (dot product filter) - Skip spectators and hostile actors GazeComponent fix: - Sync SmoothedBodyYaw to actual actor rotation when body tracking is disabled, preventing stale head offset during spline movement Files: - New: BTService_UpdateGaze.h/.cpp - Modified: AIController.h/.cpp (gaze bridge, config, bind, cleanup) - Modified: GazeComponent.cpp (body yaw sync fix) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d471714fbd
commit
d9fb46bc96
@ -0,0 +1,28 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#include "BT/PS_AI_Behavior_BTService_UpdateGaze.h"
|
||||
#include "PS_AI_Behavior_AIController.h"
|
||||
|
||||
UPS_AI_Behavior_BTService_UpdateGaze::UPS_AI_Behavior_BTService_UpdateGaze()
|
||||
{
|
||||
NodeName = TEXT("Update Gaze");
|
||||
Interval = 0.3f;
|
||||
RandomDeviation = 0.05f;
|
||||
}
|
||||
|
||||
void UPS_AI_Behavior_BTService_UpdateGaze::TickNode(
|
||||
UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
|
||||
{
|
||||
Super::TickNode(OwnerComp, NodeMemory, DeltaSeconds);
|
||||
|
||||
APS_AI_Behavior_AIController* AIC = Cast<APS_AI_Behavior_AIController>(OwnerComp.GetAIOwner());
|
||||
if (AIC)
|
||||
{
|
||||
AIC->UpdateGazeTarget(DeltaSeconds);
|
||||
}
|
||||
}
|
||||
|
||||
FString UPS_AI_Behavior_BTService_UpdateGaze::GetStaticDescription() const
|
||||
{
|
||||
return TEXT("Updates gaze target from behavior state + proximity.\nBridges to PS_AI_ConvAgent GazeComponent (if present).");
|
||||
}
|
||||
@ -18,6 +18,8 @@
|
||||
#include "BehaviorTree/Blackboard/BlackboardKeyType_Vector.h"
|
||||
#include "BehaviorTree/Blackboard/BlackboardKeyType_String.h"
|
||||
#include "BehaviorTree/Blackboard/BlackboardKeyType_Bool.h"
|
||||
#include "DrawDebugHelpers.h"
|
||||
#include "GameFramework/SpectatorPawn.h"
|
||||
|
||||
APS_AI_Behavior_AIController::APS_AI_Behavior_AIController()
|
||||
{
|
||||
@ -80,6 +82,7 @@ void APS_AI_Behavior_AIController::OnPossess(APawn* InPawn)
|
||||
SetupBlackboard();
|
||||
StartBehavior();
|
||||
TryBindConversationAgent();
|
||||
TryBindGazeComponent();
|
||||
|
||||
UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] Possessed Pawn '%s' — BT started, TeamId=%d."),
|
||||
*GetName(), *InPawn->GetName(), TeamId);
|
||||
@ -94,6 +97,11 @@ void APS_AI_Behavior_AIController::OnUnPossess()
|
||||
Brain->StopLogic(TEXT("Unpossessed"));
|
||||
}
|
||||
|
||||
ClearGazeTarget();
|
||||
CachedGazeComponent = nullptr;
|
||||
CachedConvAgentComponent = nullptr;
|
||||
ProximityGazeTarget = nullptr;
|
||||
|
||||
PersonalityComp = nullptr;
|
||||
Super::OnUnPossess();
|
||||
}
|
||||
@ -269,6 +277,9 @@ void APS_AI_Behavior_AIController::HandleDeath()
|
||||
{
|
||||
UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] HandleDeath — shutting down AI systems."), *GetName());
|
||||
|
||||
ClearGazeTarget();
|
||||
ProximityGazeTarget = nullptr;
|
||||
|
||||
// 1. Stop the Behavior Tree (no more services, tasks, or decorators)
|
||||
if (UBrainComponent* Brain = GetBrainComponent())
|
||||
{
|
||||
@ -477,3 +488,272 @@ void APS_AI_Behavior_AIController::TryBindConversationAgent()
|
||||
*GetName());
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Gaze Bridge ────────────────────────────────────────────────────────────
|
||||
|
||||
void APS_AI_Behavior_AIController::TryBindGazeComponent()
|
||||
{
|
||||
APawn* MyPawn = GetPawn();
|
||||
if (!MyPawn) return;
|
||||
|
||||
// Soft lookup via reflection — no compile dependency on PS_AI_ConvAgent
|
||||
static UClass* GazeClass = nullptr;
|
||||
if (!GazeClass)
|
||||
{
|
||||
GazeClass = LoadClass<UActorComponent>(nullptr,
|
||||
TEXT("/Script/PS_AI_ConvAgent.PS_AI_ConvAgent_GazeComponent"));
|
||||
}
|
||||
if (!GazeClass) return; // PS_AI_ConvAgent plugin not loaded
|
||||
|
||||
UActorComponent* GazeComp = MyPawn->FindComponentByClass(GazeClass);
|
||||
if (!GazeComp) return;
|
||||
|
||||
CachedGazeComponent = GazeComp;
|
||||
|
||||
// Cache property pointers
|
||||
GazeProp_TargetActor = CastField<FObjectProperty>(
|
||||
GazeClass->FindPropertyByName(TEXT("TargetActor")));
|
||||
GazeProp_bActive = CastField<FBoolProperty>(
|
||||
GazeClass->FindPropertyByName(TEXT("bActive")));
|
||||
GazeProp_bEnableBodyTracking = CastField<FBoolProperty>(
|
||||
GazeClass->FindPropertyByName(TEXT("bEnableBodyTracking")));
|
||||
|
||||
// Also cache the ElevenLabsComponent for conversation detection
|
||||
static UClass* ConvClass = nullptr;
|
||||
if (!ConvClass)
|
||||
{
|
||||
ConvClass = LoadClass<UActorComponent>(nullptr,
|
||||
TEXT("/Script/PS_AI_ConvAgent.PS_AI_ConvAgent_ElevenLabsComponent"));
|
||||
}
|
||||
if (ConvClass)
|
||||
{
|
||||
UActorComponent* ConvComp = MyPawn->FindComponentByClass(ConvClass);
|
||||
if (ConvComp)
|
||||
{
|
||||
CachedConvAgentComponent = ConvComp;
|
||||
ConvProp_bNetIsConversing = CastField<FBoolProperty>(
|
||||
ConvClass->FindPropertyByName(TEXT("bNetIsConversing")));
|
||||
}
|
||||
}
|
||||
|
||||
UE_LOG(LogPS_AI_Behavior, Log,
|
||||
TEXT("[%s] Gaze bridge bound: TargetActor=%s, BodyTracking=%s, Conversation=%s"),
|
||||
*GetName(),
|
||||
GazeProp_TargetActor ? TEXT("OK") : TEXT("MISS"),
|
||||
GazeProp_bEnableBodyTracking ? TEXT("OK") : TEXT("MISS"),
|
||||
ConvProp_bNetIsConversing ? TEXT("OK") : TEXT("N/A"));
|
||||
}
|
||||
|
||||
void APS_AI_Behavior_AIController::SetGazeTarget(AActor* Target, bool bEnableBody)
|
||||
{
|
||||
UActorComponent* GazeComp = CachedGazeComponent.Get();
|
||||
if (!GazeComp) return;
|
||||
|
||||
if (GazeProp_TargetActor)
|
||||
{
|
||||
GazeProp_TargetActor->SetObjectPropertyValue_InContainer(GazeComp, Target);
|
||||
}
|
||||
if (GazeProp_bActive)
|
||||
{
|
||||
GazeProp_bActive->SetPropertyValue_InContainer(GazeComp, true);
|
||||
}
|
||||
if (GazeProp_bEnableBodyTracking)
|
||||
{
|
||||
GazeProp_bEnableBodyTracking->SetPropertyValue_InContainer(GazeComp, bEnableBody);
|
||||
}
|
||||
|
||||
UE_LOG(LogPS_AI_Behavior, Verbose, TEXT("[%s] Gaze → '%s' (body=%d)"),
|
||||
*GetName(), Target ? *Target->GetName() : TEXT("null"), bEnableBody ? 1 : 0);
|
||||
}
|
||||
|
||||
void APS_AI_Behavior_AIController::ClearGazeTarget()
|
||||
{
|
||||
UActorComponent* GazeComp = CachedGazeComponent.Get();
|
||||
if (!GazeComp || !GazeProp_TargetActor) return;
|
||||
|
||||
GazeProp_TargetActor->SetObjectPropertyValue_InContainer(GazeComp, static_cast<UObject*>(nullptr));
|
||||
if (GazeProp_bEnableBodyTracking)
|
||||
{
|
||||
GazeProp_bEnableBodyTracking->SetPropertyValue_InContainer(GazeComp, false);
|
||||
}
|
||||
|
||||
UE_LOG(LogPS_AI_Behavior, Verbose, TEXT("[%s] Gaze → cleared"), *GetName());
|
||||
}
|
||||
|
||||
bool APS_AI_Behavior_AIController::IsConversationActiveOnPawn() const
|
||||
{
|
||||
UActorComponent* ConvComp = CachedConvAgentComponent.Get();
|
||||
if (!ConvComp || !ConvProp_bNetIsConversing) return false;
|
||||
|
||||
return ConvProp_bNetIsConversing->GetPropertyValue_InContainer(ConvComp);
|
||||
}
|
||||
|
||||
void APS_AI_Behavior_AIController::UpdateGazeTarget(float DeltaSeconds)
|
||||
{
|
||||
if (!CachedGazeComponent.IsValid()) return;
|
||||
|
||||
// Tick cooldown
|
||||
if (ProximityGazeCooldownTimer > 0.0f)
|
||||
{
|
||||
ProximityGazeCooldownTimer -= DeltaSeconds;
|
||||
}
|
||||
|
||||
const EPS_AI_Behavior_State CurrentState = GetBehaviorState();
|
||||
|
||||
// ── Priority 1: Combat → disable gaze (aim animation handles it) ───
|
||||
if (CurrentState == EPS_AI_Behavior_State::Combat ||
|
||||
CurrentState == EPS_AI_Behavior_State::TakingCover)
|
||||
{
|
||||
ClearGazeTarget();
|
||||
ProximityGazeTarget = nullptr;
|
||||
ProximityGazeDuration = 0.0f;
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Priority 1b: Alerted → look at ThreatActor (not shooting yet) ──
|
||||
if (CurrentState == EPS_AI_Behavior_State::Alerted)
|
||||
{
|
||||
AActor* ThreatActor = Blackboard
|
||||
? Cast<AActor>(Blackboard->GetValueAsObject(PS_AI_Behavior_BB::ThreatActor))
|
||||
: nullptr;
|
||||
|
||||
if (ThreatActor)
|
||||
{
|
||||
APawn* ThreatPawn = UPS_AI_Behavior_PerceptionComponent::FindOwningPawn(ThreatActor);
|
||||
AActor* GazeActor = ThreatPawn ? static_cast<AActor*>(ThreatPawn) : ThreatActor;
|
||||
SetGazeTarget(GazeActor, /*bEnableBody=*/ false);
|
||||
ProximityGazeTarget = nullptr;
|
||||
ProximityGazeDuration = 0.0f;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Priority 2: Conversation active → do NOT touch gaze ─────────
|
||||
if (IsConversationActiveOnPawn())
|
||||
{
|
||||
ProximityGazeTarget = nullptr;
|
||||
ProximityGazeDuration = 0.0f;
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Priority 3/4: Proximity gaze ────────────────────────────────
|
||||
if (!bEnableProximityGaze || CurrentState == EPS_AI_Behavior_State::Dead)
|
||||
{
|
||||
ClearGazeTarget();
|
||||
return;
|
||||
}
|
||||
|
||||
APawn* MyPawn = GetPawn();
|
||||
if (!MyPawn) return;
|
||||
|
||||
// Check current locked target
|
||||
AActor* LockedTarget = ProximityGazeTarget.Get();
|
||||
if (LockedTarget)
|
||||
{
|
||||
const float Dist = FVector::Dist(MyPawn->GetActorLocation(), LockedTarget->GetActorLocation());
|
||||
ProximityGazeDuration += DeltaSeconds;
|
||||
|
||||
bool bRelease = (Dist > GazeProximityRadius);
|
||||
bRelease |= (ProximityGazeDuration >= GazeMaxDuration);
|
||||
bRelease |= !IsValid(LockedTarget);
|
||||
|
||||
if (bRelease)
|
||||
{
|
||||
ProximityGazeCooldownActor = LockedTarget;
|
||||
ProximityGazeCooldownTimer = GazeCooldown;
|
||||
ProximityGazeTarget = nullptr;
|
||||
ProximityGazeDuration = 0.0f;
|
||||
ClearGazeTarget();
|
||||
return;
|
||||
}
|
||||
|
||||
return; // Still locked
|
||||
}
|
||||
|
||||
// Scan for new proximity target from perceived actors (within sight cone)
|
||||
if (!BehaviorPerception)
|
||||
{
|
||||
ClearGazeTarget();
|
||||
return;
|
||||
}
|
||||
|
||||
TArray<AActor*> PerceivedActors;
|
||||
BehaviorPerception->GetCurrentlyPerceivedActors(nullptr, PerceivedActors);
|
||||
|
||||
const FVector MyLoc = MyPawn->GetActorLocation();
|
||||
AActor* BestTarget = nullptr;
|
||||
float BestDistSq = GazeProximityRadius * GazeProximityRadius;
|
||||
|
||||
for (AActor* RawActor : PerceivedActors)
|
||||
{
|
||||
APawn* CandidatePawn = UPS_AI_Behavior_PerceptionComponent::FindOwningPawn(RawActor);
|
||||
if (!CandidatePawn || CandidatePawn == MyPawn) continue;
|
||||
if (CandidatePawn->IsA<ASpectatorPawn>()) continue;
|
||||
|
||||
// Skip actor on cooldown
|
||||
if (CandidatePawn == ProximityGazeCooldownActor.Get() && ProximityGazeCooldownTimer > 0.0f) continue;
|
||||
|
||||
const FVector CandidateLoc = CandidatePawn->GetActorLocation();
|
||||
const float DistSq = FVector::DistSquared(MyLoc, CandidateLoc);
|
||||
if (DistSq > BestDistSq) continue;
|
||||
|
||||
// Skip actors behind the NPC (only look at actors in front)
|
||||
const FVector DirToCandidate = (CandidateLoc - MyLoc).GetSafeNormal2D();
|
||||
const FVector MyForward = MyPawn->GetActorForwardVector().GetSafeNormal2D();
|
||||
if (FVector::DotProduct(MyForward, DirToCandidate) < 0.0f) continue;
|
||||
|
||||
// Skip hostile actors (handled by Priority 1)
|
||||
const ETeamAttitude::Type Attitude = GetTeamAttitudeTowards(*CandidatePawn);
|
||||
if (Attitude == ETeamAttitude::Hostile) continue;
|
||||
|
||||
BestDistSq = DistSq;
|
||||
BestTarget = CandidatePawn;
|
||||
}
|
||||
|
||||
if (BestTarget)
|
||||
{
|
||||
ProximityGazeTarget = BestTarget;
|
||||
ProximityGazeDuration = 0.0f;
|
||||
|
||||
UE_LOG(LogPS_AI_Behavior, Log,
|
||||
TEXT("[%s] Gaze proximity: target='%s' at %s, myLoc=%s, dist=%.0f"),
|
||||
*GetName(), *BestTarget->GetName(),
|
||||
*BestTarget->GetActorLocation().ToString(),
|
||||
*MyLoc.ToString(),
|
||||
FVector::Dist(MyLoc, BestTarget->GetActorLocation()));
|
||||
|
||||
SetGazeTarget(BestTarget, /*bEnableBody=*/ false); // Head + eyes only
|
||||
}
|
||||
else
|
||||
{
|
||||
ClearGazeTarget();
|
||||
}
|
||||
|
||||
#if ENABLE_DRAW_DEBUG
|
||||
// Draw gaze debug line if PersonalityComponent has bDebug enabled
|
||||
if (PersonalityComp && PersonalityComp->bDebug)
|
||||
{
|
||||
AActor* GazeTarget = ProximityGazeTarget.Get();
|
||||
// In combat, the gaze target is the ThreatActor
|
||||
if (!GazeTarget && Blackboard &&
|
||||
(CurrentState == EPS_AI_Behavior_State::Combat ||
|
||||
CurrentState == EPS_AI_Behavior_State::Alerted ||
|
||||
CurrentState == EPS_AI_Behavior_State::TakingCover))
|
||||
{
|
||||
GazeTarget = Cast<AActor>(Blackboard->GetValueAsObject(PS_AI_Behavior_BB::ThreatActor));
|
||||
}
|
||||
|
||||
if (GazeTarget)
|
||||
{
|
||||
const FVector HeadLoc = MyPawn->GetActorLocation() + FVector(0, 0, 160.0f);
|
||||
const FVector TargetLoc = GazeTarget->GetActorLocation() + FVector(0, 0, 60.0f);
|
||||
DrawDebugLine(GetWorld(), HeadLoc, TargetLoc,
|
||||
FColor::Magenta, false, 0.0f, 0, 0.5f);
|
||||
DrawDebugString(GetWorld(), HeadLoc + FVector(0, 0, 20.0f),
|
||||
FString::Printf(TEXT("GAZE: %s [%.1fs]"),
|
||||
*GazeTarget->GetName(), ProximityGazeDuration),
|
||||
nullptr, FColor::Magenta, 0.0f, true);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@ -0,0 +1,33 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "BehaviorTree/BTService.h"
|
||||
#include "PS_AI_Behavior_BTService_UpdateGaze.generated.h"
|
||||
|
||||
/**
|
||||
* BT Service: Updates the NPC's gaze target based on behavior state and proximity.
|
||||
*
|
||||
* Bridges PS_AI_Behavior to PS_AI_ConvAgent's GazeComponent via runtime reflection.
|
||||
* No compile-time dependency on PS_AI_ConvAgent — silently skips if not present.
|
||||
*
|
||||
* Priority order:
|
||||
* 1. Combat/Alerted → look at ThreatActor (head + eyes + body)
|
||||
* 2. Conversation active → skip (managed by ConvAgent)
|
||||
* 3. Proximity → closest perceived actor within radius (head + eyes only)
|
||||
*
|
||||
* Place on the root Selector alongside UpdateThreat and EvaluateReaction.
|
||||
*/
|
||||
UCLASS(meta = (DisplayName = "PS AI: Update Gaze"))
|
||||
class PS_AI_BEHAVIOR_API UPS_AI_Behavior_BTService_UpdateGaze : public UBTService
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPS_AI_Behavior_BTService_UpdateGaze();
|
||||
|
||||
protected:
|
||||
virtual void TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
|
||||
virtual FString GetStaticDescription() const override;
|
||||
};
|
||||
@ -71,6 +71,24 @@ public:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Patrol")
|
||||
TArray<FVector> PatrolPoints;
|
||||
|
||||
// ─── Gaze Configuration ────────────────────────────────────────────
|
||||
|
||||
/** Detection radius for proximity gaze (cm). NPC glances at nearby actors within this range. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS AI Behavior|Gaze", meta = (ClampMin = "100.0", ClampMax = "1500.0"))
|
||||
float GazeProximityRadius = 400.0f;
|
||||
|
||||
/** Maximum duration (seconds) the NPC looks at a proximity target before releasing. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS AI Behavior|Gaze", meta = (ClampMin = "0.5", ClampMax = "30.0"))
|
||||
float GazeMaxDuration = 3.0f;
|
||||
|
||||
/** Cooldown (seconds) before re-targeting the same actor after releasing. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS AI Behavior|Gaze", meta = (ClampMin = "0.0", ClampMax = "30.0"))
|
||||
float GazeCooldown = 5.0f;
|
||||
|
||||
/** Enable/disable proximity gaze (NPC glances at nearby actors in Idle/Patrol). */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS AI Behavior|Gaze")
|
||||
bool bEnableProximityGaze = true;
|
||||
|
||||
// ─── Component Access ───────────────────────────────────────────────
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "PS AI Behavior")
|
||||
@ -116,4 +134,39 @@ private:
|
||||
* Uses UObject reflection — no compile-time dependency on PS_AI_ConvAgent.
|
||||
*/
|
||||
void TryBindConversationAgent();
|
||||
|
||||
// ─── Gaze Bridge (runtime reflection to PS_AI_ConvAgent) ────────
|
||||
|
||||
/** Discover GazeComponent on the Pawn and cache property pointers. */
|
||||
void TryBindGazeComponent();
|
||||
|
||||
/** Update the gaze target based on behavior state and proximity. Called by BTService_UpdateGaze. */
|
||||
void UpdateGazeTarget(float DeltaSeconds);
|
||||
|
||||
/** Set the GazeComponent's target actor via reflection. */
|
||||
void SetGazeTarget(AActor* Target, bool bEnableBody);
|
||||
|
||||
/** Clear the GazeComponent's target (smooth return to neutral). */
|
||||
void ClearGazeTarget();
|
||||
|
||||
/** Check if a conversation is active on the Pawn (via reflection on ElevenLabsComponent). */
|
||||
bool IsConversationActiveOnPawn() const;
|
||||
|
||||
// Cached reflection pointers (null if PS_AI_ConvAgent not loaded)
|
||||
TWeakObjectPtr<UActorComponent> CachedGazeComponent;
|
||||
FObjectProperty* GazeProp_TargetActor = nullptr;
|
||||
FBoolProperty* GazeProp_bActive = nullptr;
|
||||
FBoolProperty* GazeProp_bEnableBodyTracking = nullptr;
|
||||
|
||||
// Conversation detection cache
|
||||
TWeakObjectPtr<UActorComponent> CachedConvAgentComponent;
|
||||
FBoolProperty* ConvProp_bNetIsConversing = nullptr;
|
||||
|
||||
// Proximity gaze state
|
||||
TWeakObjectPtr<AActor> ProximityGazeTarget;
|
||||
float ProximityGazeDuration = 0.0f;
|
||||
float ProximityGazeCooldownTimer = 0.0f;
|
||||
TWeakObjectPtr<AActor> ProximityGazeCooldownActor;
|
||||
|
||||
friend class UPS_AI_Behavior_BTService_UpdateGaze;
|
||||
};
|
||||
|
||||
@ -456,6 +456,15 @@ void UPS_AI_ConvAgent_GazeComponent::TickComponent(
|
||||
}
|
||||
|
||||
// ── 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.
|
||||
if (!bEnableBodyTracking)
|
||||
{
|
||||
SmoothedBodyYaw = Owner->GetActorRotation().Yaw;
|
||||
TargetBodyWorldYaw = SmoothedBodyYaw;
|
||||
}
|
||||
|
||||
float DeltaYaw = 0.0f;
|
||||
if (!HorizontalDir.IsNearlyZero(1.0f))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user