Files
KingshearthLegacy/Source/KingshearthLegacy/Interaction/InteractionComponent.cpp
T
Dolobarsch a5eb673c6e Interaktionsoptionen und UI-Verbesserungen
Neue Methode `RefreshFocusedOptions()` hinzugefügt, um die Optionen des fokussierten Akteurs nach einer Interaktion zu aktualisieren. Änderungen an der Navigation im Kontextmenü: Begrenzte Navigation ersetzt zyklische Navigation.

`FInteractionOption` erweitert um `TextColorOverride` und `HighlightColorOverride`, um individuelle Farben für Optionen zu ermöglichen.

UI-Widgets (`InteractionOptionEntryWidget`) angepasst, um Hintergrund- und Hervorhebungsfarben dynamisch zu setzen. Hinweistext für sekundäre Interaktionen zeigt nun spezifische Optionennamen an.
2026-09-11 01:52:59 +02:00

259 lines
6.8 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "InteractionComponent.h"
#include "Interactable.h"
#include "InteractionLookupProvider.h"
#include "Engine/World.h"
#include "TimerManager.h"
#include "GameFramework/Actor.h"
#include "GameFramework/Pawn.h"
#include "GameFramework/Controller.h"
#include "DrawDebugHelpers.h"
#include "KingshearthLegacy.h"
UInteractionComponent::UInteractionComponent()
{
PrimaryComponentTick.bCanEverTick = true;
}
void UInteractionComponent::BeginPlay()
{
Super::BeginPlay();
UpdateFocus();
}
void UInteractionComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
UpdateFocus();
UpdateHoldProgress();
}
bool UInteractionComponent::PerformInteractionTrace(FHitResult& OutHit) const
{
AActor* Owner = GetOwner();
UWorld* World = GetWorld();
if (!Owner || !World)
{
return false;
}
if (Owner->Implements<UInteractionLookupProvider>())
{
// Authoritative once implemented - a miss here means "nothing found", full stop.
// Don't silently fall through to the default trace below just because this one
// came back empty; that would use a completely different (un-offset) trace and
// could contradict what the override deliberately decided.
return IInteractionLookupProvider::Execute_InteractionLookupLoop(Owner, OutHit);
}
FVector TraceStart = Owner->GetActorLocation();
FRotator TraceRotation = Owner->GetActorRotation();
if (const APawn* OwnerPawn = Cast<APawn>(Owner))
{
if (const AController* Controller = OwnerPawn->GetController())
{
Controller->GetPlayerViewPoint(TraceStart, TraceRotation);
}
else
{
TraceStart = OwnerPawn->GetPawnViewLocation();
TraceRotation = OwnerPawn->GetViewRotation();
}
}
const FVector TraceEnd = TraceStart + TraceRotation.Vector() * InteractionRange;
FCollisionQueryParams QueryParams;
QueryParams.AddIgnoredActor(Owner);
const bool bHit = World->LineTraceSingleByChannel(OutHit, TraceStart, TraceEnd, InteractionTraceChannel, QueryParams);
#if ENABLE_DRAW_DEBUG
if (bShowDebugTraces)
{
const bool bHitInteractable = bHit && OutHit.GetActor() && OutHit.GetActor()->Implements<UInteractable>();
DrawDebugLine(World, TraceStart, bHit ? OutHit.Location : TraceEnd, bHitInteractable ? FColor::Green : FColor::Red, false, 0.0f, 0, 1.5f);
if (bHit)
{
DrawDebugSphere(World, OutHit.Location, 8.0f, 12, bHitInteractable ? FColor::Green : FColor::Red, false, 0.0f);
}
}
#endif
return bHit;
}
void UInteractionComponent::UpdateFocus()
{
FHitResult Hit;
const bool bHit = PerformInteractionTrace(Hit);
const bool bImplementsInteractable = bHit && Hit.GetActor() && Hit.GetActor()->Implements<UInteractable>();
AActor* NewFocusedActor = bImplementsInteractable ? Hit.GetActor() : nullptr;
#if ENABLE_DRAW_DEBUG
if (bShowDebugTraces)
{
UE_LOG(LogKingshearthLegacy, Log, TEXT("[Interaction] Tick: bHit=%s hitActor='%s' implementsInteractable=%s currentFocused='%s'"),
bHit ? TEXT("true") : TEXT("false"),
*GetNameSafe(bHit ? Hit.GetActor() : nullptr),
bImplementsInteractable ? TEXT("true") : TEXT("false"),
*GetNameSafe(FocusedActor.Get()));
}
#endif
if (NewFocusedActor == FocusedActor.Get())
{
return;
}
FocusedActor = NewFocusedActor;
FocusedOptions = NewFocusedActor ? IInteractable::Execute_GetInteractionOptions(NewFocusedActor, GetOwner()) : TArray<FInteractionOption>();
#if ENABLE_DRAW_DEBUG
if (bShowDebugTraces)
{
UE_LOG(LogKingshearthLegacy, Log, TEXT("[Interaction] Focus changed to '%s' (%d option(s))"), *GetNameSafe(NewFocusedActor), FocusedOptions.Num());
}
#endif
OnFocusedInteractableChanged.Broadcast(NewFocusedActor, FocusedOptions);
}
void UInteractionComponent::UpdateHoldProgress()
{
const bool bTimerActive = GetWorld()->GetTimerManager().IsTimerActive(HoldTimerHandle);
const float Progress = bTimerActive && HoldThreshold > 0.0f
? FMath::Clamp(GetWorld()->GetTimerManager().GetTimerElapsed(HoldTimerHandle) / HoldThreshold, 0.0f, 1.0f)
: 0.0f;
if (FMath::IsNearlyEqual(Progress, LastBroadcastHoldProgress, KINDA_SMALL_NUMBER))
{
return;
}
LastBroadcastHoldProgress = Progress;
OnInteractHoldProgressChanged.Broadcast(Progress);
}
void UInteractionComponent::TryBeginInteract()
{
UpdateFocus();
if (!FocusedActor.IsValid() || FocusedOptions.Num() == 0)
{
return;
}
CurrentTarget = FocusedActor;
CurrentOptions = FocusedOptions;
HighlightedIndex = 0;
bHoldConsumed = false;
if (CurrentOptions.Num() >= 2)
{
GetWorld()->GetTimerManager().SetTimer(HoldTimerHandle, this, &UInteractionComponent::HandleHoldThresholdReached, HoldThreshold, false);
}
}
void UInteractionComponent::TryEndInteract()
{
GetWorld()->GetTimerManager().ClearTimer(HoldTimerHandle);
if (bContextMenuOpen)
{
ExecuteOption(HighlightedIndex);
RefreshFocusedOptions();
CloseContextMenu();
}
else if (!bHoldConsumed && CurrentOptions.Num() >= 1)
{
ExecuteOption(0);
RefreshFocusedOptions();
}
ClearCachedTarget();
}
void UInteractionComponent::CycleContextMenuOptions(float Direction)
{
if (!bContextMenuOpen || CurrentOptions.Num() == 0 || FMath::IsNearlyZero(Direction))
{
return;
}
// Scrolling up (positive Direction) moves the highlight toward the top of the list (index 0),
// matching how the mouse wheel scrolls content elsewhere - hence the flipped sign here.
const int32 Step = Direction > 0.0f ? -1 : 1;
const int32 NewHighlightedIndex = FMath::Clamp(HighlightedIndex + Step, 0, CurrentOptions.Num() - 1);
if (NewHighlightedIndex == HighlightedIndex)
{
return;
}
HighlightedIndex = NewHighlightedIndex;
OnContextMenuHighlightChanged.Broadcast(HighlightedIndex);
}
void UInteractionComponent::HandleHoldThresholdReached()
{
bHoldConsumed = true;
if (CurrentOptions.Num() == 2)
{
ExecuteOption(1);
RefreshFocusedOptions();
ClearCachedTarget();
}
else if (CurrentOptions.Num() >= 3)
{
OpenContextMenu();
}
}
void UInteractionComponent::OpenContextMenu()
{
bContextMenuOpen = true;
HighlightedIndex = 0;
OnContextMenuOpened.Broadcast(CurrentOptions, HighlightedIndex);
}
void UInteractionComponent::CloseContextMenu()
{
bContextMenuOpen = false;
OnContextMenuClosed.Broadcast();
}
void UInteractionComponent::ExecuteOption(int32 Index)
{
AActor* Target = CurrentTarget.Get();
if (!Target || !CurrentOptions.IsValidIndex(Index))
{
return;
}
IInteractable::Execute_ExecuteInteraction(Target, GetOwner(), CurrentOptions[Index].ActionID);
}
void UInteractionComponent::RefreshFocusedOptions()
{
AActor* Actor = FocusedActor.Get();
if (!Actor)
{
return;
}
FocusedOptions = IInteractable::Execute_GetInteractionOptions(Actor, GetOwner());
OnFocusedInteractableChanged.Broadcast(Actor, FocusedOptions);
}
void UInteractionComponent::ClearCachedTarget()
{
CurrentTarget = nullptr;
CurrentOptions.Reset();
HighlightedIndex = 0;
bHoldConsumed = false;
}