Contextual interaction system #6
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)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user