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.
This commit is contained in:
2026-09-11 01:52:59 +02:00
parent e4fb702a1c
commit a5eb673c6e
9 changed files with 111 additions and 6 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -164,11 +164,13 @@ void UInteractionComponent::TryEndInteract()
if (bContextMenuOpen) if (bContextMenuOpen)
{ {
ExecuteOption(HighlightedIndex); ExecuteOption(HighlightedIndex);
RefreshFocusedOptions();
CloseContextMenu(); CloseContextMenu();
} }
else if (!bHoldConsumed && CurrentOptions.Num() >= 1) else if (!bHoldConsumed && CurrentOptions.Num() >= 1)
{ {
ExecuteOption(0); ExecuteOption(0);
RefreshFocusedOptions();
} }
ClearCachedTarget(); ClearCachedTarget();
@@ -181,8 +183,17 @@ void UInteractionComponent::CycleContextMenuOptions(float Direction)
return; return;
} }
const int32 Step = Direction > 0.0f ? 1 : -1; // Scrolling up (positive Direction) moves the highlight toward the top of the list (index 0),
HighlightedIndex = (HighlightedIndex + Step + CurrentOptions.Num()) % CurrentOptions.Num(); // 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); OnContextMenuHighlightChanged.Broadcast(HighlightedIndex);
} }
@@ -193,6 +204,7 @@ void UInteractionComponent::HandleHoldThresholdReached()
if (CurrentOptions.Num() == 2) if (CurrentOptions.Num() == 2)
{ {
ExecuteOption(1); ExecuteOption(1);
RefreshFocusedOptions();
ClearCachedTarget(); ClearCachedTarget();
} }
else if (CurrentOptions.Num() >= 3) else if (CurrentOptions.Num() >= 3)
@@ -225,6 +237,18 @@ void UInteractionComponent::ExecuteOption(int32 Index)
IInteractable::Execute_ExecuteInteraction(Target, GetOwner(), CurrentOptions[Index].ActionID); 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() void UInteractionComponent::ClearCachedTarget()
{ {
CurrentTarget = nullptr; CurrentTarget = nullptr;
@@ -130,6 +130,15 @@ protected:
/** Executes CurrentOptions[Index] against CurrentTarget, if both are still valid. */ /** Executes CurrentOptions[Index] against CurrentTarget, if both are still valid. */
void ExecuteOption(int32 Index); 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(); void ClearCachedTarget();
/** Actor currently under the look trace and implementing IInteractable, updated every tick by UpdateFocus. */ /** Actor currently under the look trace and implementing IInteractable, updated every tick by UpdateFocus. */
@@ -37,4 +37,22 @@ struct FInteractionOption
*/ */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction")
FText PromptOverride; FText PromptOverride;
/**
* Optional per-option override for the Context Menu row's text color (both normal and
* highlighted state use this same color, so the indication doesn't disappear on hover) -
* e.g. red for an action you currently can't perform, gold for a special one. Overrides
* UInteractionOptionEntryWidget's NormalTextColor/HighlightedTextColor for this option
* only. Alpha 0 (the default) means "use the entry widget's own colors".
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction")
FLinearColor TextColorOverride = FLinearColor(0.0f, 0.0f, 0.0f, 0.0f);
/**
* Optional per-option override for the Context Menu row's highlight tint - overrides
* UInteractionOptionEntryWidget's HighlightColor for this option only. Alpha 0 (the
* default) means "use the entry widget's own color".
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction")
FLinearColor HighlightColorOverride = FLinearColor(0.0f, 0.0f, 0.0f, 0.0f);
}; };
@@ -11,6 +11,17 @@ void UInteractionOptionEntryWidget::NativeConstruct()
{ {
Super::NativeConstruct(); Super::NativeConstruct();
// Capture Background's Designer-authored color before ApplyOptionVisuals ever runs, so
// "no override" can fall back to it later instead of an arbitrary hardcoded default.
if (UImage* BackgroundImage = Cast<UImage>(Background.Get()))
{
DefaultBackgroundColor = BackgroundImage->GetColorAndOpacity();
}
else if (UBorder* BackgroundBorder = Cast<UBorder>(Background.Get()))
{
DefaultBackgroundColor = BackgroundBorder->GetBrushColor();
}
// The ListView only calls NativeOnItemSelectionChanged when a row's selection state // The ListView only calls NativeOnItemSelectionChanged when a row's selection state
// actually changes, so a freshly-spawned row that has never been (de)selected would // actually changes, so a freshly-spawned row that has never been (de)selected would
// otherwise keep whatever visibility Highlight has in the Designer - force it into // otherwise keep whatever visibility Highlight has in the Designer - force it into
@@ -59,26 +70,47 @@ void UInteractionOptionEntryWidget::ApplyOptionVisuals()
Icon->SetVisibility(ESlateVisibility::Collapsed); Icon->SetVisibility(ESlateVisibility::Collapsed);
} }
} }
if (Background)
{
const FLinearColor ResolvedBackgroundColor = Option.HighlightColorOverride.A > 0.0f ? Option.HighlightColorOverride : DefaultBackgroundColor;
if (UImage* BackgroundImage = Cast<UImage>(Background.Get()))
{
BackgroundImage->SetColorAndOpacity(ResolvedBackgroundColor);
}
else if (UBorder* BackgroundBorder = Cast<UBorder>(Background.Get()))
{
BackgroundBorder->SetBrushColor(ResolvedBackgroundColor);
}
}
} }
void UInteractionOptionEntryWidget::ApplyHighlightVisuals() void UInteractionOptionEntryWidget::ApplyHighlightVisuals()
{ {
// Alpha 0 (the struct default) means the option didn't request an override - fall back
// to this widget's own Style colors.
const FLinearColor ResolvedHighlightColor = Option.HighlightColorOverride.A > 0.0f ? Option.HighlightColorOverride : HighlightColor;
const FLinearColor ResolvedTextColor = Option.TextColorOverride.A > 0.0f
? Option.TextColorOverride
: (bHighlighted ? HighlightedTextColor : NormalTextColor);
if (Highlight) if (Highlight)
{ {
Highlight->SetVisibility(bHighlighted ? ESlateVisibility::HitTestInvisible : ESlateVisibility::Hidden); Highlight->SetVisibility(bHighlighted ? ESlateVisibility::HitTestInvisible : ESlateVisibility::Hidden);
if (UImage* HighlightImage = Cast<UImage>(Highlight.Get())) if (UImage* HighlightImage = Cast<UImage>(Highlight.Get()))
{ {
HighlightImage->SetColorAndOpacity(HighlightColor); HighlightImage->SetColorAndOpacity(ResolvedHighlightColor);
} }
else if (UBorder* HighlightBorder = Cast<UBorder>(Highlight.Get())) else if (UBorder* HighlightBorder = Cast<UBorder>(Highlight.Get()))
{ {
HighlightBorder->SetBrushColor(HighlightColor); HighlightBorder->SetBrushColor(ResolvedHighlightColor);
} }
} }
if (InteractionText) if (InteractionText)
{ {
InteractionText->SetColorAndOpacity(bHighlighted ? HighlightedTextColor : NormalTextColor); InteractionText->SetColorAndOpacity(ResolvedTextColor);
} }
} }
@@ -61,6 +61,17 @@ protected:
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional = true)) UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional = true))
TObjectPtr<UWidget> Highlight; TObjectPtr<UWidget> Highlight;
/**
* Optional persistent row background - bind a Border or an Image named exactly
* "Background" in the Designer. Unlike Highlight, this isn't gated on selection - it's
* tinted with the option's HighlightColorOverride (from FInteractionOption) whenever one
* is set, so a "can't do this" or "special" row stays visually marked regardless of
* whether it's currently highlighted. Left at its Designer-authored appearance when the
* option has no override.
*/
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional = true))
TObjectPtr<UWidget> Background;
/** Tint applied to Highlight while this row is highlighted. */ /** Tint applied to Highlight while this row is highlighted. */
UPROPERTY(EditAnywhere, Category = "Interaction|Style") UPROPERTY(EditAnywhere, Category = "Interaction|Style")
FLinearColor HighlightColor = FLinearColor(1.0f, 0.85f, 0.3f, 1.0f); FLinearColor HighlightColor = FLinearColor(1.0f, 0.85f, 0.3f, 1.0f);
@@ -78,4 +89,15 @@ protected:
UPROPERTY(BlueprintReadOnly, Category = "Interaction") UPROPERTY(BlueprintReadOnly, Category = "Interaction")
bool bHighlighted = false; bool bHighlighted = false;
private:
/**
* Background's Designer-authored color, captured once in NativeConstruct before any
* option is ever assigned. Rows are recycled by the ListView, so a row previously tinted
* by one option's HighlightColorOverride needs an explicit color to fall back to when
* reassigned to a different, unconfigured option - rather than just leaving whatever
* tint the recycled widget last had.
*/
FLinearColor DefaultBackgroundColor = FLinearColor::White;
}; };
@@ -154,7 +154,7 @@ void UInteractionPromptWidget::UpdatePromptVisibility()
FString Hint = TEXT("Press E"); FString Hint = TEXT("Press E");
if (LastFocusedOptions.Num() == 2) if (LastFocusedOptions.Num() == 2)
{ {
Hint += TEXT("\nHold E for secondary interaction"); Hint += FString::Printf(TEXT("\nHold E for %s"), *LastFocusedOptions[1].DisplayName.ToString());
} }
else if (LastFocusedOptions.Num() >= 3) else if (LastFocusedOptions.Num() >= 3)
{ {