From a5b0d5db845aaecb82c98b02ebe5366cbe47b743 Mon Sep 17 00:00:00 2001 From: "j.foucher" Date: Mon, 1 Jun 2026 09:41:06 +0200 Subject: [PATCH] PS_AI_Behavior: freeze movement during conversation + manual lever MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../Private/PS_AI_Behavior_AIController.cpp | 158 +++++++----------- .../Public/PS_AI_Behavior_AIController.h | 26 +++ 2 files changed, 83 insertions(+), 101 deletions(-) diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/PS_AI_Behavior_AIController.cpp b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/PS_AI_Behavior_AIController.cpp index d1d7c08..1648f2b 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/PS_AI_Behavior_AIController.cpp +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/PS_AI_Behavior_AIController.cpp @@ -802,6 +802,53 @@ bool APS_AI_Behavior_AIController::IsConversationActiveOnPawn() const 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()) + { + 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) { 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(); - - // 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(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(); - 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 - } + ApplyMovementPaused(bMovementManuallyDisabled || 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; ProximityGazeDuration = 0.0f; return; } - - // Conversation ended — clear BB flag, resume movement - 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()) - { - Spline->ResumeFollowing(); - } - } - - UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] Conversation ended — resumed."), *GetName()); - } + // Manual-only freeze (no active conversation): movement stays paused above, + // but proximity gaze below still runs so the NPC can look around. } // ── Priority 3/4: Proximity gaze ──────────────────────────────── diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/PS_AI_Behavior_AIController.h b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/PS_AI_Behavior_AIController.h index 174f775..550be9c 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/PS_AI_Behavior_AIController.h +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/PS_AI_Behavior_AIController.h @@ -134,6 +134,22 @@ public: UFUNCTION(BlueprintCallable, Category = "ASTERION|PS_AI_Behavior") 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: virtual void OnPossess(APawn* InPawn) override; virtual void OnUnPossess() override; @@ -176,6 +192,11 @@ private: /** Check if a conversation is active on the Pawn (via reflection on ElevenLabsComponent). */ 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) TWeakObjectPtr CachedGazeComponent; FObjectProperty* GazeProp_TargetActor = nullptr; @@ -194,6 +215,11 @@ private: bool bConversationPaused = 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 TWeakObjectPtr ProximityGazeTarget; float ProximityGazeDuration = 0.0f;