Rework interaction system
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
#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<APawn>(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<ANakedDesireCharacter>(GetOwner())))
|
||||
return;
|
||||
IInteractable::Execute_Interact(Target, Cast<ANakedDesireCharacter>(GetOwner()));
|
||||
}
|
||||
|
||||
void UInteractionComponent::UpdateNearbyInteractables()
|
||||
{
|
||||
AActor* Owner = GetOwner();
|
||||
if (!Owner)
|
||||
return;
|
||||
|
||||
// Sphere overlap to find all nearby actors
|
||||
TArray<FOverlapResult> 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<TWeakObjectPtr<AActor>> NewNearby;
|
||||
for (const FOverlapResult& Overlap : Overlaps)
|
||||
{
|
||||
AActor* Actor = Overlap.GetActor();
|
||||
if (Actor && Actor != Owner && Actor->Implements<UInteractable>())
|
||||
{
|
||||
NewNearby.Add(Actor);
|
||||
}
|
||||
}
|
||||
|
||||
// Actors that left proximity: hide their hints
|
||||
for (const TWeakObjectPtr<AActor>& 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<AActor>& 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<ANakedDesireCharacter>(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<AActor>& 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<APawn>(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<APawn>(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;
|
||||
}
|
||||
Reference in New Issue
Block a user