"Hold-to-Interact"-System hinzugefügt
Ein neues "Hold-to-Interact"-System wurde implementiert, das es Spielern ermöglicht, durch Halten einer Taste mit Objekten zu interagieren. - `UInteractionComponent` erweitert, um den Fortschritt des Halte-Timers zu berechnen und über das neue `OnInteractHoldProgressChanged`-Event zu übertragen. - Neues Widget `UInteractionPromptWidget` eingeführt, das die Fortschrittsanzeige und Interaktionsaufforderungen modular handhabt. - Veraltete Logik aus `UInteractionContextMenuWidget` entfernt und in `UInteractionPromptWidget` ausgelagert. - Zwei neue Widgets (`WBP_InteractionBar.uasset`, `WBP_InteractionQuick.uasset`) hinzugefügt. - Logging und neue Abhängigkeiten (`USegmentedBarWidget`) integriert. - Verbesserte Modularität und Benutzererfahrung durch visuelles Feedback und klare Trennung der Logik.
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -26,6 +26,7 @@ void UInteractionComponent::TickComponent(float DeltaTime, ELevelTick TickType,
|
||||
{
|
||||
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
|
||||
UpdateFocus();
|
||||
UpdateHoldProgress();
|
||||
}
|
||||
|
||||
bool UInteractionComponent::PerformInteractionTrace(FHitResult& OutHit) const
|
||||
@@ -120,6 +121,22 @@ void UInteractionComponent::UpdateFocus()
|
||||
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();
|
||||
|
||||
@@ -11,6 +11,7 @@ DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnFocusedInteractableChanged, AAct
|
||||
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:
|
||||
@@ -53,6 +54,17 @@ public:
|
||||
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();
|
||||
@@ -106,6 +118,9 @@ protected:
|
||||
/** 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();
|
||||
|
||||
@@ -141,4 +156,7 @@ protected:
|
||||
bool bHoldConsumed = false;
|
||||
|
||||
FTimerHandle HoldTimerHandle;
|
||||
|
||||
/** Last value passed to OnInteractHoldProgressChanged, so UpdateHoldProgress only broadcasts on actual change. */
|
||||
float LastBroadcastHoldProgress = 0.0f;
|
||||
};
|
||||
|
||||
@@ -2,37 +2,15 @@
|
||||
|
||||
#include "InteractionContextMenuWidget.h"
|
||||
#include "InteractionOptionListItem.h"
|
||||
#include "InteractionPromptWidget.h"
|
||||
#include "Interaction/InteractionComponent.h"
|
||||
#include "Interaction/Interactable.h"
|
||||
#include "Components/ListView.h"
|
||||
#include "Components/TextBlock.h"
|
||||
#include "Components/Widget.h"
|
||||
#include "KingshearthLegacy.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
/** Toggles TextContainer when bound (so a background living alongside PromptText hides with it), else PromptText itself. */
|
||||
void SetPromptVisible(UWidget* TextContainer, UTextBlock* PromptText, bool bVisible)
|
||||
{
|
||||
const ESlateVisibility Visibility = bVisible ? ESlateVisibility::HitTestInvisible : ESlateVisibility::Collapsed;
|
||||
|
||||
if (TextContainer)
|
||||
{
|
||||
TextContainer->SetVisibility(Visibility);
|
||||
}
|
||||
else if (PromptText)
|
||||
{
|
||||
PromptText->SetVisibility(Visibility);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UInteractionContextMenuWidget::NativeConstruct()
|
||||
{
|
||||
Super::NativeConstruct();
|
||||
|
||||
SetPromptVisible(TextContainer, PromptText, false);
|
||||
|
||||
if (OptionsList)
|
||||
{
|
||||
OptionsList->SetVisibility(ESlateVisibility::Collapsed);
|
||||
@@ -43,7 +21,6 @@ void UInteractionContextMenuWidget::InitializeInteraction(UInteractionComponent*
|
||||
{
|
||||
if (InteractionComponent)
|
||||
{
|
||||
InteractionComponent->OnFocusedInteractableChanged.RemoveDynamic(this, &UInteractionContextMenuWidget::HandleFocusedInteractableChanged);
|
||||
InteractionComponent->OnContextMenuOpened.RemoveDynamic(this, &UInteractionContextMenuWidget::HandleContextMenuOpened);
|
||||
InteractionComponent->OnContextMenuHighlightChanged.RemoveDynamic(this, &UInteractionContextMenuWidget::HandleContextMenuHighlightChanged);
|
||||
InteractionComponent->OnContextMenuClosed.RemoveDynamic(this, &UInteractionContextMenuWidget::HandleContextMenuClosed);
|
||||
@@ -53,72 +30,19 @@ void UInteractionContextMenuWidget::InitializeInteraction(UInteractionComponent*
|
||||
|
||||
if (InteractionComponent)
|
||||
{
|
||||
InteractionComponent->OnFocusedInteractableChanged.AddDynamic(this, &UInteractionContextMenuWidget::HandleFocusedInteractableChanged);
|
||||
InteractionComponent->OnContextMenuOpened.AddDynamic(this, &UInteractionContextMenuWidget::HandleContextMenuOpened);
|
||||
InteractionComponent->OnContextMenuHighlightChanged.AddDynamic(this, &UInteractionContextMenuWidget::HandleContextMenuHighlightChanged);
|
||||
InteractionComponent->OnContextMenuClosed.AddDynamic(this, &UInteractionContextMenuWidget::HandleContextMenuClosed);
|
||||
}
|
||||
}
|
||||
|
||||
void UInteractionContextMenuWidget::HandleFocusedInteractableChanged(AActor* FocusedActor, const TArray<FInteractionOption>& Options)
|
||||
{
|
||||
LastFocusedActor = FocusedActor;
|
||||
LastFocusedOptions = Options;
|
||||
UpdatePromptVisibility();
|
||||
}
|
||||
UE_LOG(LogKingshearthLegacy, Log, TEXT("[Interaction] ContextMenu widget InitializeInteraction: OptionsList bound: %s, WBP_InteractionQuick bound: %s"),
|
||||
OptionsList ? TEXT("yes") : TEXT("NO"),
|
||||
WBP_InteractionQuick ? TEXT("yes") : TEXT("NO - check its Name in the Designer tree matches the native property, and that it's reparented to UInteractionPromptWidget"));
|
||||
|
||||
void UInteractionContextMenuWidget::UpdatePromptVisibility()
|
||||
if (WBP_InteractionQuick)
|
||||
{
|
||||
if (!PromptText)
|
||||
{
|
||||
return;
|
||||
WBP_InteractionQuick->InitializeInteraction(InInteractionComponent);
|
||||
}
|
||||
|
||||
// The Context Menu owns the screen while it's open - keep the ever-present prompt
|
||||
// out of its way until OnContextMenuClosed brings us back here.
|
||||
if (InteractionComponent && InteractionComponent->IsContextMenuOpen())
|
||||
{
|
||||
SetPromptVisible(TextContainer, PromptText, false);
|
||||
return;
|
||||
}
|
||||
|
||||
AActor* FocusedActor = LastFocusedActor.Get();
|
||||
if (!FocusedActor || LastFocusedOptions.Num() == 0)
|
||||
{
|
||||
SetPromptVisible(TextContainer, PromptText, false);
|
||||
return;
|
||||
}
|
||||
|
||||
// "{Verb} with {ObjectName}" on line one (or PromptOverride verbatim, if the primary
|
||||
// option set one), "Press E" always, then a hint for the hold behaviour only when
|
||||
// there is one (2 options = secondary, 3+ = Context Menu).
|
||||
const FInteractionOption& PrimaryOption = LastFocusedOptions[0];
|
||||
FString Prompt;
|
||||
if (!PrimaryOption.PromptOverride.IsEmpty())
|
||||
{
|
||||
Prompt = PrimaryOption.PromptOverride.ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
const FText ObjectName = IInteractable::Execute_GetInteractableDisplayName(FocusedActor);
|
||||
Prompt = ObjectName.IsEmpty()
|
||||
? PrimaryOption.DisplayName.ToString()
|
||||
: FString::Printf(TEXT("%s with %s"), *PrimaryOption.DisplayName.ToString(), *ObjectName.ToString());
|
||||
}
|
||||
|
||||
Prompt += TEXT("\nPress E");
|
||||
|
||||
if (LastFocusedOptions.Num() == 2)
|
||||
{
|
||||
Prompt += TEXT("\nHold E for secondary interaction");
|
||||
}
|
||||
else if (LastFocusedOptions.Num() >= 3)
|
||||
{
|
||||
Prompt += TEXT("\nHold E for Context Menu");
|
||||
}
|
||||
|
||||
PromptText->SetText(FText::FromString(Prompt));
|
||||
SetPromptVisible(TextContainer, PromptText, true);
|
||||
}
|
||||
|
||||
void UInteractionContextMenuWidget::PopulateOptionsList(const TArray<FInteractionOption>& Options)
|
||||
@@ -175,8 +99,6 @@ void UInteractionContextMenuWidget::HandleContextMenuOpened(const TArray<FIntera
|
||||
{
|
||||
OptionsList->SetVisibility(ESlateVisibility::Visible);
|
||||
}
|
||||
|
||||
UpdatePromptVisibility();
|
||||
}
|
||||
|
||||
void UInteractionContextMenuWidget::HandleContextMenuHighlightChanged(int32 NewHighlightedIndex)
|
||||
@@ -194,6 +116,4 @@ void UInteractionContextMenuWidget::HandleContextMenuClosed()
|
||||
OptionsList->ClearListItems();
|
||||
OptionsList->SetVisibility(ESlateVisibility::Collapsed);
|
||||
}
|
||||
|
||||
UpdatePromptVisibility();
|
||||
}
|
||||
|
||||
@@ -9,20 +9,18 @@
|
||||
|
||||
class UInteractionComponent;
|
||||
class UInteractionOptionListItem;
|
||||
class UInteractionPromptWidget;
|
||||
class UListView;
|
||||
class UTextBlock;
|
||||
class UWidget;
|
||||
|
||||
/**
|
||||
* One widget covering both interaction UI needs so you only wire up a single thing
|
||||
* in WBP_PlayerHUD: an ever-present "you're looking at something" prompt (PromptText,
|
||||
* driven by OnFocusedInteractableChanged - shows any time the look trace is on an
|
||||
* IInteractable, regardless of option count or whether Interact is even pressed) and
|
||||
* the Context Menu shown while holding Interact on a 3+-option IInteractable
|
||||
* (OptionsList, driven by OnContextMenuOpened/HighlightChanged/Closed). Both bound
|
||||
* widgets are optional and toggle their own visibility independently - add whichever
|
||||
* you want in the Designer, named exactly as below; all population/highlight/show-hide
|
||||
* logic lives here in C++.
|
||||
* The Context Menu shown while holding Interact on a 3+-option IInteractable
|
||||
* (OptionsList, driven by OnContextMenuOpened/HighlightChanged/Closed). The
|
||||
* ever-present "you're looking at something" prompt lives in a separate
|
||||
* nested UInteractionPromptWidget instance (e.g. WBP_InteractionQuick) -
|
||||
* BindWidget can only see direct nodes of this widget's own tree, not reach
|
||||
* inside another UserWidget's tree, so that prompt/slider logic can't live
|
||||
* here; InitializeInteraction forwards to it instead, so the HUD only needs
|
||||
* to call this one entry point.
|
||||
*/
|
||||
UCLASS(abstract)
|
||||
class UInteractionContextMenuWidget : public UUserWidget
|
||||
@@ -31,7 +29,7 @@ class UInteractionContextMenuWidget : public UUserWidget
|
||||
|
||||
public:
|
||||
|
||||
/** Call once, right after creation (see UPlayerHUDWidget::InitializeHUD), to bind to InInteractionComponent's delegates. */
|
||||
/** Call once, right after creation (see UPlayerHUDWidget::InitializeHUD), to bind to InInteractionComponent's delegates, including forwarding to the nested WBP_InteractionQuick prompt widget. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Interaction")
|
||||
void InitializeInteraction(UInteractionComponent* InInteractionComponent);
|
||||
|
||||
@@ -45,9 +43,6 @@ protected:
|
||||
/** Selects ListItems[Index] on OptionsList, which drives each row's IUserObjectListEntry::NativeOnItemSelectionChanged. */
|
||||
void SelectHighlighted(int32 Index);
|
||||
|
||||
UFUNCTION()
|
||||
void HandleFocusedInteractableChanged(AActor* FocusedActor, const TArray<FInteractionOption>& Options);
|
||||
|
||||
UFUNCTION()
|
||||
void HandleContextMenuOpened(const TArray<FInteractionOption>& Options, int32 InitialHighlightedIndex);
|
||||
|
||||
@@ -57,19 +52,6 @@ protected:
|
||||
UFUNCTION()
|
||||
void HandleContextMenuClosed();
|
||||
|
||||
/** Ever-present prompt - shows the primary option's DisplayName any time the look trace is on an IInteractable. Optional, place it in the WBP subclass's Designer tree. */
|
||||
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional = true))
|
||||
TObjectPtr<UTextBlock> PromptText;
|
||||
|
||||
/**
|
||||
* Optional wrapper around PromptText (e.g. a panel/border providing its background) named
|
||||
* exactly "TextContainer" in the Designer. When bound, its visibility is toggled instead of
|
||||
* PromptText's own - so a background that lives alongside PromptText, not inside it, actually
|
||||
* hides too. Falls back to toggling PromptText directly when this isn't bound.
|
||||
*/
|
||||
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional = true))
|
||||
TObjectPtr<UWidget> TextContainer;
|
||||
|
||||
/**
|
||||
* The Context Menu's option list. Optional, place it in the WBP subclass's Designer
|
||||
* tree as a ListView, single-selection, with its Entry Widget Class set to a WBP
|
||||
@@ -79,6 +61,15 @@ protected:
|
||||
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional = true))
|
||||
TObjectPtr<UListView> OptionsList;
|
||||
|
||||
/**
|
||||
* The ever-present prompt + hold-progress bar, nested as a WBP_InteractionQuick
|
||||
* instance (a UInteractionPromptWidget subclass) named exactly "WBP_InteractionQuick"
|
||||
* in the Designer tree. Optional - InitializeInteraction just skips forwarding to it
|
||||
* when unbound.
|
||||
*/
|
||||
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional = true))
|
||||
TObjectPtr<UInteractionPromptWidget> WBP_InteractionQuick;
|
||||
|
||||
UPROPERTY()
|
||||
TArray<FInteractionOption> CachedOptions;
|
||||
|
||||
@@ -88,14 +79,4 @@ protected:
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<UInteractionComponent> InteractionComponent;
|
||||
|
||||
private:
|
||||
|
||||
/** Rebuilds PromptText from LastFocused*, collapsing it while the Context Menu is open. Called on focus change and on menu close. */
|
||||
void UpdatePromptVisibility();
|
||||
|
||||
TWeakObjectPtr<AActor> LastFocusedActor;
|
||||
|
||||
UPROPERTY()
|
||||
TArray<FInteractionOption> LastFocusedOptions;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#include "InteractionPromptWidget.h"
|
||||
#include "Interaction/InteractionComponent.h"
|
||||
#include "Interaction/Interactable.h"
|
||||
#include "Components/TextBlock.h"
|
||||
#include "Components/Widget.h"
|
||||
#include "SegmentedBarWidget.h"
|
||||
#include "KingshearthLegacy.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
/** Toggles TextContainer when bound (so a background living alongside PromptText hides with it), else PromptText itself. */
|
||||
void SetPromptVisible(UWidget* TextContainer, UTextBlock* PromptText, bool bVisible)
|
||||
{
|
||||
const ESlateVisibility Visibility = bVisible ? ESlateVisibility::HitTestInvisible : ESlateVisibility::Collapsed;
|
||||
|
||||
if (TextContainer)
|
||||
{
|
||||
TextContainer->SetVisibility(Visibility);
|
||||
}
|
||||
else if (PromptText)
|
||||
{
|
||||
PromptText->SetVisibility(Visibility);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UInteractionPromptWidget::NativeConstruct()
|
||||
{
|
||||
Super::NativeConstruct();
|
||||
SetPromptVisible(TextContainer, PromptText, false);
|
||||
if (InfoText)
|
||||
{
|
||||
InfoText->SetVisibility(ESlateVisibility::Collapsed);
|
||||
}
|
||||
if (WBP_InteractionsSlider)
|
||||
{
|
||||
WBP_InteractionsSlider->SetVisibility(ESlateVisibility::Collapsed);
|
||||
// USegmentedBarWidget defaults to full (it's shared with the health/stamina bars), but
|
||||
// the hold bar should start empty and only fill as OnInteractHoldProgressChanged reports.
|
||||
WBP_InteractionsSlider->SetSegments(0.0f, 0.0f);
|
||||
}
|
||||
}
|
||||
|
||||
void UInteractionPromptWidget::InitializeInteraction(UInteractionComponent* InInteractionComponent)
|
||||
{
|
||||
UE_LOG(LogKingshearthLegacy, Log, TEXT("[Interaction] Prompt widget InitializeInteraction: component=%s, PromptText bound: %s, InfoText bound: %s, TextContainer bound: %s, Slider bound: %s"),
|
||||
*GetNameSafe(InInteractionComponent),
|
||||
PromptText ? TEXT("yes") : TEXT("NO"),
|
||||
InfoText ? TEXT("yes") : TEXT("NO"),
|
||||
TextContainer ? TEXT("yes") : TEXT("NO"),
|
||||
WBP_InteractionsSlider ? TEXT("yes") : TEXT("NO"));
|
||||
|
||||
if (InteractionComponent)
|
||||
{
|
||||
InteractionComponent->OnFocusedInteractableChanged.RemoveDynamic(this, &UInteractionPromptWidget::HandleFocusedInteractableChanged);
|
||||
InteractionComponent->OnContextMenuOpened.RemoveDynamic(this, &UInteractionPromptWidget::HandleContextMenuOpened);
|
||||
InteractionComponent->OnContextMenuClosed.RemoveDynamic(this, &UInteractionPromptWidget::HandleContextMenuClosed);
|
||||
InteractionComponent->OnInteractHoldProgressChanged.RemoveDynamic(this, &UInteractionPromptWidget::HandleHoldProgressChanged);
|
||||
}
|
||||
|
||||
InteractionComponent = InInteractionComponent;
|
||||
|
||||
if (InteractionComponent)
|
||||
{
|
||||
InteractionComponent->OnFocusedInteractableChanged.AddDynamic(this, &UInteractionPromptWidget::HandleFocusedInteractableChanged);
|
||||
InteractionComponent->OnContextMenuOpened.AddDynamic(this, &UInteractionPromptWidget::HandleContextMenuOpened);
|
||||
InteractionComponent->OnContextMenuClosed.AddDynamic(this, &UInteractionPromptWidget::HandleContextMenuClosed);
|
||||
InteractionComponent->OnInteractHoldProgressChanged.AddDynamic(this, &UInteractionPromptWidget::HandleHoldProgressChanged);
|
||||
}
|
||||
}
|
||||
|
||||
void UInteractionPromptWidget::HandleHoldProgressChanged(float Progress)
|
||||
{
|
||||
if (WBP_InteractionsSlider)
|
||||
{
|
||||
WBP_InteractionsSlider->SetSegments(Progress, 0.0f);
|
||||
}
|
||||
}
|
||||
|
||||
void UInteractionPromptWidget::HandleFocusedInteractableChanged(AActor* FocusedActor, const TArray<FInteractionOption>& Options)
|
||||
{
|
||||
UE_LOG(LogKingshearthLegacy, Log, TEXT("[Interaction] Prompt widget HandleFocusedInteractableChanged: actor='%s' options=%d"),
|
||||
*GetNameSafe(FocusedActor), Options.Num());
|
||||
|
||||
LastFocusedActor = FocusedActor;
|
||||
LastFocusedOptions = Options;
|
||||
UpdatePromptVisibility();
|
||||
}
|
||||
|
||||
void UInteractionPromptWidget::HandleContextMenuOpened(const TArray<FInteractionOption>& Options, int32 InitialHighlightedIndex)
|
||||
{
|
||||
UpdatePromptVisibility();
|
||||
}
|
||||
|
||||
void UInteractionPromptWidget::HandleContextMenuClosed()
|
||||
{
|
||||
UpdatePromptVisibility();
|
||||
}
|
||||
|
||||
void UInteractionPromptWidget::UpdatePromptVisibility()
|
||||
{
|
||||
if (!PromptText)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const auto HideAll = [this]()
|
||||
{
|
||||
SetPromptVisible(TextContainer, PromptText, false);
|
||||
if (InfoText)
|
||||
{
|
||||
InfoText->SetVisibility(ESlateVisibility::Collapsed);
|
||||
}
|
||||
if (WBP_InteractionsSlider)
|
||||
{
|
||||
WBP_InteractionsSlider->SetVisibility(ESlateVisibility::Collapsed);
|
||||
}
|
||||
};
|
||||
|
||||
// The Context Menu owns the screen while it's open - keep the ever-present prompt
|
||||
// out of its way until OnContextMenuClosed brings us back here.
|
||||
if (InteractionComponent && InteractionComponent->IsContextMenuOpen())
|
||||
{
|
||||
HideAll();
|
||||
return;
|
||||
}
|
||||
|
||||
AActor* FocusedActor = LastFocusedActor.Get();
|
||||
if (!FocusedActor || LastFocusedOptions.Num() == 0)
|
||||
{
|
||||
HideAll();
|
||||
return;
|
||||
}
|
||||
|
||||
// Primary line: "{Verb} with {ObjectName}" (or PromptOverride verbatim, if the primary
|
||||
// option set one). Hint line: "Press E" always, plus a hint for the hold behaviour only
|
||||
// when there is one (2 options = secondary, 3+ = Context Menu).
|
||||
const FInteractionOption& PrimaryOption = LastFocusedOptions[0];
|
||||
FString Prompt;
|
||||
if (!PrimaryOption.PromptOverride.IsEmpty())
|
||||
{
|
||||
Prompt = PrimaryOption.PromptOverride.ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
const FText ObjectName = IInteractable::Execute_GetInteractableDisplayName(FocusedActor);
|
||||
Prompt = ObjectName.IsEmpty()
|
||||
? PrimaryOption.DisplayName.ToString()
|
||||
: FString::Printf(TEXT("%s with %s"), *PrimaryOption.DisplayName.ToString(), *ObjectName.ToString());
|
||||
}
|
||||
|
||||
FString Hint = TEXT("Press E");
|
||||
if (LastFocusedOptions.Num() == 2)
|
||||
{
|
||||
Hint += TEXT("\nHold E for secondary interaction");
|
||||
}
|
||||
else if (LastFocusedOptions.Num() >= 3)
|
||||
{
|
||||
Hint += TEXT("\nHold E for Context Menu");
|
||||
}
|
||||
|
||||
PromptText->SetText(FText::FromString(Prompt));
|
||||
SetPromptVisible(TextContainer, PromptText, true);
|
||||
|
||||
if (InfoText)
|
||||
{
|
||||
InfoText->SetText(FText::FromString(Hint));
|
||||
InfoText->SetVisibility(ESlateVisibility::HitTestInvisible);
|
||||
}
|
||||
|
||||
if (WBP_InteractionsSlider)
|
||||
{
|
||||
WBP_InteractionsSlider->SetVisibility(ESlateVisibility::HitTestInvisible);
|
||||
WBP_InteractionsSlider->SetSegments(0.0f, 0.0f);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Blueprint/UserWidget.h"
|
||||
#include "Interaction/InteractionTypes.h"
|
||||
#include "InteractionPromptWidget.generated.h"
|
||||
|
||||
class UInteractionComponent;
|
||||
class USegmentedBarWidget;
|
||||
class UTextBlock;
|
||||
class UWidget;
|
||||
|
||||
/**
|
||||
* The ever-present "you're looking at something" prompt (PromptText, driven by
|
||||
* OnFocusedInteractableChanged - shows any time the look trace is on an
|
||||
* IInteractable, regardless of option count or whether Interact is even
|
||||
* pressed) plus an optional hold-to-interact fill bar. Kept as its own
|
||||
* UserWidget (e.g. WBP_InteractionQuick) rather than folded into
|
||||
* UInteractionContextMenuWidget so it can be laid out and nested independently
|
||||
* of the Context Menu's option list - see UInteractionContextMenuWidget's
|
||||
* WBP_InteractionQuick binding, which forwards InitializeInteraction here.
|
||||
*/
|
||||
UCLASS(abstract)
|
||||
class UInteractionPromptWidget : public UUserWidget
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
|
||||
/** Call once, right after creation, to bind to InInteractionComponent's delegates. Safe to call again to rebind to a different component. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Interaction")
|
||||
void InitializeInteraction(UInteractionComponent* InInteractionComponent);
|
||||
|
||||
protected:
|
||||
|
||||
virtual void NativeConstruct() override;
|
||||
|
||||
UFUNCTION()
|
||||
void HandleFocusedInteractableChanged(AActor* FocusedActor, const TArray<FInteractionOption>& Options);
|
||||
|
||||
UFUNCTION()
|
||||
void HandleContextMenuOpened(const TArray<FInteractionOption>& Options, int32 InitialHighlightedIndex);
|
||||
|
||||
UFUNCTION()
|
||||
void HandleContextMenuClosed();
|
||||
|
||||
UFUNCTION()
|
||||
void HandleHoldProgressChanged(float Progress);
|
||||
|
||||
/** Ever-present prompt's primary line - "{Verb} with {ObjectName}" (or PromptOverride verbatim). Named exactly "PromptText" in the Designer. */
|
||||
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional = true))
|
||||
TObjectPtr<UTextBlock> PromptText;
|
||||
|
||||
/**
|
||||
* Optional wrapper around PromptText (e.g. a panel/border providing its background, or the
|
||||
* Overlay it shares with WBP_InteractionsSlider) named exactly "TextContainer" in the
|
||||
* Designer. When bound, its visibility is toggled instead of PromptText's own - so a
|
||||
* background/slider living alongside PromptText, not inside it, actually hides too. Falls
|
||||
* back to toggling PromptText directly when this isn't bound.
|
||||
*/
|
||||
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional = true))
|
||||
TObjectPtr<UWidget> TextContainer;
|
||||
|
||||
/**
|
||||
* Optional second line - "Press E" plus a hint for the hold behaviour (2 options =
|
||||
* secondary, 3+ = Context Menu). Named exactly "InfoText" in the Designer. Kept separate
|
||||
* from PromptText so it can sit outside TextContainer (e.g. below the slider bar rather
|
||||
* than layered on top of it). Toggles visibility on its own, in lockstep with PromptText.
|
||||
*/
|
||||
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional = true))
|
||||
TObjectPtr<UTextBlock> InfoText;
|
||||
|
||||
/**
|
||||
* Optional hold-to-interact fill bar - drop a WBP_SegmentedBar instance into the
|
||||
* Designer tree named exactly "WBP_InteractionsSlider" and it fills 0 to 1 while
|
||||
* holding E on a 2+-option target, driven by InteractionComponent's
|
||||
* OnInteractHoldProgressChanged. Left unbound, holding just does nothing visually.
|
||||
*/
|
||||
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional = true))
|
||||
TObjectPtr<USegmentedBarWidget> WBP_InteractionsSlider;
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<UInteractionComponent> InteractionComponent;
|
||||
|
||||
private:
|
||||
|
||||
/** Rebuilds PromptText from LastFocused*, collapsing it while the Context Menu is open. */
|
||||
void UpdatePromptVisibility();
|
||||
|
||||
TWeakObjectPtr<AActor> LastFocusedActor;
|
||||
|
||||
UPROPERTY()
|
||||
TArray<FInteractionOption> LastFocusedOptions;
|
||||
};
|
||||
Reference in New Issue
Block a user