Contextual interaction system #6
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);
|
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
|
||||||
UpdateFocus();
|
UpdateFocus();
|
||||||
|
UpdateHoldProgress();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool UInteractionComponent::PerformInteractionTrace(FHitResult& OutHit) const
|
bool UInteractionComponent::PerformInteractionTrace(FHitResult& OutHit) const
|
||||||
@@ -120,6 +121,22 @@ void UInteractionComponent::UpdateFocus()
|
|||||||
OnFocusedInteractableChanged.Broadcast(NewFocusedActor, FocusedOptions);
|
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()
|
void UInteractionComponent::TryBeginInteract()
|
||||||
{
|
{
|
||||||
UpdateFocus();
|
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_TwoParams(FOnContextMenuOpened, const TArray<FInteractionOption>&, Options, int32, InitialHighlightedIndex);
|
||||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnContextMenuHighlightChanged, int32, NewHighlightedIndex);
|
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnContextMenuHighlightChanged, int32, NewHighlightedIndex);
|
||||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnContextMenuClosed);
|
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:
|
* Drives the whole "hold E" contextual interaction system for its owning Actor:
|
||||||
@@ -53,6 +54,17 @@ public:
|
|||||||
UPROPERTY(BlueprintAssignable, Category = "Interaction")
|
UPROPERTY(BlueprintAssignable, Category = "Interaction")
|
||||||
FOnContextMenuClosed OnContextMenuClosed;
|
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. */
|
/** Call from the Interact input's Started/pressed event. */
|
||||||
UFUNCTION(BlueprintCallable, Category = "Interaction")
|
UFUNCTION(BlueprintCallable, Category = "Interaction")
|
||||||
void TryBeginInteract();
|
void TryBeginInteract();
|
||||||
@@ -106,6 +118,9 @@ protected:
|
|||||||
/** Re-runs PerformInteractionTrace, updates FocusedActor/FocusedOptions, and broadcasts OnFocusedInteractableChanged if either changed. */
|
/** Re-runs PerformInteractionTrace, updates FocusedActor/FocusedOptions, and broadcasts OnFocusedInteractableChanged if either changed. */
|
||||||
void UpdateFocus();
|
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. */
|
/** Bound to the hold timer; only ever runs when CurrentOptions.Num() >= 2. */
|
||||||
void HandleHoldThresholdReached();
|
void HandleHoldThresholdReached();
|
||||||
|
|
||||||
@@ -141,4 +156,7 @@ protected:
|
|||||||
bool bHoldConsumed = false;
|
bool bHoldConsumed = false;
|
||||||
|
|
||||||
FTimerHandle HoldTimerHandle;
|
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 "InteractionContextMenuWidget.h"
|
||||||
#include "InteractionOptionListItem.h"
|
#include "InteractionOptionListItem.h"
|
||||||
|
#include "InteractionPromptWidget.h"
|
||||||
#include "Interaction/InteractionComponent.h"
|
#include "Interaction/InteractionComponent.h"
|
||||||
#include "Interaction/Interactable.h"
|
|
||||||
#include "Components/ListView.h"
|
#include "Components/ListView.h"
|
||||||
#include "Components/TextBlock.h"
|
|
||||||
#include "Components/Widget.h"
|
|
||||||
#include "KingshearthLegacy.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()
|
void UInteractionContextMenuWidget::NativeConstruct()
|
||||||
{
|
{
|
||||||
Super::NativeConstruct();
|
Super::NativeConstruct();
|
||||||
|
|
||||||
SetPromptVisible(TextContainer, PromptText, false);
|
|
||||||
|
|
||||||
if (OptionsList)
|
if (OptionsList)
|
||||||
{
|
{
|
||||||
OptionsList->SetVisibility(ESlateVisibility::Collapsed);
|
OptionsList->SetVisibility(ESlateVisibility::Collapsed);
|
||||||
@@ -43,7 +21,6 @@ void UInteractionContextMenuWidget::InitializeInteraction(UInteractionComponent*
|
|||||||
{
|
{
|
||||||
if (InteractionComponent)
|
if (InteractionComponent)
|
||||||
{
|
{
|
||||||
InteractionComponent->OnFocusedInteractableChanged.RemoveDynamic(this, &UInteractionContextMenuWidget::HandleFocusedInteractableChanged);
|
|
||||||
InteractionComponent->OnContextMenuOpened.RemoveDynamic(this, &UInteractionContextMenuWidget::HandleContextMenuOpened);
|
InteractionComponent->OnContextMenuOpened.RemoveDynamic(this, &UInteractionContextMenuWidget::HandleContextMenuOpened);
|
||||||
InteractionComponent->OnContextMenuHighlightChanged.RemoveDynamic(this, &UInteractionContextMenuWidget::HandleContextMenuHighlightChanged);
|
InteractionComponent->OnContextMenuHighlightChanged.RemoveDynamic(this, &UInteractionContextMenuWidget::HandleContextMenuHighlightChanged);
|
||||||
InteractionComponent->OnContextMenuClosed.RemoveDynamic(this, &UInteractionContextMenuWidget::HandleContextMenuClosed);
|
InteractionComponent->OnContextMenuClosed.RemoveDynamic(this, &UInteractionContextMenuWidget::HandleContextMenuClosed);
|
||||||
@@ -53,72 +30,19 @@ void UInteractionContextMenuWidget::InitializeInteraction(UInteractionComponent*
|
|||||||
|
|
||||||
if (InteractionComponent)
|
if (InteractionComponent)
|
||||||
{
|
{
|
||||||
InteractionComponent->OnFocusedInteractableChanged.AddDynamic(this, &UInteractionContextMenuWidget::HandleFocusedInteractableChanged);
|
|
||||||
InteractionComponent->OnContextMenuOpened.AddDynamic(this, &UInteractionContextMenuWidget::HandleContextMenuOpened);
|
InteractionComponent->OnContextMenuOpened.AddDynamic(this, &UInteractionContextMenuWidget::HandleContextMenuOpened);
|
||||||
InteractionComponent->OnContextMenuHighlightChanged.AddDynamic(this, &UInteractionContextMenuWidget::HandleContextMenuHighlightChanged);
|
InteractionComponent->OnContextMenuHighlightChanged.AddDynamic(this, &UInteractionContextMenuWidget::HandleContextMenuHighlightChanged);
|
||||||
InteractionComponent->OnContextMenuClosed.AddDynamic(this, &UInteractionContextMenuWidget::HandleContextMenuClosed);
|
InteractionComponent->OnContextMenuClosed.AddDynamic(this, &UInteractionContextMenuWidget::HandleContextMenuClosed);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
void UInteractionContextMenuWidget::HandleFocusedInteractableChanged(AActor* FocusedActor, const TArray<FInteractionOption>& Options)
|
UE_LOG(LogKingshearthLegacy, Log, TEXT("[Interaction] ContextMenu widget InitializeInteraction: OptionsList bound: %s, WBP_InteractionQuick bound: %s"),
|
||||||
{
|
OptionsList ? TEXT("yes") : TEXT("NO"),
|
||||||
LastFocusedActor = FocusedActor;
|
WBP_InteractionQuick ? TEXT("yes") : TEXT("NO - check its Name in the Designer tree matches the native property, and that it's reparented to UInteractionPromptWidget"));
|
||||||
LastFocusedOptions = Options;
|
|
||||||
UpdatePromptVisibility();
|
|
||||||
}
|
|
||||||
|
|
||||||
void UInteractionContextMenuWidget::UpdatePromptVisibility()
|
if (WBP_InteractionQuick)
|
||||||
{
|
{
|
||||||
if (!PromptText)
|
WBP_InteractionQuick->InitializeInteraction(InInteractionComponent);
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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)
|
void UInteractionContextMenuWidget::PopulateOptionsList(const TArray<FInteractionOption>& Options)
|
||||||
@@ -175,8 +99,6 @@ void UInteractionContextMenuWidget::HandleContextMenuOpened(const TArray<FIntera
|
|||||||
{
|
{
|
||||||
OptionsList->SetVisibility(ESlateVisibility::Visible);
|
OptionsList->SetVisibility(ESlateVisibility::Visible);
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdatePromptVisibility();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void UInteractionContextMenuWidget::HandleContextMenuHighlightChanged(int32 NewHighlightedIndex)
|
void UInteractionContextMenuWidget::HandleContextMenuHighlightChanged(int32 NewHighlightedIndex)
|
||||||
@@ -194,6 +116,4 @@ void UInteractionContextMenuWidget::HandleContextMenuClosed()
|
|||||||
OptionsList->ClearListItems();
|
OptionsList->ClearListItems();
|
||||||
OptionsList->SetVisibility(ESlateVisibility::Collapsed);
|
OptionsList->SetVisibility(ESlateVisibility::Collapsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdatePromptVisibility();
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,20 +9,18 @@
|
|||||||
|
|
||||||
class UInteractionComponent;
|
class UInteractionComponent;
|
||||||
class UInteractionOptionListItem;
|
class UInteractionOptionListItem;
|
||||||
|
class UInteractionPromptWidget;
|
||||||
class UListView;
|
class UListView;
|
||||||
class UTextBlock;
|
|
||||||
class UWidget;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* One widget covering both interaction UI needs so you only wire up a single thing
|
* The Context Menu shown while holding Interact on a 3+-option IInteractable
|
||||||
* in WBP_PlayerHUD: an ever-present "you're looking at something" prompt (PromptText,
|
* (OptionsList, driven by OnContextMenuOpened/HighlightChanged/Closed). The
|
||||||
* driven by OnFocusedInteractableChanged - shows any time the look trace is on an
|
* ever-present "you're looking at something" prompt lives in a separate
|
||||||
* IInteractable, regardless of option count or whether Interact is even pressed) and
|
* nested UInteractionPromptWidget instance (e.g. WBP_InteractionQuick) -
|
||||||
* the Context Menu shown while holding Interact on a 3+-option IInteractable
|
* BindWidget can only see direct nodes of this widget's own tree, not reach
|
||||||
* (OptionsList, driven by OnContextMenuOpened/HighlightChanged/Closed). Both bound
|
* inside another UserWidget's tree, so that prompt/slider logic can't live
|
||||||
* widgets are optional and toggle their own visibility independently - add whichever
|
* here; InitializeInteraction forwards to it instead, so the HUD only needs
|
||||||
* you want in the Designer, named exactly as below; all population/highlight/show-hide
|
* to call this one entry point.
|
||||||
* logic lives here in C++.
|
|
||||||
*/
|
*/
|
||||||
UCLASS(abstract)
|
UCLASS(abstract)
|
||||||
class UInteractionContextMenuWidget : public UUserWidget
|
class UInteractionContextMenuWidget : public UUserWidget
|
||||||
@@ -31,7 +29,7 @@ class UInteractionContextMenuWidget : public UUserWidget
|
|||||||
|
|
||||||
public:
|
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")
|
UFUNCTION(BlueprintCallable, Category = "Interaction")
|
||||||
void InitializeInteraction(UInteractionComponent* InInteractionComponent);
|
void InitializeInteraction(UInteractionComponent* InInteractionComponent);
|
||||||
|
|
||||||
@@ -45,9 +43,6 @@ protected:
|
|||||||
/** Selects ListItems[Index] on OptionsList, which drives each row's IUserObjectListEntry::NativeOnItemSelectionChanged. */
|
/** Selects ListItems[Index] on OptionsList, which drives each row's IUserObjectListEntry::NativeOnItemSelectionChanged. */
|
||||||
void SelectHighlighted(int32 Index);
|
void SelectHighlighted(int32 Index);
|
||||||
|
|
||||||
UFUNCTION()
|
|
||||||
void HandleFocusedInteractableChanged(AActor* FocusedActor, const TArray<FInteractionOption>& Options);
|
|
||||||
|
|
||||||
UFUNCTION()
|
UFUNCTION()
|
||||||
void HandleContextMenuOpened(const TArray<FInteractionOption>& Options, int32 InitialHighlightedIndex);
|
void HandleContextMenuOpened(const TArray<FInteractionOption>& Options, int32 InitialHighlightedIndex);
|
||||||
|
|
||||||
@@ -57,19 +52,6 @@ protected:
|
|||||||
UFUNCTION()
|
UFUNCTION()
|
||||||
void HandleContextMenuClosed();
|
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
|
* 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
|
* 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))
|
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional = true))
|
||||||
TObjectPtr<UListView> OptionsList;
|
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()
|
UPROPERTY()
|
||||||
TArray<FInteractionOption> CachedOptions;
|
TArray<FInteractionOption> CachedOptions;
|
||||||
|
|
||||||
@@ -88,14 +79,4 @@ protected:
|
|||||||
|
|
||||||
UPROPERTY()
|
UPROPERTY()
|
||||||
TObjectPtr<UInteractionComponent> InteractionComponent;
|
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