#include "InteractionComponent.h" #include "Interactable.h" #include "Engine/OverlapResult.h" #include "CollisionQueryParams.h" #include "NakedDesire/Player/NakedDesireCharacter.h" UInteractionComponent::UInteractionComponent() { PrimaryComponentTick.bCanEverTick = false; } void UInteractionComponent::BeginPlay() { Super::BeginPlay(); // Only run on the locally controlled pawn — outlines and focus are per-client visuals const APawn* OwnerPawn = Cast(GetOwner()); if (!OwnerPawn) return; GetWorld()->GetTimerManager().SetTimer( InteractionTimerHandle, this, &UInteractionComponent::UpdateInteraction, 0.1f, true ); } void UInteractionComponent::UpdateInteraction() { UpdateNearbyInteractables(); UpdateFocusedInteractable(); } void UInteractionComponent::Interact() { AActor* Target = FocusedInteractable.Get(); if (!Target) return; if (!IInteractable::Execute_CanInteract(Target, Cast(GetOwner()))) return; IInteractable::Execute_Interact(Target, Cast(GetOwner())); } void UInteractionComponent::UpdateNearbyInteractables() { AActor* Owner = GetOwner(); if (!Owner) return; // Sphere overlap to find all nearby actors TArray Overlaps; FCollisionObjectQueryParams ObjectParams; ObjectParams.AddObjectTypesToQuery(ECC_WorldDynamic); ObjectParams.AddObjectTypesToQuery(ECC_WorldStatic); GetWorld()->OverlapMultiByObjectType( Overlaps, Owner->GetActorLocation(), FQuat::Identity, ObjectParams, FCollisionShape::MakeSphere(ProximityRadius) ); // Build the new set from overlap results, filtering to IInteractable actors TSet> NewNearby; for (const FOverlapResult& Overlap : Overlaps) { AActor* Actor = Overlap.GetActor(); if (Actor && Actor != Owner && Actor->Implements()) { NewNearby.Add(Actor); } } // Actors that left proximity: hide their hints for (const TWeakObjectPtr& OldActor : NearbyInteractables) { if (OldActor.IsValid() && !NewNearby.Contains(OldActor)) { if (FocusedInteractable == OldActor) { FocusedInteractable = nullptr; OnFocusedInteractableChanged.Broadcast(nullptr); } IInteractable::Execute_HideInteractionHint(OldActor.Get()); } } NearbyInteractables = MoveTemp(NewNearby); // Recompute the proximity hint for every nearby actor based on current LOS. // Runs every tick so actors that enter/leave line-of-sight mid-session update // immediately — e.g. a wall sliding into place hides the hint without the // actor ever leaving the proximity radius. // The focused actor is skipped: UpdateFocusedInteractable owns its hint state. for (const TWeakObjectPtr& WeakActor : NearbyInteractables) { AActor* Actor = WeakActor.Get(); if (!Actor || FocusedInteractable == Actor) continue; if (HasLineOfSightFromPawn(Actor)) IInteractable::Execute_ShowInteractionProximityHint(Actor); else IInteractable::Execute_HideInteractionHint(Actor); } } void UInteractionComponent::UpdateFocusedInteractable() { FVector ViewLocation; FRotator ViewRotation; if (!GetOwnerViewPoint(ViewLocation, ViewRotation)) return; const FVector ViewForward = ViewRotation.Vector(); ANakedDesireCharacter* Player = Cast(GetOwner()); // Among nearby interactables, find the one most centered in the player's view // that is also within interaction distance and passes CanInteract AActor* BestActor = nullptr; float BestDot = -1.f; for (const TWeakObjectPtr& WeakActor : NearbyInteractables) { AActor* Actor = WeakActor.Get(); if (!Actor) continue; if (FVector::DistSquared(Player->GetActorLocation(), Actor->GetActorLocation()) > FMath::Square(InteractionDistance)) continue; if (!IInteractable::Execute_CanInteract(Actor, Player)) continue; // Look-cone check — cheap filter before the LOS trace const FVector ToActor = (Actor->GetActorLocation() - ViewLocation).GetSafeNormal(); const float Dot = FVector::DotProduct(ViewForward, ToActor); if (Dot < LookDotThreshold || Dot <= BestDot) continue; // LOS from the pawn's eye position — not the camera, so looking over a // wall with the third-person camera does not grant interaction range if (!HasLineOfSightFromPawn(Actor)) continue; BestDot = Dot; BestActor = Actor; } if (FocusedInteractable.Get() == BestActor) return; // Restore the previously focused actor — only show a proximity hint if it is // still nearby AND in line-of-sight; hide it otherwise if (AActor* OldFocused = FocusedInteractable.Get()) { if (NearbyInteractables.Contains(OldFocused) && HasLineOfSightFromPawn(OldFocused)) IInteractable::Execute_ShowInteractionProximityHint(OldFocused); else IInteractable::Execute_HideInteractionHint(OldFocused); } FocusedInteractable = BestActor; if (BestActor) IInteractable::Execute_ShowInteractionFocusHint(BestActor); OnFocusedInteractableChanged.Broadcast(BestActor); } bool UInteractionComponent::GetOwnerViewPoint(FVector& OutLocation, FRotator& OutRotation) const { const APawn* OwnerPawn = Cast(GetOwner()); if (!OwnerPawn) return false; AController* Controller = OwnerPawn->GetController(); if (!Controller) return false; Controller->GetPlayerViewPoint(OutLocation, OutRotation); return true; } bool UInteractionComponent::HasLineOfSightFromPawn(AActor* Target) const { const APawn* OwnerPawn = Cast(GetOwner()); if (!OwnerPawn || !Target) return false; // Start from the pawn's own eye position, NOT the spring-arm camera. // This means a player who tilts the camera above a wall still cannot // reach objects on the other side — the trace originates from the character mesh. const FVector Start = OwnerPawn->GetPawnViewLocation(); const FVector End = Target->GetActorLocation(); FCollisionQueryParams Params(SCENE_QUERY_STAT(InteractionLOS), false); Params.AddIgnoredActor(GetOwner()); Params.AddIgnoredActor(Target); FHitResult Hit; const bool bBlocked = GetWorld()->LineTraceSingleByChannel(Hit, Start, End, ECC_Visibility, Params); return !bBlocked; }