From c0c1b2cea44bbaabbe3042253c8f7b45a90de282 Mon Sep 17 00:00:00 2001 From: "j.foucher" Date: Mon, 2 Mar 2026 15:16:48 +0100 Subject: [PATCH] Fix head tracking on remote clients: replicate Pawn instead of PlayerController PlayerControllers have bOnlyRelevantToOwner=true, so NetConversatingPlayer was always nullptr on remote clients. Added NetConversatingPawn (APawn*) which IS replicated to all clients. OnRep_ConversationState now uses NetConversatingPawn as the posture TargetActor, enabling head/eye tracking on all clients. Co-Authored-By: Claude Opus 4.6 --- .../PS_AI_ConvAgent_ElevenLabsComponent.cpp | 20 ++++++++++++------- .../PS_AI_ConvAgent_ElevenLabsComponent.h | 7 ++++++- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgent/Private/PS_AI_ConvAgent_ElevenLabsComponent.cpp b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgent/Private/PS_AI_ConvAgent_ElevenLabsComponent.cpp index 83d5732..48bd780 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgent/Private/PS_AI_ConvAgent_ElevenLabsComponent.cpp +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgent/Private/PS_AI_ConvAgent_ElevenLabsComponent.cpp @@ -225,6 +225,7 @@ void UPS_AI_ConvAgent_ElevenLabsComponent::StartConversation() APlayerController* PC = GetWorld()->GetFirstPlayerController(); bNetIsConversing = true; NetConversatingPlayer = PC; + NetConversatingPawn = PC ? PC->GetPawn() : nullptr; } StartConversation_Internal(); } @@ -298,6 +299,7 @@ void UPS_AI_ConvAgent_ElevenLabsComponent::EndConversation() // Reset replicated state so other players can talk to this NPC. bNetIsConversing = false; NetConversatingPlayer = nullptr; + NetConversatingPawn = nullptr; } else { @@ -608,6 +610,7 @@ void UPS_AI_ConvAgent_ElevenLabsComponent::HandleDisconnected(int32 StatusCode, { bNetIsConversing = false; NetConversatingPlayer = nullptr; + NetConversatingPawn = nullptr; } OnAgentDisconnected.Broadcast(StatusCode, Reason); @@ -1195,6 +1198,7 @@ void UPS_AI_ConvAgent_ElevenLabsComponent::GetLifetimeReplicatedProps( Super::GetLifetimeReplicatedProps(OutLifetimeProps); DOREPLIFETIME(UPS_AI_ConvAgent_ElevenLabsComponent, bNetIsConversing); DOREPLIFETIME(UPS_AI_ConvAgent_ElevenLabsComponent, NetConversatingPlayer); + DOREPLIFETIME(UPS_AI_ConvAgent_ElevenLabsComponent, NetConversatingPawn); DOREPLIFETIME(UPS_AI_ConvAgent_ElevenLabsComponent, CurrentEmotion); DOREPLIFETIME(UPS_AI_ConvAgent_ElevenLabsComponent, CurrentEmotionIntensity); } @@ -1210,14 +1214,14 @@ void UPS_AI_ConvAgent_ElevenLabsComponent::OnRep_ConversationState() // on the local pawn, but remote clients never run that code path. if (UPS_AI_ConvAgent_PostureComponent* Posture = Owner->FindComponentByClass()) { - if (bNetIsConversing && NetConversatingPlayer) + // Use NetConversatingPawn (replicated to ALL clients) instead of + // NetConversatingPlayer->GetPawn() — PlayerControllers are only + // replicated to their owning client (bOnlyRelevantToOwner=true). + if (bNetIsConversing && NetConversatingPawn) { - if (APawn* PlayerPawn = NetConversatingPlayer->GetPawn()) - { - Posture->TargetActor = PlayerPawn; - Posture->ResetBodyTarget(); - Posture->bEnableBodyTracking = true; - } + Posture->TargetActor = NetConversatingPawn; + Posture->ResetBodyTarget(); + Posture->bEnableBodyTracking = true; } else { @@ -1271,6 +1275,7 @@ void UPS_AI_ConvAgent_ElevenLabsComponent::ServerRequestConversation_Implementat bNetIsConversing = true; NetConversatingPlayer = RequestingPlayer; + NetConversatingPawn = RequestingPlayer ? RequestingPlayer->GetPawn() : nullptr; StartConversation_Internal(); } @@ -1289,6 +1294,7 @@ void UPS_AI_ConvAgent_ElevenLabsComponent::ServerReleaseConversation_Implementat bNetIsConversing = false; NetConversatingPlayer = nullptr; + NetConversatingPawn = nullptr; } void UPS_AI_ConvAgent_ElevenLabsComponent::ServerSendMicAudio_Implementation( diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgent/Public/PS_AI_ConvAgent_ElevenLabsComponent.h b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgent/Public/PS_AI_ConvAgent_ElevenLabsComponent.h index d180eab..720691b 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgent/Public/PS_AI_ConvAgent_ElevenLabsComponent.h +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgent/Public/PS_AI_ConvAgent_ElevenLabsComponent.h @@ -296,10 +296,15 @@ public: bool bNetIsConversing = false; /** The player controller currently in conversation with this NPC (null if free). - * Replicated so each client knows who is speaking (used for posture target, LOD). */ + * Only valid on server and owning client (PlayerControllers are not replicated to other clients). */ UPROPERTY(ReplicatedUsing = OnRep_ConversationState, BlueprintReadOnly, Category = "PS AI ConvAgent|Network") TObjectPtr NetConversatingPlayer = nullptr; + /** The pawn of the conversating player. Replicated to ALL clients (unlike PlayerController). + * Used by remote clients for posture target (head/eye tracking) and LOD distance checks. */ + UPROPERTY(ReplicatedUsing = OnRep_ConversationState, BlueprintReadOnly, Category = "PS AI ConvAgent|Network") + TObjectPtr NetConversatingPawn = nullptr; + // ── Network LOD ────────────────────────────────────────────────────────── /** Distance (cm) beyond which remote clients stop receiving agent audio entirely.