Fixed radial menu input
This commit is contained in:
@@ -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"
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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<FRadialMenuEntry>& 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<URadialMenuWidget*>(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
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user