Verbesserung der Interaktionslogik und UI-Optimierung
Die Änderungen umfassen: - Projektname in `DefaultGame.ini` auf "Kingshearth Legacy" geändert. - Neue Konsolenvariablen für CommonUI hinzugefügt. - `ListView` ersetzt `VerticalBox` für Kontextmenü-Optionen. - `FInteractionOption` um `Icon` und `PromptOverride` erweitert. - Debugging-Logs in `InteractionComponent.cpp` hinzugefügt. - Neue Klassen: `UInteractionOptionEntryWidget` und `UInteractionOptionListItem`. - Verbesserte Eingabeaufforderungslogik und Sichtbarkeitssteuerung. - Binärdateien (`.uasset`) aktualisiert und neue hinzugefügt.
This commit is contained in:
@@ -1,8 +1,9 @@
|
|||||||
[/Script/EngineSettings.GeneralProjectSettings]
|
[/Script/EngineSettings.GeneralProjectSettings]
|
||||||
ProjectID=CE954E444D2A872317AAC38182169D03
|
ProjectID=CE954E444D2A872317AAC38182169D03
|
||||||
ProjectName=Third Person Game Template
|
ProjectName=Kingsharth Legacy
|
||||||
|
|
||||||
[ConsoleVariables]
|
[ConsoleVariables]
|
||||||
CommonUI.CheckKeyboardFocusAndParentage=1
|
CommonUI.CheckKeyboardFocusAndParentage=1
|
||||||
CommonUI.DisallowUserFocusedWidgetForPendingFocusRecipient=1
|
CommonUI.DisallowUserFocusedWidgetForPendingFocusRecipient=1
|
||||||
CommonUI.FallbackToDesiredOnAutoRestoreFailure=1
|
CommonUI.FallbackToDesiredOnAutoRestoreFailure=1
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -39,10 +39,11 @@ bool UInteractionComponent::PerformInteractionTrace(FHitResult& OutHit) const
|
|||||||
|
|
||||||
if (Owner->Implements<UInteractionLookupProvider>())
|
if (Owner->Implements<UInteractionLookupProvider>())
|
||||||
{
|
{
|
||||||
if (IInteractionLookupProvider::Execute_InteractionLookupLoop(Owner, OutHit))
|
// Authoritative once implemented - a miss here means "nothing found", full stop.
|
||||||
{
|
// Don't silently fall through to the default trace below just because this one
|
||||||
return true;
|
// came back empty; that would use a completely different (un-offset) trace and
|
||||||
}
|
// could contradict what the override deliberately decided.
|
||||||
|
return IInteractionLookupProvider::Execute_InteractionLookupLoop(Owner, OutHit);
|
||||||
}
|
}
|
||||||
|
|
||||||
FVector TraceStart = Owner->GetActorLocation();
|
FVector TraceStart = Owner->GetActorLocation();
|
||||||
@@ -87,8 +88,19 @@ void UInteractionComponent::UpdateFocus()
|
|||||||
{
|
{
|
||||||
FHitResult Hit;
|
FHitResult Hit;
|
||||||
const bool bHit = PerformInteractionTrace(Hit);
|
const bool bHit = PerformInteractionTrace(Hit);
|
||||||
|
const bool bImplementsInteractable = bHit && Hit.GetActor() && Hit.GetActor()->Implements<UInteractable>();
|
||||||
|
AActor* NewFocusedActor = bImplementsInteractable ? Hit.GetActor() : nullptr;
|
||||||
|
|
||||||
AActor* NewFocusedActor = (bHit && Hit.GetActor() && Hit.GetActor()->Implements<UInteractable>()) ? Hit.GetActor() : nullptr;
|
#if ENABLE_DRAW_DEBUG
|
||||||
|
if (bShowDebugTraces)
|
||||||
|
{
|
||||||
|
UE_LOG(LogKingshearthLegacy, Log, TEXT("[Interaction] Tick: bHit=%s hitActor='%s' implementsInteractable=%s currentFocused='%s'"),
|
||||||
|
bHit ? TEXT("true") : TEXT("false"),
|
||||||
|
*GetNameSafe(bHit ? Hit.GetActor() : nullptr),
|
||||||
|
bImplementsInteractable ? TEXT("true") : TEXT("false"),
|
||||||
|
*GetNameSafe(FocusedActor.Get()));
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
if (NewFocusedActor == FocusedActor.Get())
|
if (NewFocusedActor == FocusedActor.Get())
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
#include "CoreMinimal.h"
|
#include "CoreMinimal.h"
|
||||||
#include "InteractionTypes.generated.h"
|
#include "InteractionTypes.generated.h"
|
||||||
|
|
||||||
|
class UTexture2D;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A single interaction an IInteractable currently offers. Order matters:
|
* A single interaction an IInteractable currently offers. Order matters:
|
||||||
* index 0 is always the primary (tap) action, index 1 (if present) is the
|
* index 0 is always the primary (tap) action, index 1 (if present) is the
|
||||||
@@ -22,4 +24,17 @@ struct FInteractionOption
|
|||||||
/** Opaque id passed back to IInteractable::ExecuteInteraction - the object interprets it itself */
|
/** Opaque id passed back to IInteractable::ExecuteInteraction - the object interprets it itself */
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction")
|
||||||
FName ActionID;
|
FName ActionID;
|
||||||
|
|
||||||
|
/** Optional - shown by UInteractionOptionEntryWidget's Icon image when set, hidden otherwise. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction")
|
||||||
|
TObjectPtr<UTexture2D> Icon = nullptr;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional - when this is the primary (index 0) option and this is set, the ever-present
|
||||||
|
* prompt's first line uses this verbatim instead of building "{DisplayName} with
|
||||||
|
* {ObjectName}" - e.g. a chest can set this to "Open Chest" instead of ending up as
|
||||||
|
* "Open with Chest". The "Press E" / "Hold E for ..." lines are unaffected.
|
||||||
|
*/
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction")
|
||||||
|
FText PromptOverride;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ public class KingshearthLegacy : ModuleRules
|
|||||||
"GameplayStateTreeModule",
|
"GameplayStateTreeModule",
|
||||||
"UMG",
|
"UMG",
|
||||||
"Slate",
|
"Slate",
|
||||||
|
"SlateCore",
|
||||||
"GameplayAbilities",
|
"GameplayAbilities",
|
||||||
"GameplayTags",
|
"GameplayTags",
|
||||||
"GameplayTasks"
|
"GameplayTasks"
|
||||||
|
|||||||
@@ -1,25 +1,41 @@
|
|||||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||||
|
|
||||||
#include "InteractionContextMenuWidget.h"
|
#include "InteractionContextMenuWidget.h"
|
||||||
|
#include "InteractionOptionListItem.h"
|
||||||
#include "Interaction/InteractionComponent.h"
|
#include "Interaction/InteractionComponent.h"
|
||||||
#include "Interaction/Interactable.h"
|
#include "Interaction/Interactable.h"
|
||||||
#include "Components/VerticalBox.h"
|
#include "Components/ListView.h"
|
||||||
#include "Components/TextBlock.h"
|
#include "Components/TextBlock.h"
|
||||||
#include "Blueprint/WidgetTree.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();
|
||||||
|
|
||||||
if (PromptText)
|
SetPromptVisible(TextContainer, PromptText, false);
|
||||||
{
|
|
||||||
PromptText->SetVisibility(ESlateVisibility::Collapsed);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (OptionsBox)
|
if (OptionsList)
|
||||||
{
|
{
|
||||||
OptionsBox->SetVisibility(ESlateVisibility::Collapsed);
|
OptionsList->SetVisibility(ESlateVisibility::Collapsed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,84 +61,139 @@ void UInteractionContextMenuWidget::InitializeInteraction(UInteractionComponent*
|
|||||||
}
|
}
|
||||||
|
|
||||||
void UInteractionContextMenuWidget::HandleFocusedInteractableChanged(AActor* FocusedActor, const TArray<FInteractionOption>& Options)
|
void UInteractionContextMenuWidget::HandleFocusedInteractableChanged(AActor* FocusedActor, const TArray<FInteractionOption>& Options)
|
||||||
|
{
|
||||||
|
LastFocusedActor = FocusedActor;
|
||||||
|
LastFocusedOptions = Options;
|
||||||
|
UpdatePromptVisibility();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UInteractionContextMenuWidget::UpdatePromptVisibility()
|
||||||
{
|
{
|
||||||
if (!PromptText)
|
if (!PromptText)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!FocusedActor || Options.Num() == 0)
|
// 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())
|
||||||
{
|
{
|
||||||
PromptText->SetVisibility(ESlateVisibility::Collapsed);
|
SetPromptVisible(TextContainer, PromptText, false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// "{Verb} with {ObjectName}" on line one, "Press E" always, then a hint for the
|
AActor* FocusedActor = LastFocusedActor.Get();
|
||||||
// hold behaviour only when there is one (2 options = secondary, 3+ = Context Menu).
|
if (!FocusedActor || LastFocusedOptions.Num() == 0)
|
||||||
const FText ObjectName = IInteractable::Execute_GetInteractableDisplayName(FocusedActor);
|
{
|
||||||
FString Prompt = ObjectName.IsEmpty()
|
SetPromptVisible(TextContainer, PromptText, false);
|
||||||
? Options[0].DisplayName.ToString()
|
return;
|
||||||
: FString::Printf(TEXT("%s with %s"), *Options[0].DisplayName.ToString(), *ObjectName.ToString());
|
}
|
||||||
|
|
||||||
|
// "{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");
|
Prompt += TEXT("\nPress E");
|
||||||
|
|
||||||
if (Options.Num() == 2)
|
if (LastFocusedOptions.Num() == 2)
|
||||||
{
|
{
|
||||||
Prompt += TEXT("\nHold E for secondary interaction");
|
Prompt += TEXT("\nHold E for secondary interaction");
|
||||||
}
|
}
|
||||||
else if (Options.Num() >= 3)
|
else if (LastFocusedOptions.Num() >= 3)
|
||||||
{
|
{
|
||||||
Prompt += TEXT("\nHold E for Context Menu");
|
Prompt += TEXT("\nHold E for Context Menu");
|
||||||
}
|
}
|
||||||
|
|
||||||
PromptText->SetText(FText::FromString(Prompt));
|
PromptText->SetText(FText::FromString(Prompt));
|
||||||
PromptText->SetVisibility(ESlateVisibility::HitTestInvisible);
|
SetPromptVisible(TextContainer, PromptText, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void UInteractionContextMenuWidget::RefreshOptions(const TArray<FInteractionOption>& Options, int32 HighlightedIndex)
|
void UInteractionContextMenuWidget::PopulateOptionsList(const TArray<FInteractionOption>& Options)
|
||||||
{
|
{
|
||||||
if (!OptionsBox)
|
if (!OptionsList)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
OptionsBox->ClearChildren();
|
OptionsList->ClearListItems();
|
||||||
|
ListItems.Reset(Options.Num());
|
||||||
|
|
||||||
for (int32 Index = 0; Index < Options.Num(); ++Index)
|
TArray<UObject*> ItemObjects;
|
||||||
|
ItemObjects.Reserve(Options.Num());
|
||||||
|
|
||||||
|
for (const FInteractionOption& OptionEntry : Options)
|
||||||
{
|
{
|
||||||
UTextBlock* Row = WidgetTree->ConstructWidget<UTextBlock>(UTextBlock::StaticClass());
|
UInteractionOptionListItem* Item = NewObject<UInteractionOptionListItem>(this);
|
||||||
Row->SetText(Options[Index].DisplayName);
|
Item->Option = OptionEntry;
|
||||||
Row->SetColorAndOpacity(FSlateColor(Index == HighlightedIndex ? HighlightedColor : NormalColor));
|
ListItems.Add(Item);
|
||||||
OptionsBox->AddChildToVerticalBox(Row);
|
ItemObjects.Add(Item);
|
||||||
|
}
|
||||||
|
|
||||||
|
OptionsList->SetListItems(ItemObjects);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UInteractionContextMenuWidget::SelectHighlighted(int32 Index)
|
||||||
|
{
|
||||||
|
if (!OptionsList)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ListItems.IsValidIndex(Index))
|
||||||
|
{
|
||||||
|
OptionsList->SetSelectedItem(ListItems[Index]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
OptionsList->ClearSelection();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void UInteractionContextMenuWidget::HandleContextMenuOpened(const TArray<FInteractionOption>& Options, int32 InitialHighlightedIndex)
|
void UInteractionContextMenuWidget::HandleContextMenuOpened(const TArray<FInteractionOption>& Options, int32 InitialHighlightedIndex)
|
||||||
{
|
{
|
||||||
UE_LOG(LogKingshearthLegacy, Log, TEXT("[Interaction] Widget received OnContextMenuOpened: %d option(s), OptionsBox bound: %s"),
|
UE_LOG(LogKingshearthLegacy, Log, TEXT("[Interaction] Widget received OnContextMenuOpened: %d option(s), OptionsList bound: %s"),
|
||||||
Options.Num(), OptionsBox ? TEXT("yes") : TEXT("NO - add a VerticalBox named exactly \"OptionsBox\" in this widget's Designer tree"));
|
Options.Num(), OptionsList ? TEXT("yes") : TEXT("NO - add a ListView named exactly \"OptionsList\" in this widget's Designer tree"));
|
||||||
|
|
||||||
CachedOptions = Options;
|
CachedOptions = Options;
|
||||||
RefreshOptions(CachedOptions, InitialHighlightedIndex);
|
PopulateOptionsList(CachedOptions);
|
||||||
|
SelectHighlighted(InitialHighlightedIndex);
|
||||||
|
|
||||||
if (OptionsBox)
|
if (OptionsList)
|
||||||
{
|
{
|
||||||
OptionsBox->SetVisibility(ESlateVisibility::HitTestInvisible);
|
OptionsList->SetVisibility(ESlateVisibility::Visible);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UpdatePromptVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
void UInteractionContextMenuWidget::HandleContextMenuHighlightChanged(int32 NewHighlightedIndex)
|
void UInteractionContextMenuWidget::HandleContextMenuHighlightChanged(int32 NewHighlightedIndex)
|
||||||
{
|
{
|
||||||
RefreshOptions(CachedOptions, NewHighlightedIndex);
|
SelectHighlighted(NewHighlightedIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
void UInteractionContextMenuWidget::HandleContextMenuClosed()
|
void UInteractionContextMenuWidget::HandleContextMenuClosed()
|
||||||
{
|
{
|
||||||
CachedOptions.Reset();
|
CachedOptions.Reset();
|
||||||
|
ListItems.Reset();
|
||||||
|
|
||||||
if (OptionsBox)
|
if (OptionsList)
|
||||||
{
|
{
|
||||||
OptionsBox->ClearChildren();
|
OptionsList->ClearListItems();
|
||||||
OptionsBox->SetVisibility(ESlateVisibility::Collapsed);
|
OptionsList->SetVisibility(ESlateVisibility::Collapsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UpdatePromptVisibility();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,10 @@
|
|||||||
#include "InteractionContextMenuWidget.generated.h"
|
#include "InteractionContextMenuWidget.generated.h"
|
||||||
|
|
||||||
class UInteractionComponent;
|
class UInteractionComponent;
|
||||||
class UVerticalBox;
|
class UInteractionOptionListItem;
|
||||||
|
class UListView;
|
||||||
class UTextBlock;
|
class UTextBlock;
|
||||||
|
class UWidget;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* One widget covering both interaction UI needs so you only wire up a single thing
|
* One widget covering both interaction UI needs so you only wire up a single thing
|
||||||
@@ -17,7 +19,7 @@ class UTextBlock;
|
|||||||
* driven by OnFocusedInteractableChanged - shows any time the look trace is on an
|
* driven by OnFocusedInteractableChanged - shows any time the look trace is on an
|
||||||
* IInteractable, regardless of option count or whether Interact is even pressed) and
|
* IInteractable, regardless of option count or whether Interact is even pressed) and
|
||||||
* the Context Menu shown while holding Interact on a 3+-option IInteractable
|
* the Context Menu shown while holding Interact on a 3+-option IInteractable
|
||||||
* (OptionsBox, driven by OnContextMenuOpened/HighlightChanged/Closed). Both bound
|
* (OptionsList, driven by OnContextMenuOpened/HighlightChanged/Closed). Both bound
|
||||||
* widgets are optional and toggle their own visibility independently - add whichever
|
* 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
|
* you want in the Designer, named exactly as below; all population/highlight/show-hide
|
||||||
* logic lives here in C++.
|
* logic lives here in C++.
|
||||||
@@ -37,8 +39,11 @@ protected:
|
|||||||
|
|
||||||
virtual void NativeConstruct() override;
|
virtual void NativeConstruct() override;
|
||||||
|
|
||||||
/** Rebuilds OptionsBox's rows from Options, styling HighlightedIndex differently from the rest. */
|
/** Rebuilds OptionsList's items from Options; ListItems is kept alive here so the ListView's UObjects don't get GC'd. */
|
||||||
void RefreshOptions(const TArray<FInteractionOption>& Options, int32 HighlightedIndex);
|
void PopulateOptionsList(const TArray<FInteractionOption>& Options);
|
||||||
|
|
||||||
|
/** Selects ListItems[Index] on OptionsList, which drives each row's IUserObjectListEntry::NativeOnItemSelectionChanged. */
|
||||||
|
void SelectHighlighted(int32 Index);
|
||||||
|
|
||||||
UFUNCTION()
|
UFUNCTION()
|
||||||
void HandleFocusedInteractableChanged(AActor* FocusedActor, const TArray<FInteractionOption>& Options);
|
void HandleFocusedInteractableChanged(AActor* FocusedActor, const TArray<FInteractionOption>& Options);
|
||||||
@@ -52,25 +57,45 @@ protected:
|
|||||||
UFUNCTION()
|
UFUNCTION()
|
||||||
void HandleContextMenuClosed();
|
void HandleContextMenuClosed();
|
||||||
|
|
||||||
/** Text color used for the highlighted row. */
|
|
||||||
UPROPERTY(EditAnywhere, Category = "Interaction|Style")
|
|
||||||
FLinearColor HighlightedColor = FLinearColor(1.0f, 0.85f, 0.3f, 1.0f);
|
|
||||||
|
|
||||||
/** Text color used for every other row. */
|
|
||||||
UPROPERTY(EditAnywhere, Category = "Interaction|Style")
|
|
||||||
FLinearColor NormalColor = FLinearColor::White;
|
|
||||||
|
|
||||||
/** 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. */
|
/** 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))
|
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional = true))
|
||||||
TObjectPtr<UTextBlock> PromptText;
|
TObjectPtr<UTextBlock> PromptText;
|
||||||
|
|
||||||
/** Container the Context Menu's option rows are built into. Optional, place it in the WBP subclass's Designer tree. */
|
/**
|
||||||
|
* 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))
|
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional = true))
|
||||||
TObjectPtr<UVerticalBox> OptionsBox;
|
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
|
||||||
|
* subclass of UInteractionOptionEntryWidget - that class receives each row's data
|
||||||
|
* and highlighted state natively, no BP event graph logic needed.
|
||||||
|
*/
|
||||||
|
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional = true))
|
||||||
|
TObjectPtr<UListView> OptionsList;
|
||||||
|
|
||||||
UPROPERTY()
|
UPROPERTY()
|
||||||
TArray<FInteractionOption> CachedOptions;
|
TArray<FInteractionOption> CachedOptions;
|
||||||
|
|
||||||
|
/** UObject wrappers backing OptionsList - one per CachedOptions entry, rebuilt in PopulateOptionsList. */
|
||||||
|
UPROPERTY()
|
||||||
|
TArray<TObjectPtr<UInteractionOptionListItem>> ListItems;
|
||||||
|
|
||||||
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,84 @@
|
|||||||
|
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||||
|
|
||||||
|
#include "InteractionOptionEntryWidget.h"
|
||||||
|
#include "InteractionOptionListItem.h"
|
||||||
|
#include "Components/Border.h"
|
||||||
|
#include "Components/Image.h"
|
||||||
|
#include "Components/TextBlock.h"
|
||||||
|
#include "Components/Widget.h"
|
||||||
|
|
||||||
|
void UInteractionOptionEntryWidget::NativeConstruct()
|
||||||
|
{
|
||||||
|
Super::NativeConstruct();
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// otherwise keep whatever visibility Highlight has in the Designer - force it into
|
||||||
|
// the correct "not highlighted" state right away instead.
|
||||||
|
ApplyHighlightVisuals();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UInteractionOptionEntryWidget::NativeOnListItemObjectSet(UObject* ListItemObject)
|
||||||
|
{
|
||||||
|
if (const UInteractionOptionListItem* ListItem = Cast<UInteractionOptionListItem>(ListItemObject))
|
||||||
|
{
|
||||||
|
Option = ListItem->Option;
|
||||||
|
ApplyOptionVisuals();
|
||||||
|
|
||||||
|
// SListView recycles row widgets between items; a row reassigned from a previously-
|
||||||
|
// selected item to a new one doesn't necessarily get a fresh NativeOnItemSelectionChanged
|
||||||
|
// call in the same pass, so re-derive the real selection state here rather than trusting
|
||||||
|
// whatever bHighlighted this (possibly recycled) widget already had.
|
||||||
|
bHighlighted = IsListItemSelected();
|
||||||
|
ApplyHighlightVisuals();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UInteractionOptionEntryWidget::NativeOnItemSelectionChanged(bool bIsSelected)
|
||||||
|
{
|
||||||
|
bHighlighted = bIsSelected;
|
||||||
|
ApplyHighlightVisuals();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UInteractionOptionEntryWidget::ApplyOptionVisuals()
|
||||||
|
{
|
||||||
|
if (InteractionText)
|
||||||
|
{
|
||||||
|
InteractionText->SetText(Option.DisplayName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Icon)
|
||||||
|
{
|
||||||
|
if (Option.Icon)
|
||||||
|
{
|
||||||
|
Icon->SetBrushFromTexture(Option.Icon);
|
||||||
|
Icon->SetVisibility(ESlateVisibility::HitTestInvisible);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Icon->SetVisibility(ESlateVisibility::Collapsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UInteractionOptionEntryWidget::ApplyHighlightVisuals()
|
||||||
|
{
|
||||||
|
if (Highlight)
|
||||||
|
{
|
||||||
|
Highlight->SetVisibility(bHighlighted ? ESlateVisibility::HitTestInvisible : ESlateVisibility::Hidden);
|
||||||
|
|
||||||
|
if (UImage* HighlightImage = Cast<UImage>(Highlight.Get()))
|
||||||
|
{
|
||||||
|
HighlightImage->SetColorAndOpacity(HighlightColor);
|
||||||
|
}
|
||||||
|
else if (UBorder* HighlightBorder = Cast<UBorder>(Highlight.Get()))
|
||||||
|
{
|
||||||
|
HighlightBorder->SetBrushColor(HighlightColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (InteractionText)
|
||||||
|
{
|
||||||
|
InteractionText->SetColorAndOpacity(bHighlighted ? HighlightedTextColor : NormalTextColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Blueprint/UserWidget.h"
|
||||||
|
#include "Blueprint/IUserObjectListEntry.h"
|
||||||
|
#include "Interaction/InteractionTypes.h"
|
||||||
|
#include "InteractionOptionEntryWidget.generated.h"
|
||||||
|
|
||||||
|
class UImage;
|
||||||
|
class UTextBlock;
|
||||||
|
class UWidget;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One row of the Context Menu's OptionsList (a UListView). Assign a WBP
|
||||||
|
* subclass of this as that ListView's Entry Widget Class in the Designer -
|
||||||
|
* name your widgets exactly "InteractionText" (Text), "Icon" (Image) and
|
||||||
|
* "Highlight" (a Border, or an Image - either works, tinted with
|
||||||
|
* HighlightColor) and all population/highlight styling is handled here in
|
||||||
|
* C++, driven by the ListView itself via IUserObjectListEntry; no BP event
|
||||||
|
* graph logic needed.
|
||||||
|
*/
|
||||||
|
UCLASS(abstract)
|
||||||
|
class KINGSHEARTHLEGACY_API UInteractionOptionEntryWidget : public UUserWidget, public IUserObjectListEntry
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Interaction")
|
||||||
|
const FInteractionOption& GetOption() const { return Option; }
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Interaction")
|
||||||
|
bool IsHighlighted() const { return bHighlighted; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
|
||||||
|
virtual void NativeConstruct() override;
|
||||||
|
|
||||||
|
/** IUserObjectListEntry - called by the owning ListView when this row is assigned an UInteractionOptionListItem. */
|
||||||
|
virtual void NativeOnListItemObjectSet(UObject* ListItemObject) override;
|
||||||
|
|
||||||
|
/** IUserObjectListEntry - called by the owning ListView when this row's selected state changes (drives the Context Menu highlight). */
|
||||||
|
virtual void NativeOnItemSelectionChanged(bool bIsSelected) override;
|
||||||
|
|
||||||
|
void ApplyOptionVisuals();
|
||||||
|
|
||||||
|
/** Applies bHighlighted to Highlight's visibility/tint and InteractionText's color. Called on construct (so freshly-spawned rows never default to "selected") and on every selection change. */
|
||||||
|
void ApplyHighlightVisuals();
|
||||||
|
|
||||||
|
/** Row label - bind a TextBlock named exactly "InteractionText" in the Designer. */
|
||||||
|
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional = true))
|
||||||
|
TObjectPtr<UTextBlock> InteractionText;
|
||||||
|
|
||||||
|
/** Option icon - bind an Image named exactly "Icon" in the Designer. Hidden automatically when the option has no Icon set. */
|
||||||
|
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional = true))
|
||||||
|
TObjectPtr<UImage> Icon;
|
||||||
|
|
||||||
|
/** Highlight background - bind a Border or an Image named exactly "Highlight" in the Designer. Shown and tinted HighlightColor only while this row is selected/highlighted. */
|
||||||
|
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional = true))
|
||||||
|
TObjectPtr<UWidget> Highlight;
|
||||||
|
|
||||||
|
/** Tint applied to Highlight while this row is highlighted. */
|
||||||
|
UPROPERTY(EditAnywhere, Category = "Interaction|Style")
|
||||||
|
FLinearColor HighlightColor = FLinearColor(1.0f, 0.85f, 0.3f, 1.0f);
|
||||||
|
|
||||||
|
/** InteractionText color while this row is NOT highlighted. */
|
||||||
|
UPROPERTY(EditAnywhere, Category = "Interaction|Style")
|
||||||
|
FLinearColor NormalTextColor = FLinearColor::White;
|
||||||
|
|
||||||
|
/** InteractionText color while this row IS highlighted. */
|
||||||
|
UPROPERTY(EditAnywhere, Category = "Interaction|Style")
|
||||||
|
FLinearColor HighlightedTextColor = FLinearColor::Black;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Interaction")
|
||||||
|
FInteractionOption Option;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Interaction")
|
||||||
|
bool bHighlighted = false;
|
||||||
|
};
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "UObject/Object.h"
|
||||||
|
#include "Interaction/InteractionTypes.h"
|
||||||
|
#include "InteractionOptionListItem.generated.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plain UObject wrapper around one FInteractionOption - UListView items must be
|
||||||
|
* UObjects, not structs, so UInteractionContextMenuWidget wraps each option in
|
||||||
|
* one of these before calling OptionsList->SetListItems.
|
||||||
|
*/
|
||||||
|
UCLASS()
|
||||||
|
class KINGSHEARTHLEGACY_API UInteractionOptionListItem : public UObject
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Interaction")
|
||||||
|
FInteractionOption Option;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user