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_Vector.h"
|
||||||
#include "BehaviorTree/Blackboard/BlackboardKeyType_String.h"
|
#include "BehaviorTree/Blackboard/BlackboardKeyType_String.h"
|
||||||
#include "BehaviorTree/Blackboard/BlackboardKeyType_Bool.h"
|
#include "BehaviorTree/Blackboard/BlackboardKeyType_Bool.h"
|
||||||
|
#include "DrawDebugHelpers.h"
|
||||||
|
#include "GameFramework/SpectatorPawn.h"
|
||||||
|
|
||||||
APS_AI_Behavior_AIController::APS_AI_Behavior_AIController()
|
APS_AI_Behavior_AIController::APS_AI_Behavior_AIController()
|
||||||
{
|
{
|
||||||
@ -80,6 +82,7 @@ void APS_AI_Behavior_AIController::OnPossess(APawn* InPawn)
|
|||||||
SetupBlackboard();
|
SetupBlackboard();
|
||||||
StartBehavior();
|
StartBehavior();
|
||||||
TryBindConversationAgent();
|
TryBindConversationAgent();
|
||||||
|
TryBindGazeComponent();
|
||||||
|
|
||||||
UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] Possessed Pawn '%s' — BT started, TeamId=%d."),
|
UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] Possessed Pawn '%s' — BT started, TeamId=%d."),
|
||||||
*GetName(), *InPawn->GetName(), TeamId);
|
*GetName(), *InPawn->GetName(), TeamId);
|
||||||
@ -94,6 +97,11 @@ void APS_AI_Behavior_AIController::OnUnPossess()
|
|||||||
Brain->StopLogic(TEXT("Unpossessed"));
|
Brain->StopLogic(TEXT("Unpossessed"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ClearGazeTarget();
|
||||||
|
CachedGazeComponent = nullptr;
|
||||||
|
CachedConvAgentComponent = nullptr;
|
||||||
|
ProximityGazeTarget = nullptr;
|
||||||
|
|
||||||
PersonalityComp = nullptr;
|
PersonalityComp = nullptr;
|
||||||
Super::OnUnPossess();
|
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());
|
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)
|
// 1. Stop the Behavior Tree (no more services, tasks, or decorators)
|
||||||
if (UBrainComponent* Brain = GetBrainComponent())
|
if (UBrainComponent* Brain = GetBrainComponent())
|
||||||
{
|
{
|
||||||
@ -477,3 +488,272 @@ void APS_AI_Behavior_AIController::TryBindConversationAgent()
|
|||||||
*GetName());
|
*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")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Patrol")
|
||||||
TArray<FVector> PatrolPoints;
|
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 ───────────────────────────────────────────────
|
// ─── Component Access ───────────────────────────────────────────────
|
||||||
|
|
||||||
UFUNCTION(BlueprintCallable, Category = "PS AI Behavior")
|
UFUNCTION(BlueprintCallable, Category = "PS AI Behavior")
|
||||||
@ -116,4 +134,39 @@ private:
|
|||||||
* Uses UObject reflection — no compile-time dependency on PS_AI_ConvAgent.
|
* Uses UObject reflection — no compile-time dependency on PS_AI_ConvAgent.
|
||||||
*/
|
*/
|
||||||
void TryBindConversationAgent();
|
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 ──────────────────────────
|
// ── 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;
|
float DeltaYaw = 0.0f;
|
||||||
if (!HorizontalDir.IsNearlyZero(1.0f))
|
if (!HorizontalDir.IsNearlyZero(1.0f))
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user