Allow switching agents mid-conversation by looking at another agent

Conversation lock no longer prevents switching to a different agent.
When in an active conversation, the player can look at another nearby
agent for ConversationSwitchDelay seconds (default 1s) to switch.
Looking at empty space keeps the current agent selected (no deselect).
Works in multiplayer — each player has independent switch tracking.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
j.foucher 2026-03-06 17:07:03 +01:00
parent 4456dfa9dc
commit 28aed55cd3
2 changed files with 73 additions and 12 deletions

View File

@ -165,7 +165,7 @@ void UPS_AI_ConvAgent_InteractionComponent::TickComponent(float DeltaTime, ELeve
// ───────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────
// Selection evaluation // Selection evaluation
// ───────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────
UPS_AI_ConvAgent_ElevenLabsComponent* UPS_AI_ConvAgent_InteractionComponent::EvaluateBestAgent() const UPS_AI_ConvAgent_ElevenLabsComponent* UPS_AI_ConvAgent_InteractionComponent::EvaluateBestAgent()
{ {
UWorld* World = GetWorld(); UWorld* World = GetWorld();
if (!World) return nullptr; if (!World) return nullptr;
@ -190,23 +190,25 @@ UPS_AI_ConvAgent_ElevenLabsComponent* UPS_AI_ConvAgent_InteractionComponent::Eva
UPS_AI_ConvAgent_ElevenLabsComponent* CurrentAgent = SelectedAgent.Get(); UPS_AI_ConvAgent_ElevenLabsComponent* CurrentAgent = SelectedAgent.Get();
// ── Conversation lock ────────────────────────────────────────────── // ── Conversation lock ──────────────────────────────────────────────
// While we're actively conversing with an agent, keep it selected as // While we're actively conversing with an agent, keep it selected UNLESS
// long as it's within interaction distance — ignore the view cone. // the player is looking directly at a DIFFERENT agent within range.
// This prevents deselect/reselect flicker when the player turns quickly // This allows switching between nearby agents by looking at them, while
// (which would cause spurious OnAgentConnected re-broadcasts in // preventing deselect when looking at empty space (no agent in view cone).
// persistent session mode). // If no other agent is in the view cone, the current agent stays selected
if (CurrentAgent && CurrentAgent->bNetIsConversing) // regardless of look direction — only distance can break the lock.
const bool bConversationLocked = CurrentAgent && CurrentAgent->bNetIsConversing;
bool bCurrentAgentInRange = false;
if (bConversationLocked)
{ {
if (AActor* AgentActor = CurrentAgent->GetOwner()) if (AActor* AgentActor = CurrentAgent->GetOwner())
{ {
const FVector AgentLoc = AgentActor->GetActorLocation() const FVector AgentLoc = AgentActor->GetActorLocation()
+ FVector(0.0f, 0.0f, AgentEyeLevelOffset); + FVector(0.0f, 0.0f, AgentEyeLevelOffset);
const float DistSq = (AgentLoc - ViewLocation).SizeSquared(); const float DistSq = (AgentLoc - ViewLocation).SizeSquared();
if (DistSq <= MaxDistSq) bCurrentAgentInRange = (DistSq <= MaxDistSq);
{
return CurrentAgent; // Keep conversing agent selected.
}
} }
// If current agent is out of range, fall through to normal evaluation
// (which will select a new agent or nullptr).
} }
for (UPS_AI_ConvAgent_ElevenLabsComponent* Agent : Agents) for (UPS_AI_ConvAgent_ElevenLabsComponent* Agent : Agents)
@ -259,6 +261,51 @@ UPS_AI_ConvAgent_ElevenLabsComponent* UPS_AI_ConvAgent_InteractionComponent::Eva
} }
} }
// ── Conversation lock fallback ────────────────────────────────────
// If we're in conversation and the current agent is still in range:
// - No other agent in view cone → keep current agent (don't deselect).
// - Different agent in view cone → switch after ConversationSwitchDelay.
if (bConversationLocked && bCurrentAgentInRange)
{
if (!BestCandidate || BestCandidate == CurrentAgent)
{
// Looking at current agent or empty space → keep current, reset switch timer.
PendingSwitchAgent.Reset();
PendingSwitchStartTime = 0.0;
return CurrentAgent;
}
// Player is looking at a different agent. Apply switch delay.
if (ConversationSwitchDelay <= 0.0f)
{
// Instant switch (delay = 0).
PendingSwitchAgent.Reset();
PendingSwitchStartTime = 0.0;
return BestCandidate;
}
// Start or continue the switch timer.
if (PendingSwitchAgent.Get() != BestCandidate)
{
// Player started looking at a new candidate — reset timer.
PendingSwitchAgent = BestCandidate;
PendingSwitchStartTime = FPlatformTime::Seconds();
return CurrentAgent; // Not yet — keep current.
}
// Same candidate as before — check if delay has elapsed.
const double Elapsed = FPlatformTime::Seconds() - PendingSwitchStartTime;
if (Elapsed < static_cast<double>(ConversationSwitchDelay))
{
return CurrentAgent; // Still waiting.
}
// Delay elapsed → allow the switch.
PendingSwitchAgent.Reset();
PendingSwitchStartTime = 0.0;
return BestCandidate;
}
return BestCandidate; return BestCandidate;
} }

View File

@ -117,6 +117,14 @@ public:
// ── Conversation management ────────────────────────────────────────────── // ── Conversation management ──────────────────────────────────────────────
/** How long (seconds) the player must look at a different agent before switching
* during an active conversation. Prevents accidental switches when glancing around.
* Only applies when a conversation is active (bNetIsConversing). */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS AI ConvAgent|Interaction",
meta = (ClampMin = "0.0", ClampMax = "5.0",
ToolTip = "Seconds the player must look at another agent before switching mid-conversation.\nPrevents accidental switches when glancing around.\n0 = instant switch."))
float ConversationSwitchDelay = 1.0f;
/** Automatically start the WebSocket conversation when an agent is selected /** Automatically start the WebSocket conversation when an agent is selected
* (enters range + view cone). When false, selecting an agent only manages * (enters range + view cone). When false, selecting an agent only manages
* gaze and visual awareness the conversation must be started explicitly * gaze and visual awareness the conversation must be started explicitly
@ -251,7 +259,7 @@ private:
// ── Selection logic ────────────────────────────────────────────────────── // ── Selection logic ──────────────────────────────────────────────────────
/** Evaluate all registered agents, return the best candidate (or null). */ /** Evaluate all registered agents, return the best candidate (or null). */
UPS_AI_ConvAgent_ElevenLabsComponent* EvaluateBestAgent() const; UPS_AI_ConvAgent_ElevenLabsComponent* EvaluateBestAgent();
/** Apply a new selection — fire events, reroute mic. */ /** Apply a new selection — fire events, reroute mic. */
void SetSelectedAgent(UPS_AI_ConvAgent_ElevenLabsComponent* NewAgent); void SetSelectedAgent(UPS_AI_ConvAgent_ElevenLabsComponent* NewAgent);
@ -296,4 +304,10 @@ private:
FTimerHandle GazeAttachTimerHandle; FTimerHandle GazeAttachTimerHandle;
FTimerHandle GazeDetachTimerHandle; FTimerHandle GazeDetachTimerHandle;
// ── Conversation switch delay ────────────────────────────────────────
// Tracks how long the player has been looking at a different agent
// while in an active conversation. Switch only happens after the delay.
TWeakObjectPtr<UPS_AI_ConvAgent_ElevenLabsComponent> PendingSwitchAgent;
double PendingSwitchStartTime = 0.0;
}; };