From c6aed472f9354079011c35e871af69cc9b8d7164 Mon Sep 17 00:00:00 2001 From: "j.foucher" Date: Fri, 27 Feb 2026 17:39:05 +0100 Subject: [PATCH] Add auto posture management to InteractionComponent InteractionComponent now automatically sets/clears the agent's PostureComponent TargetActor on selection/deselection, with configurable attach/detach delays and a master toggle for manual control. Co-Authored-By: Claude Opus 4.6 --- .../PS_AI_ConvAgent_InteractionComponent.cpp | 105 ++++++++++++++++++ .../PS_AI_ConvAgent_InteractionComponent.h | 40 +++++++ 2 files changed, 145 insertions(+) diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgent/Private/PS_AI_ConvAgent_InteractionComponent.cpp b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgent/Private/PS_AI_ConvAgent_InteractionComponent.cpp index 7e423d2..8721c21 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgent/Private/PS_AI_ConvAgent_InteractionComponent.cpp +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgent/Private/PS_AI_ConvAgent_InteractionComponent.cpp @@ -4,10 +4,12 @@ #include "PS_AI_ConvAgent_InteractionSubsystem.h" #include "PS_AI_ConvAgent_ElevenLabsComponent.h" #include "PS_AI_ConvAgent_MicrophoneCaptureComponent.h" +#include "PS_AI_ConvAgent_PostureComponent.h" #include "GameFramework/Pawn.h" #include "GameFramework/PlayerController.h" #include "Camera/PlayerCameraManager.h" +#include "TimerManager.h" DEFINE_LOG_CATEGORY_STATIC(LogPS_AI_ConvAgent_Select, Log, All); @@ -48,6 +50,13 @@ void UPS_AI_ConvAgent_InteractionComponent::BeginPlay() void UPS_AI_ConvAgent_InteractionComponent::EndPlay(const EEndPlayReason::Type EndPlayReason) { + // Cancel any pending posture timers. + if (UWorld* World = GetWorld()) + { + World->GetTimerManager().ClearTimer(PostureAttachTimerHandle); + World->GetTimerManager().ClearTimer(PostureDetachTimerHandle); + } + if (MicComponent) { MicComponent->StopCapture(); @@ -57,6 +66,12 @@ void UPS_AI_ConvAgent_InteractionComponent::EndPlay(const EEndPlayReason::Type E // Fire deselection event for cleanup. if (UPS_AI_ConvAgent_ElevenLabsComponent* Agent = SelectedAgent.Get()) { + // Clear posture immediately on shutdown — no delay. + if (bAutoManagePosture) + { + DetachPostureTarget(Agent); + } + SelectedAgent.Reset(); OnAgentDeselected.Broadcast(Agent); } @@ -162,6 +177,7 @@ UPS_AI_ConvAgent_ElevenLabsComponent* UPS_AI_ConvAgent_InteractionComponent::Eva void UPS_AI_ConvAgent_InteractionComponent::SetSelectedAgent(UPS_AI_ConvAgent_ElevenLabsComponent* NewAgent) { UPS_AI_ConvAgent_ElevenLabsComponent* OldAgent = SelectedAgent.Get(); + UWorld* World = GetWorld(); // Deselect old agent. if (OldAgent) @@ -174,6 +190,26 @@ void UPS_AI_ConvAgent_InteractionComponent::SetSelectedAgent(UPS_AI_ConvAgent_El OldAgent->GetOwner() ? *OldAgent->GetOwner()->GetName() : TEXT("(null)")); } + // ── Posture: detach ────────────────────────────────────────────── + if (bAutoManagePosture && World) + { + // Cancel any pending attach — agent left before attach fired. + World->GetTimerManager().ClearTimer(PostureAttachTimerHandle); + + if (PostureDetachDelay > 0.0f) + { + TWeakObjectPtr WeakOld = OldAgent; + World->GetTimerManager().SetTimer(PostureDetachTimerHandle, + FTimerDelegate::CreateUObject(this, + &UPS_AI_ConvAgent_InteractionComponent::DetachPostureTarget, WeakOld), + PostureDetachDelay, false); + } + else + { + DetachPostureTarget(OldAgent); + } + } + OnAgentDeselected.Broadcast(OldAgent); } @@ -194,6 +230,26 @@ void UPS_AI_ConvAgent_InteractionComponent::SetSelectedAgent(UPS_AI_ConvAgent_El MicComponent->StartCapture(); } + // ── Posture: attach ────────────────────────────────────────────── + if (bAutoManagePosture && World) + { + // Cancel any pending detach — agent came back before detach fired. + World->GetTimerManager().ClearTimer(PostureDetachTimerHandle); + + if (PostureAttachDelay > 0.0f) + { + TWeakObjectPtr WeakNew = NewAgent; + World->GetTimerManager().SetTimer(PostureAttachTimerHandle, + FTimerDelegate::CreateUObject(this, + &UPS_AI_ConvAgent_InteractionComponent::AttachPostureTarget, WeakNew), + PostureAttachDelay, false); + } + else + { + AttachPostureTarget(NewAgent); + } + } + OnAgentSelected.Broadcast(NewAgent); } else @@ -252,6 +308,55 @@ void UPS_AI_ConvAgent_InteractionComponent::ClearSelection() SetSelectedAgent(nullptr); } +// ───────────────────────────────────────────────────────────────────────────── +// Posture helpers +// ───────────────────────────────────────────────────────────────────────────── +UPS_AI_ConvAgent_PostureComponent* UPS_AI_ConvAgent_InteractionComponent::FindPostureOnAgent( + UPS_AI_ConvAgent_ElevenLabsComponent* Agent) +{ + if (!Agent) return nullptr; + AActor* AgentActor = Agent->GetOwner(); + if (!AgentActor) return nullptr; + return AgentActor->FindComponentByClass(); +} + +void UPS_AI_ConvAgent_InteractionComponent::AttachPostureTarget( + TWeakObjectPtr Agent) +{ + UPS_AI_ConvAgent_ElevenLabsComponent* AgentPtr = Agent.Get(); + if (!AgentPtr) return; + + if (UPS_AI_ConvAgent_PostureComponent* Posture = FindPostureOnAgent(AgentPtr)) + { + Posture->TargetActor = GetOwner(); + + if (bDebug) + { + UE_LOG(LogPS_AI_ConvAgent_Select, Log, TEXT("Posture attached: %s -> %s"), + AgentPtr->GetOwner() ? *AgentPtr->GetOwner()->GetName() : TEXT("(null)"), + GetOwner() ? *GetOwner()->GetName() : TEXT("(null)")); + } + } +} + +void UPS_AI_ConvAgent_InteractionComponent::DetachPostureTarget( + TWeakObjectPtr Agent) +{ + UPS_AI_ConvAgent_ElevenLabsComponent* AgentPtr = Agent.Get(); + if (!AgentPtr) return; + + if (UPS_AI_ConvAgent_PostureComponent* Posture = FindPostureOnAgent(AgentPtr)) + { + Posture->TargetActor = nullptr; + + if (bDebug) + { + UE_LOG(LogPS_AI_ConvAgent_Select, Log, TEXT("Posture detached: %s"), + AgentPtr->GetOwner() ? *AgentPtr->GetOwner()->GetName() : TEXT("(null)")); + } + } +} + // ───────────────────────────────────────────────────────────────────────────── // Mic routing // ───────────────────────────────────────────────────────────────────────────── diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgent/Public/PS_AI_ConvAgent_InteractionComponent.h b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgent/Public/PS_AI_ConvAgent_InteractionComponent.h index 68a706d..5069dd8 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgent/Public/PS_AI_ConvAgent_InteractionComponent.h +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgent/Public/PS_AI_ConvAgent_InteractionComponent.h @@ -8,6 +8,7 @@ class UPS_AI_ConvAgent_ElevenLabsComponent; class UPS_AI_ConvAgent_MicrophoneCaptureComponent; +class UPS_AI_ConvAgent_PostureComponent; // ───────────────────────────────────────────────────────────────────────────── // Delegates @@ -82,6 +83,29 @@ public: meta = (ToolTip = "Require the player to look at an agent to select it.\nWhen false, the closest agent within range is always selected.")) bool bRequireLookAt = true; + // ── Posture management ─────────────────────────────────────────────────── + + /** Automatically set/clear the agent's PostureComponent TargetActor + * when the agent is selected/deselected. When false, posture must + * be managed from Blueprint (e.g. on conversation start). */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS AI ConvAgent|Interaction|Posture", + meta = (ToolTip = "Automatically point the agent's posture at the pawn on selection.\nDisable for manual control (e.g. set target only when conversation starts).")) + bool bAutoManagePosture = true; + + /** Delay (seconds) before setting the agent's posture target after selection. + * 0 = immediate. Useful to let the agent "notice" the player with a beat. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS AI ConvAgent|Interaction|Posture", + meta = (EditCondition = "bAutoManagePosture", ClampMin = "0", + ToolTip = "Seconds to wait before the agent looks at the pawn.\n0 = immediate.")) + float PostureAttachDelay = 0.0f; + + /** Delay (seconds) before clearing the agent's posture target after deselection. + * 0 = immediate. Useful to have the agent keep looking briefly as the player leaves. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS AI ConvAgent|Interaction|Posture", + meta = (EditCondition = "bAutoManagePosture", ClampMin = "0", + ToolTip = "Seconds to wait before the agent stops looking at the pawn.\n0 = immediate.")) + float PostureDetachDelay = 0.0f; + // ── Debug ──────────────────────────────────────────────────────────────── /** Enable debug logging for this component. */ @@ -144,6 +168,17 @@ private: /** Get the pawn's view location and direction (uses camera or control rotation). */ void GetPawnViewPoint(FVector& OutLocation, FVector& OutDirection) const; + // ── Posture helpers ────────────────────────────────────────────────────── + + /** Find the PostureComponent on an agent's owner actor (null if absent). */ + static UPS_AI_ConvAgent_PostureComponent* FindPostureOnAgent(UPS_AI_ConvAgent_ElevenLabsComponent* Agent); + + /** Set the agent's PostureComponent target to the pawn (attach). */ + void AttachPostureTarget(TWeakObjectPtr Agent); + + /** Clear the agent's PostureComponent target (detach). */ + void DetachPostureTarget(TWeakObjectPtr Agent); + // ── Mic routing ────────────────────────────────────────────────────────── /** Forward captured mic audio to the currently selected agent. */ @@ -160,4 +195,9 @@ private: /** True while ForceSelectAgent is active (suppresses automatic re-evaluation for one frame). */ bool bForceSelectionActive = false; + + // ── Posture timers ─────────────────────────────────────────────────────── + + FTimerHandle PostureAttachTimerHandle; + FTimerHandle PostureDetachTimerHandle; };