Rework interaction system

This commit is contained in:
2026-05-28 21:53:34 +03:00
parent 891a0744a0
commit 6f0aa5274c
26 changed files with 496 additions and 216 deletions
@@ -0,0 +1,43 @@
#pragma once
#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "Interactable.generated.h"
class ANakedDesireCharacter;
UINTERFACE(MinimalAPI, BlueprintType)
class UInteractable : public UInterface
{
GENERATED_BODY()
};
class NAKEDDESIRE_API IInteractable
{
GENERATED_BODY()
public:
// Returns true if this actor can currently be interacted with by the given player
UFUNCTION(BlueprintNativeEvent)
bool CanInteract(ANakedDesireCharacter* Player) const;
// Executes the interaction (called server-side)
UFUNCTION(BlueprintNativeEvent)
void Interact(ANakedDesireCharacter* Player);
// Text shown in the interaction prompt UI
UFUNCTION(BlueprintNativeEvent)
FText GetInteractionPrompt() const;
// Called (client-side) when this actor enters the player's proximity radius
UFUNCTION(BlueprintNativeEvent)
void ShowInteractionProximityHint();
// Called (client-side) when this actor becomes the focused interaction target
UFUNCTION(BlueprintNativeEvent)
void ShowInteractionFocusHint();
// Called (client-side) when this actor loses proximity or focus
UFUNCTION(BlueprintNativeEvent)
void HideInteractionHint();
};
@@ -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;
}
@@ -0,0 +1,64 @@
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "InteractionComponent.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnFocusedInteractableChanged, AActor*, NewFocused);
UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
class NAKEDDESIRE_API UInteractionComponent : public UActorComponent
{
GENERATED_BODY()
public:
UInteractionComponent();
virtual void BeginPlay() override;
// Call from interact input — sends server RPC if not authority
void Interact();
UFUNCTION(BlueprintPure)
AActor* GetFocusedInteractable() const { return FocusedInteractable.Get(); }
// Broadcasts whenever the focused (interactable) actor changes; nullptr = no target
UPROPERTY(BlueprintAssignable)
FOnFocusedInteractableChanged OnFocusedInteractableChanged;
protected:
// Actors within this radius receive a proximity outline
UPROPERTY(EditDefaultsOnly, Category="Interaction")
float ProximityRadius = 350.f;
// Player must be within this distance to focus an interactable (must be <= ProximityRadius)
UPROPERTY(EditDefaultsOnly, Category="Interaction")
float InteractionDistance = 150.f;
// Minimum dot product between look direction and direction to target.
// cos(20°) ≈ 0.94 — lower values allow a wider look cone.
UPROPERTY(EditDefaultsOnly, Category="Interaction")
float LookDotThreshold = 0.9f;
private:
FTimerHandle InteractionTimerHandle;
void UpdateInteraction();
TSet<TWeakObjectPtr<AActor>> NearbyInteractables;
TWeakObjectPtr<AActor> FocusedInteractable;
// Physics scan → update NearbyInteractables set → manage proximity outlines
void UpdateNearbyInteractables();
// Among nearby actors, find the one best matching look + distance conditions
void UpdateFocusedInteractable();
bool GetOwnerViewPoint(FVector& OutLocation, FRotator& OutRotation) const;
// Traces from the pawn's eye position (NOT the camera) to the target.
// Returns true only if nothing blocks the line — prevents exploiting the
// third-person camera to interact through walls by looking over them.
bool HasLineOfSightFromPawn(AActor* Target) const;
};