PS_AI_Behavior: freeze movement during conversation + manual lever

Auto pause now triggers as soon as a conversation is genuinely active (agent greeting included) instead of waiting for the user to speak — the NPC no longer walks away during its opening line.

Add DisableBehaviorMovement()/EnableBehaviorMovement() (BlueprintCallable): freeze the NPC's autonomous movement while keeping the Behavior Tree running (perception, threat, gaze, reactions). Movement is paused when EITHER manually disabled OR a conversation is active. Both drive the ConversationPaused Blackboard flag read by the movement-branch decorators; leave the Flee branch un-decorated so fleeing still works.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-01 09:41:06 +02:00
parent cfade2a265
commit a5b0d5db84
2 changed files with 83 additions and 101 deletions

View File

@@ -802,6 +802,53 @@ bool APS_AI_Behavior_AIController::IsConversationActiveOnPawn() const
return ConvProp_bNetIsConversing->GetPropertyValue_InContainer(ConvComp); return ConvProp_bNetIsConversing->GetPropertyValue_InContainer(ConvComp);
} }
void APS_AI_Behavior_AIController::ApplyMovementPaused(bool bPaused)
{
if (bPaused == bConversationPaused)
{
return; // no change — idempotent
}
bConversationPaused = bPaused;
// Blackboard flag read by the movement-branch decorators (Patrol / Spline).
if (Blackboard)
{
Blackboard->SetValueAsBool(PS_AI_Behavior_BB::ConversationPaused, bPaused);
}
if (bPaused)
{
StopMovement(); // cancel any in-flight MoveTo immediately
}
if (APawn* MyPawn = GetPawn())
{
if (auto* Spline = MyPawn->FindComponentByClass<UPS_AI_Behavior_SplineFollowerComponent>())
{
if (bPaused) { Spline->PauseFollowing(); }
else { Spline->ResumeFollowing(); }
}
}
UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] Movement %s."),
*GetName(), bPaused ? TEXT("paused") : TEXT("resumed"));
}
void APS_AI_Behavior_AIController::DisableBehaviorMovement()
{
bMovementManuallyDisabled = true;
ApplyMovementPaused(true);
UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] DisableBehaviorMovement — manual freeze (BT keeps running)."), *GetName());
}
void APS_AI_Behavior_AIController::EnableBehaviorMovement()
{
bMovementManuallyDisabled = false;
// Stay paused if a conversation is still active; otherwise resume.
ApplyMovementPaused(IsConversationActiveOnPawn());
UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] EnableBehaviorMovement — manual freeze released."), *GetName());
}
void APS_AI_Behavior_AIController::UpdateGazeTarget(float DeltaSeconds) void APS_AI_Behavior_AIController::UpdateGazeTarget(float DeltaSeconds)
{ {
if (!CachedGazeComponent.IsValid()) return; if (!CachedGazeComponent.IsValid()) return;
@@ -842,117 +889,26 @@ void APS_AI_Behavior_AIController::UpdateGazeTarget(float DeltaSeconds)
} }
} }
// ── Priority 2: Conversation active manage movement pause via local VAD // ── Priority 2: Conversation active OR manual freeze → pause movement ──
// The BT keeps running; a "ConversationPaused" Blackboard decorator blocks the
// movement branches (Patrol / Spline). Leave the Flee branch un-decorated so
// fleeing still works. Movement pauses as soon as the conversation is genuinely
// active (agent greeting included) — it no longer waits for the user to speak —
// or whenever DisableBehaviorMovement() was called.
{ {
const bool bConversing = IsConversationActiveOnPawn(); const bool bConversing = IsConversationActiveOnPawn();
ApplyMovementPaused(bMovementManuallyDisabled || bConversing);
// Check local VAD: is the user actually speaking?
// The mic is on the PLAYER's Pawn — find it dynamically
bool bUserSpeaking = false;
if (MicProp_bIsUserSpeaking)
{
// Try cached mic first
UActorComponent* MicComp = CachedMicComponent.Get();
if (!MicComp)
{
// Find mic on any player pawn
static UClass* MicClass = LoadClass<UActorComponent>(nullptr,
TEXT("/Script/PS_AI_ConvAgent.PS_AI_ConvAgent_MicrophoneCaptureComponent"));
if (MicClass)
{
for (FConstPlayerControllerIterator It = GetWorld()->GetPlayerControllerIterator(); It; ++It)
{
if (APlayerController* PC = It->Get())
{
if (APawn* PlayerPawn = PC->GetPawn())
{
MicComp = PlayerPawn->FindComponentByClass(MicClass);
if (MicComp)
{
CachedMicComponent = MicComp;
break;
}
}
}
}
}
}
if (MicComp)
{
bUserSpeaking = MicProp_bIsUserSpeaking->GetPropertyValue_InContainer(MicComp);
}
}
// Once user has spoken during this conversation, stay paused until conversation ends
if (bConversing && bUserSpeaking)
{
bUserHasSpokenInConversation = true;
}
if (bConversing && bUserHasSpokenInConversation)
{
// Set BB flag to block movement branches via decorator
if (!bConversationPaused)
{
bConversationPaused = true;
if (Blackboard)
{
Blackboard->SetValueAsBool(PS_AI_Behavior_BB::ConversationPaused, true);
}
StopMovement();
APawn* ConvPawn = GetPawn();
if (ConvPawn)
{
auto* Spline = ConvPawn->FindComponentByClass<UPS_AI_Behavior_SplineFollowerComponent>();
if (Spline)
{
Spline->PauseFollowing();
}
}
UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] User speaking — conversation pause."), *GetName());
}
ProximityGazeTarget = nullptr;
ProximityGazeDuration = 0.0f;
return; // Gaze managed by ConvAgent
}
if (bConversing) if (bConversing)
{ {
// Connected but user hasn't spoken yet — don't touch gaze, don't pause // Gaze is driven by the ConvAgent during conversation.
ProximityGazeTarget = nullptr; ProximityGazeTarget = nullptr;
ProximityGazeDuration = 0.0f; ProximityGazeDuration = 0.0f;
return; return;
} }
// Manual-only freeze (no active conversation): movement stays paused above,
// Conversation ended — clear BB flag, resume movement // but proximity gaze below still runs so the NPC can look around.
if (bConversationPaused)
{
bConversationPaused = false;
bUserHasSpokenInConversation = false;
if (Blackboard)
{
Blackboard->SetValueAsBool(PS_AI_Behavior_BB::ConversationPaused, false);
}
APawn* ConvPawn = GetPawn();
if (ConvPawn)
{
if (auto* Spline = ConvPawn->FindComponentByClass<UPS_AI_Behavior_SplineFollowerComponent>())
{
Spline->ResumeFollowing();
}
}
UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] Conversation ended — resumed."), *GetName());
}
} }
// ── Priority 3/4: Proximity gaze ──────────────────────────────── // ── Priority 3/4: Proximity gaze ────────────────────────────────

