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.
259 lines
6.8 KiB
C++
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;
|
|
}
|