Files
KingshearthLegacy/Source/KingshearthLegacy/Interaction/InteractionComponent.h
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

172 lines
7.5 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "InteractionTypes.h"
#include "InteractionComponent.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnFocusedInteractableChanged, AActor*, FocusedActor, const TArray<FInteractionOption>&, FocusedOptions);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnContextMenuOpened, const TArray<FInteractionOption>&, Options, int32, InitialHighlightedIndex);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnContextMenuHighlightChanged, int32, NewHighlightedIndex);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnContextMenuClosed);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnInteractHoldProgressChanged, float, Progress);
/**
* Drives the whole "hold E" contextual interaction system for its owning Actor:
* every tick it look-traces for an IInteractable, distinguishes tap/hold on the
* currently-focused one, and either executes an interaction directly (1-2 options)
* or drives a Context Menu (3+ options). Interactable objects need to know nothing
* about any of this - they just implement IInteractable and return their own
* FInteractionOption data.
*
* Detection is a single line trace on the "Interactable" trace channel (see
* DefaultEngine.ini - ECC_GameTraceChannel1, default response Block), not a
* proximity sweep. That channel defaults to blocking everything, exactly like
* Visibility, so ordinary geometry occludes it correctly out of the box; to make
* only part of an actor count as its "interactable hit box", set that specific
* component's Interactable trace response and turn the rest of the mesh's off
* (Ignore) in its Collision settings - no C++ changes needed per-actor.
*/
UCLASS(ClassGroup = (Interaction), Blueprintable, meta = (BlueprintSpawnableComponent))
class KINGSHEARTHLEGACY_API UInteractionComponent : public UActorComponent
{
GENERATED_BODY()
public:
UInteractionComponent();
/** Fired every time the look-traced focus changes, including to/from nothing - drives an ever-present "you can interact" prompt. */
UPROPERTY(BlueprintAssignable, Category = "Interaction")
FOnFocusedInteractableChanged OnFocusedInteractableChanged;
/** Fired when 3+ options are available and the hold threshold is reached. */
UPROPERTY(BlueprintAssignable, Category = "Interaction")
FOnContextMenuOpened OnContextMenuOpened;
/** Fired whenever CycleContextMenuOptions moves the highlighted entry. */
UPROPERTY(BlueprintAssignable, Category = "Interaction")
FOnContextMenuHighlightChanged OnContextMenuHighlightChanged;
/** Fired when the Context Menu closes, whether by confirming a choice or losing its target. */
UPROPERTY(BlueprintAssignable, Category = "Interaction")
FOnContextMenuClosed OnContextMenuClosed;
/**
* Fired every tick while the hold timer is running, with the 0-1 fraction of
* HoldThreshold elapsed so far - drives a fill bar for the "hold E" prompt.
* Fires once more with 0 the instant the hold ends, however it ends (release,
* secondary fired, Context Menu opened), so a bound bar always snaps back
* empty. Never fires at all for a 1-option target, since no hold timer is
* started for it.
*/
UPROPERTY(BlueprintAssignable, Category = "Interaction")
FOnInteractHoldProgressChanged OnInteractHoldProgressChanged;
/** Call from the Interact input's Started/pressed event. */
UFUNCTION(BlueprintCallable, Category = "Interaction")
void TryBeginInteract();
/** Call from the Interact input's Completed/released event. */
UFUNCTION(BlueprintCallable, Category = "Interaction")
void TryEndInteract();
/** Call while the Context Menu is open (e.g. from a mouse wheel axis) to move the highlighted entry. No-op otherwise. */
UFUNCTION(BlueprintCallable, Category = "Interaction")
void CycleContextMenuOptions(float Direction);
UFUNCTION(BlueprintPure, Category = "Interaction")
bool IsContextMenuOpen() const { return bContextMenuOpen; }
/** The actor currently under the look-trace, or nullptr if none. */
UFUNCTION(BlueprintPure, Category = "Interaction")
AActor* GetFocusedActor() const { return FocusedActor.Get(); }
protected:
virtual void BeginPlay() override;
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
/** Max distance of the look trace. */
UPROPERTY(EditAnywhere, Category = "Interaction")
float InteractionRange = 300.0f;
/** Trace channel the look trace queries - see the "Interactable" channel set up in DefaultEngine.ini. */
UPROPERTY(EditAnywhere, Category = "Interaction")
TEnumAsByte<ECollisionChannel> InteractionTraceChannel = ECC_GameTraceChannel1;
/** Seconds the Interact input must be held before it counts as a hold rather than a tap. */
UPROPERTY(EditAnywhere, Category = "Interaction")
float HoldThreshold = 0.4f;
/** Draws the look trace every tick (green = hit an IInteractable, red = blocked/missed) and logs focus changes. */
UPROPERTY(EditAnywhere, Category = "Interaction|Debug")
bool bShowDebugTraces = false;
/**
* Finds the look-trace target. If the owning Actor implements IInteractionLookupProvider,
* defers to its InteractionLookupLoop first - implement that interface directly on your
* Character/Actor to fully replace how a target is found (VR hand ray, a cone check, ...)
* without touching the tap/hold/menu logic below. Falls back to a line trace from the
* controller's view point along its view direction, InteractionRange long, on
* InteractionTraceChannel, whenever the interface isn't implemented or returns false.
*/
bool PerformInteractionTrace(FHitResult& OutHit) const;
/** Re-runs PerformInteractionTrace, updates FocusedActor/FocusedOptions, and broadcasts OnFocusedInteractableChanged if either changed. */
void UpdateFocus();
/** Re-derives hold progress from HoldTimerHandle every tick and broadcasts OnInteractHoldProgressChanged when it changes. */
void UpdateHoldProgress();
/** Bound to the hold timer; only ever runs when CurrentOptions.Num() >= 2. */
void HandleHoldThresholdReached();
void OpenContextMenu();
void CloseContextMenu();
/** Executes CurrentOptions[Index] against CurrentTarget, if both are still valid. */
void ExecuteOption(int32 Index);
/**
* Re-pulls GetInteractionOptions() for the still-focused actor and re-broadcasts
* OnFocusedInteractableChanged. Call right after ExecuteOption - the interaction just run
* may have changed what that same actor now offers (e.g. a chest that's now empty), and
* UpdateFocus alone would never notice since it only re-pulls options when the focused
* actor's identity changes, not when its offered options do. No-op if nothing is focused.
*/
void RefreshFocusedOptions();
void ClearCachedTarget();
/** Actor currently under the look trace and implementing IInteractable, updated every tick by UpdateFocus. */
UPROPERTY()
TWeakObjectPtr<AActor> FocusedActor;
UPROPERTY()
TArray<FInteractionOption> FocusedOptions;
/** Snapshot of FocusedActor/FocusedOptions taken when Interact was pressed - what TryEndInteract actually acts on. */
UPROPERTY()
TWeakObjectPtr<AActor> CurrentTarget;
UPROPERTY()
TArray<FInteractionOption> CurrentOptions;
UPROPERTY()
int32 HighlightedIndex = 0;
UPROPERTY()
bool bContextMenuOpen = false;
/** Set the moment the hold timer fires, so TryEndInteract knows not to also run the tap path. */
bool bHoldConsumed = false;
FTimerHandle HoldTimerHandle;
/** Last value passed to OnInteractHoldProgressChanged, so UpdateHoldProgress only broadcasts on actual change. */
float LastBroadcastHoldProgress = 0.0f;
};