View File

@@ -134,6 +134,22 @@ public:
UFUNCTION(BlueprintCallable, Category = "ASTERION|PS_AI_Behavior") UFUNCTION(BlueprintCallable, Category = "ASTERION|PS_AI_Behavior")
void ResetBehavior(); void ResetBehavior();
/**
* Freeze the NPC's autonomous movement WITHOUT stopping the Behavior Tree.
* The BT keeps running (perception, threat, gaze, reactions) but movement
* branches are blocked via the ConversationPaused Blackboard flag, and any
* in-flight move is cancelled. Fleeing is NOT blocked (leave the Flee branch
* un-decorated). Use this to keep an NPC in place during a scripted moment
* (e.g. dialogue) regardless of the automatic conversation pause.
*/
UFUNCTION(BlueprintCallable, Category = "ASTERION|PS_AI_Behavior")
void DisableBehaviorMovement();
/** Release the manual movement freeze set by DisableBehaviorMovement().
* Movement stays paused if a conversation is still active. */
UFUNCTION(BlueprintCallable, Category = "ASTERION|PS_AI_Behavior")
void EnableBehaviorMovement();
protected: protected:
virtual void OnPossess(APawn* InPawn) override; virtual void OnPossess(APawn* InPawn) override;
virtual void OnUnPossess() override; virtual void OnUnPossess() override;
@@ -176,6 +192,11 @@ private:
/** Check if a conversation is active on the Pawn (via reflection on ElevenLabsComponent). */ /** Check if a conversation is active on the Pawn (via reflection on ElevenLabsComponent). */
bool IsConversationActiveOnPawn() const; bool IsConversationActiveOnPawn() const;
/** Apply (or release) the movement pause: syncs the ConversationPaused Blackboard
* flag, stops in-flight movement, and pauses/resumes the spline follower.
* No-op if the state is unchanged. */
void ApplyMovementPaused(bool bPaused);
// Cached reflection pointers (null if PS_AI_ConvAgent not loaded) // Cached reflection pointers (null if PS_AI_ConvAgent not loaded)
TWeakObjectPtr<UActorComponent> CachedGazeComponent; TWeakObjectPtr<UActorComponent> CachedGazeComponent;
FObjectProperty* GazeProp_TargetActor = nullptr; FObjectProperty* GazeProp_TargetActor = nullptr;
@@ -194,6 +215,11 @@ private:
bool bConversationPaused = false; bool bConversationPaused = false;
bool bUserHasSpokenInConversation = false; bool bUserHasSpokenInConversation = false;
// True while movement is frozen by an explicit DisableBehaviorMovement() call.
// Combined with the automatic conversation pause: movement is paused when
// either this is set OR a conversation is active.
bool bMovementManuallyDisabled = false;
// Proximity gaze state // Proximity gaze state
TWeakObjectPtr<AActor> ProximityGazeTarget; TWeakObjectPtr<AActor> ProximityGazeTarget;
float ProximityGazeDuration = 0.0f; float ProximityGazeDuration = 0.0f;