diff --git a/Content/Blueprints/Player/BP_Player.uasset b/Content/Blueprints/Player/BP_Player.uasset index 0ef63785..3e9175b8 100644 --- a/Content/Blueprints/Player/BP_Player.uasset +++ b/Content/Blueprints/Player/BP_Player.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7f8e81f56a4534293c7bea741bc4fabe932c6df4f47539c6e6380d9935e08f4e -size 53672 +oid sha256:48674b96b684f2177456daf2a5beb6f93664cac47b2c80debf1a5cc3214ee36a +size 68791 diff --git a/Content/Input/NakedDesireInputActionDataBase.uasset b/Content/Input/NakedDesireInputActionDataBase.uasset index db7f692a..bc951e9a 100644 --- a/Content/Input/NakedDesireInputActionDataBase.uasset +++ b/Content/Input/NakedDesireInputActionDataBase.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c992bc699677b0de58c9373a25116d6152d63647a6bfc0856884a1e81cb30388 -size 30135 +oid sha256:85b48bcf4348d52491187ae21f84ef05e939f72c10ef22ff03975c17552f7e55 +size 30136 diff --git a/Content/UI/RadialMenu/WBP_RadialSlice.uasset b/Content/UI/RadialMenu/WBP_RadialSlice.uasset index 9c56dfca..64b312c5 100644 --- a/Content/UI/RadialMenu/WBP_RadialSlice.uasset +++ b/Content/UI/RadialMenu/WBP_RadialSlice.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:228e5755e953b465966c79d2b29997d0acd0d60c0d362519511824b2a8164ecc -size 31321 +oid sha256:340f214c0418b5e20db0781c90b0e87d62d725b24a42e7ba912698f6f0cc9804 +size 28648 diff --git a/Source/NakedDesire/NakedDesire.Build.cs b/Source/NakedDesire/NakedDesire.Build.cs index 0b06ddd4..1347bf46 100644 --- a/Source/NakedDesire/NakedDesire.Build.cs +++ b/Source/NakedDesire/NakedDesire.Build.cs @@ -11,7 +11,7 @@ public class NakedDesire : ModuleRules PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput", "UMG", "CommonUI", "NavigationSystem", - "AIModule", "GameplayTags" + "AIModule", "GameplayTags", "Slate", "SlateCore" }); } } \ No newline at end of file diff --git a/Source/NakedDesire/UI/RadialMenu/RadialMenuController.cpp b/Source/NakedDesire/UI/RadialMenu/RadialMenuController.cpp index 2b4ec3f6..1ec386da 100644 --- a/Source/NakedDesire/UI/RadialMenu/RadialMenuController.cpp +++ b/Source/NakedDesire/UI/RadialMenu/RadialMenuController.cpp @@ -4,6 +4,7 @@ #include "RadialMenuWidget.h" #include "GameFramework/PlayerController.h" #include "Kismet/GameplayStatics.h" +#include "Blueprint/WidgetLayoutLibrary.h" URadialMenuController::URadialMenuController() { @@ -184,8 +185,25 @@ void URadialMenuController::UpdateHoverFromInput() return; } + UpdateHoverFromDirection(DirX, DirY, FVector2D(DirX, DirY).Size()); +} + +void URadialMenuController::UpdateHoverFromDirection(float DirX, float DirY, float Magnitude) +{ + if (!bIsOpen) + { + return; + } + + // Below the dead zone we keep the current hover rather than deselecting, + // so releasing the stick to neutral doesn't clear the player's choice. + if (Magnitude < SelectionDeadZone) + { + return; + } + // Angle clockwise from straight up (12 o'clock), in [0,360). - // atan2(x, y) gives angle from +Y axis turning toward +X = clockwise. Nice. + // atan2(x, y) gives angle from +Y axis turning toward +X = clockwise. float AngleDeg = FMath::RadiansToDegrees(FMath::Atan2(DirX, DirY)); if (AngleDeg < 0.f) { diff --git a/Source/NakedDesire/UI/RadialMenu/RadialMenuController.h b/Source/NakedDesire/UI/RadialMenu/RadialMenuController.h index 42ba2bb1..349b8e4e 100644 --- a/Source/NakedDesire/UI/RadialMenu/RadialMenuController.h +++ b/Source/NakedDesire/UI/RadialMenu/RadialMenuController.h @@ -66,6 +66,13 @@ public: UFUNCTION(BlueprintPure, Category = "Radial Menu") int32 GetHoveredIndex() const { return HoveredIndex; } + // Update the hovered slice from a direction vector (X right, Y up), measured + // in the same frame as the menu. Used by both the mouse path (controller + // tick) and the gamepad path (widget forwards the right-stick vector here, + // since CommonUI consumes the stick before the controller can read it). + // Magnitude below the dead zone leaves the current hover unchanged. + void UpdateHoverFromDirection(float DirX, float DirY, float Magnitude); + // The data the menu draws. Populate from wherever you like (data table, // inventory component, hardcoded list) before calling OpenMenu(). UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Radial Menu") diff --git a/Source/NakedDesire/UI/RadialMenu/RadialMenuWidget.cpp b/Source/NakedDesire/UI/RadialMenu/RadialMenuWidget.cpp index 253ea6e8..4ec1ed5c 100644 --- a/Source/NakedDesire/UI/RadialMenu/RadialMenuWidget.cpp +++ b/Source/NakedDesire/UI/RadialMenu/RadialMenuWidget.cpp @@ -5,6 +5,7 @@ #include "Components/CanvasPanel.h" #include "Components/CanvasPanelSlot.h" #include "Input/CommonUIInputTypes.h" // FBindUIActionArgs +#include "InputCoreTypes.h" // EKeys::LeftMouseButton void URadialMenuWidget::InitializeFromController( URadialMenuController* InController, const TArray& InEntries) @@ -16,10 +17,22 @@ void URadialMenuWidget::InitializeFromController( void URadialMenuWidget::NativeOnActivated() { Super::NativeOnActivated(); - + + // Make sure the widget can actually hold focus; UCanvasPanel can't, so the + // activatable widget itself is our focus target (see GetDesiredFocusTarget). SetIsFocusable(true); + // Must be hit-testable for NativeOnMouseButtonDown (left-click confirm) to + // fire. SelfHitTestInvisible/HitTestInvisible would let the click fall + // through and never reach us. + SetVisibility(ESlateVisibility::Visible); + // Bind Confirm. FBindUIActionArgs accepts the row handle directly. + // NOTE: Confirm must be mapped to a KEY (e.g. spacebar / face button), not + // left mouse button. CommonUI routes left-click through its separate Default + // Click Action path, so a click-mapped Confirm never reaches this binding. + // To confirm via trackpad/left-click instead, handle the pointer click + // directly (see NativeOnMouseButtonDown) rather than binding it here. if (!ConfirmAction.IsNull()) { FBindUIActionArgs ConfirmArgs( @@ -38,6 +51,13 @@ void URadialMenuWidget::NativeOnActivated() BackArgs.bDisplayInActionBar = true; BackHandle = RegisterUIActionBinding(BackArgs); } + + // Reinforce: explicitly route focus to this widget for the owning player so + // gamepad/keyboard input reaches the bindings even if the layer didn't. + if (APlayerController* PC = GetOwningPlayer()) + { + SetUserFocus(PC); + } } void URadialMenuWidget::NativeOnDeactivated() @@ -57,10 +77,67 @@ void URadialMenuWidget::NativeOnDeactivated() UWidget* URadialMenuWidget::NativeGetDesiredFocusTarget() const { - // Keep focus on the menu root so the bound actions receive input. + // A UCanvasPanel is NOT focusable by default, so returning it here would + // silently fail and the bound actions wouldn't receive input. The radial + // menu has no single "default" child to land on (there's no list to + // highlight), so we focus the activatable widget itself, which participates + // in focus correctly. Requires bIsFocusable = true (set in NativeConstruct). return const_cast(this); } +FReply URadialMenuWidget::NativeOnMouseButtonDown(const FGeometry& InGeometry, + const FPointerEvent& InMouseEvent) +{ + // Left-click anywhere on the menu confirms the currently hovered slice. + // The controller already tracks the hovered index each tick from the cursor + // angle, so the click itself carries no position logic — it just triggers + // the confirm. This bypasses CommonUI's Default Click Action entirely. + if (InMouseEvent.GetEffectingButton() == EKeys::LeftMouseButton) + { + if (OwningController.IsValid()) + { + OwningController->CloseAndConfirm(); + } + // Handle the event so it doesn't fall through to the click system. + return FReply::Handled(); + } + + return Super::NativeOnMouseButtonDown(InGeometry, InMouseEvent); +} + +FReply URadialMenuWidget::NativeOnAnalogValueChanged(const FGeometry& InGeometry, + const FAnalogInputEvent& InAnalogEvent) +{ + // Analog events arrive one axis per event, so we cache the latest value of + // each right-stick axis and recompute direction whenever either changes. + const FKey Key = InAnalogEvent.GetKey(); + bool bRightStick = false; + + if (Key == EKeys::Gamepad_RightX) + { + RightStickX = InAnalogEvent.GetAnalogValue(); + bRightStick = true; + } + else if (Key == EKeys::Gamepad_RightY) + { + RightStickY = InAnalogEvent.GetAnalogValue(); + bRightStick = true; + } + + if (bRightStick && OwningController.IsValid()) + { + // Stick Y is +up already (unlike screen space), so pass it straight + // through. The controller applies its own dead zone. + const float Mag = FVector2D(RightStickX, RightStickY).Size(); + OwningController->UpdateHoverFromDirection(RightStickX, RightStickY, Mag); + + // Consume so CommonUI's analog cursor doesn't also act on it. + return FReply::Handled(); + } + + return Super::NativeOnAnalogValueChanged(InGeometry, InAnalogEvent); +} + void URadialMenuWidget::HandleConfirm() { // Route to the controller, which owns the state, time dilation, and the diff --git a/Source/NakedDesire/UI/RadialMenu/RadialMenuWidget.h b/Source/NakedDesire/UI/RadialMenu/RadialMenuWidget.h index 5a99454b..bef5cbeb 100644 --- a/Source/NakedDesire/UI/RadialMenu/RadialMenuWidget.h +++ b/Source/NakedDesire/UI/RadialMenu/RadialMenuWidget.h @@ -36,6 +36,20 @@ protected: virtual void NativeOnDeactivated() override; virtual void NativeTick(const FGeometry& MyGeometry, float InDeltaTime) override; + // Left-click confirm. Handled directly here rather than as a CommonUI action + // binding, because CommonUI routes left mouse through its Default Click Action + // system (separate from RegisterUIActionBinding), so a click-mapped action + // never reaches a normal binding. Intercepting the pointer event sidesteps that. + virtual FReply NativeOnMouseButtonDown(const FGeometry& InGeometry, + const FPointerEvent& InMouseEvent) override; + + // Gamepad slice selection. When this widget is the active CommonUI node, the + // analog sticks are routed here (NOT to the PlayerController), which is why + // GetInputAnalogStickState returns 0 from the controller while a menu is up. + // We read the right stick from the analog event and drive hover directly. + virtual FReply NativeOnAnalogValueChanged(const FGeometry& InGeometry, + const FAnalogInputEvent& InAnalogEvent) override; + // CommonUI focus: returning the root keeps gamepad/keyboard focus on us so // the bound actions fire while the wheel is open. virtual UWidget* NativeGetDesiredFocusTarget() const override; @@ -85,4 +99,9 @@ private: FUIActionBindingHandle BackHandle; int32 HoveredIndex = -1; + + // Latest right-stick axis values, cached across single-axis analog events so + // we can recompute the full 2D direction whenever either axis updates. + float RightStickX = 0.f; + float RightStickY = 0.f; }; \ No newline at end of file