Compare commits
2 Commits
27d0906d21
...
a5b0d5db84
| Author | SHA1 | Date | |
|---|---|---|---|
| a5b0d5db84 | |||
| cfade2a265 |
@@ -32,6 +32,14 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_FindAndFollowSpline::ExecuteTask(
|
|||||||
return EBTNodeResult::Failed;
|
return EBTNodeResult::Failed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Respect a manual StopFollowing(): do not search for or resume a spline until
|
||||||
|
// the caller explicitly StartFollowing() again. Without this, the task would
|
||||||
|
// resume any CurrentSpline and override the stop on the very next tick.
|
||||||
|
if (Follower->bManuallyStopped)
|
||||||
|
{
|
||||||
|
return EBTNodeResult::Failed;
|
||||||
|
}
|
||||||
|
|
||||||
// Debug: log state on entry
|
// Debug: log state on entry
|
||||||
if (UPS_AI_Behavior_Statics::IsDebugEnabled(AIC->GetPawn()))
|
if (UPS_AI_Behavior_Statics::IsDebugEnabled(AIC->GetPawn()))
|
||||||
{
|
{
|
||||||
@@ -268,6 +276,18 @@ void UPS_AI_Behavior_BTTask_FindAndFollowSpline::TickTask(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Manual StopFollowing() requested mid-approach: cancel the move-to-spline and bail.
|
||||||
|
if (UPS_AI_Behavior_SplineFollowerComponent* StopCheck =
|
||||||
|
AIC->GetPawn()->FindComponentByClass<UPS_AI_Behavior_SplineFollowerComponent>())
|
||||||
|
{
|
||||||
|
if (StopCheck->bManuallyStopped)
|
||||||
|
{
|
||||||
|
AIC->StopMovement();
|
||||||
|
FinishLatentTask(OwnerComp, EBTNodeResult::Failed);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check if we've reached the spline
|
// Check if we've reached the spline
|
||||||
if (AIC->GetMoveStatus() == EPathFollowingStatus::Idle)
|
if (AIC->GetMoveStatus() == EPathFollowingStatus::Idle)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -24,6 +24,12 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_FollowSpline::ExecuteTask(
|
|||||||
AIC->GetPawn()->FindComponentByClass<UPS_AI_Behavior_SplineFollowerComponent>();
|
AIC->GetPawn()->FindComponentByClass<UPS_AI_Behavior_SplineFollowerComponent>();
|
||||||
if (!Follower) return EBTNodeResult::Failed;
|
if (!Follower) return EBTNodeResult::Failed;
|
||||||
|
|
||||||
|
// Respect a manual StopFollowing(): refuse to resume until an explicit restart.
|
||||||
|
if (Follower->bManuallyStopped)
|
||||||
|
{
|
||||||
|
return EBTNodeResult::Failed;
|
||||||
|
}
|
||||||
|
|
||||||
if (!Follower->CurrentSpline)
|
if (!Follower->CurrentSpline)
|
||||||
{
|
{
|
||||||
UE_LOG(LogPS_AI_Behavior, Warning, TEXT("[%s] FollowSpline: no current spline set."),
|
UE_LOG(LogPS_AI_Behavior, Warning, TEXT("[%s] FollowSpline: no current spline set."),
|
||||||
|
|||||||
@@ -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 ────────────────────────────────
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ bool UPS_AI_Behavior_SplineFollowerComponent::StartFollowingAtDistance(
|
|||||||
CurrentDistance = FMath::Clamp(StartDistance, 0.0f, Spline->GetSplineLength());
|
CurrentDistance = FMath::Clamp(StartDistance, 0.0f, Spline->GetSplineLength());
|
||||||
bMovingForward = bForward;
|
bMovingForward = bForward;
|
||||||
bIsFollowing = true;
|
bIsFollowing = true;
|
||||||
|
bManuallyStopped = false; // an explicit Start lifts any previous manual stop
|
||||||
LastHandledJunctionIndex = -1;
|
LastHandledJunctionIndex = -1;
|
||||||
|
|
||||||
UE_LOG(LogPS_AI_Behavior, Warning,
|
UE_LOG(LogPS_AI_Behavior, Warning,
|
||||||
@@ -82,18 +83,23 @@ bool UPS_AI_Behavior_SplineFollowerComponent::StartFollowingAtDistance(
|
|||||||
void UPS_AI_Behavior_SplineFollowerComponent::StopFollowing()
|
void UPS_AI_Behavior_SplineFollowerComponent::StopFollowing()
|
||||||
{
|
{
|
||||||
bIsFollowing = false;
|
bIsFollowing = false;
|
||||||
UE_LOG(LogPS_AI_Behavior, Verbose, TEXT("[%s] Stopped following spline."),
|
// Firm, manual stop: the Behavior Tree spline tasks will refuse to re-start
|
||||||
|
// following until StartFollowing()/StartFollowingAtDistance() is called again.
|
||||||
|
bManuallyStopped = true;
|
||||||
|
UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] StopFollowing: manual stop (BT will not auto-resume)."),
|
||||||
GetOwner() ? *GetOwner()->GetName() : TEXT("?"));
|
GetOwner() ? *GetOwner()->GetName() : TEXT("?"));
|
||||||
}
|
}
|
||||||
|
|
||||||
void UPS_AI_Behavior_SplineFollowerComponent::PauseFollowing()
|
void UPS_AI_Behavior_SplineFollowerComponent::PauseFollowing()
|
||||||
{
|
{
|
||||||
|
// Pause is NOT a manual stop — the BT is allowed to resume it.
|
||||||
bIsFollowing = false;
|
bIsFollowing = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void UPS_AI_Behavior_SplineFollowerComponent::ResumeFollowing()
|
void UPS_AI_Behavior_SplineFollowerComponent::ResumeFollowing()
|
||||||
{
|
{
|
||||||
if (CurrentSpline)
|
// Do not resume over a manual stop — only an explicit Start lifts that.
|
||||||
|
if (CurrentSpline && !bManuallyStopped)
|
||||||
{
|
{
|
||||||
bIsFollowing = true;
|
bIsFollowing = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -106,6 +106,13 @@ public:
|
|||||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Replicated, Category = "Spline Follower|Runtime")
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Replicated, Category = "Spline Follower|Runtime")
|
||||||
bool bIsFollowing = false;
|
bool bIsFollowing = false;
|
||||||
|
|
||||||
|
/** Set by StopFollowing(): a firm, manual stop. While true, the Behavior Tree
|
||||||
|
* spline tasks refuse to (re)start following — only an explicit StartFollowing()
|
||||||
|
* / StartFollowingAtDistance() clears it. This is what distinguishes a deliberate
|
||||||
|
* stop from PauseFollowing() (which the BT is allowed to auto-resume). */
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Spline Follower|Runtime")
|
||||||
|
bool bManuallyStopped = false;
|
||||||
|
|
||||||
// ─── Delegates ──────────────────────────────────────────────────────
|
// ─── Delegates ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
/** Fired when approaching a junction. Use to make custom spline selection. */
|
/** Fired when approaching a junction. Use to make custom spline selection. */
|
||||||
|
|||||||
Reference in New Issue
Block a user