243 lines
7.0 KiB
C++
243 lines
7.0 KiB
C++
// RadialMenuController.cpp
|
|
|
|
#include "RadialMenuController.h"
|
|
#include "RadialMenuWidget.h"
|
|
#include "GameFramework/PlayerController.h"
|
|
#include "Kismet/GameplayStatics.h"
|
|
#include "Blueprint/WidgetLayoutLibrary.h"
|
|
|
|
URadialMenuController::URadialMenuController()
|
|
{
|
|
// We only tick while the menu is open; we toggle this in Open/Close so the
|
|
// component costs nothing the rest of the time.
|
|
PrimaryComponentTick.bCanEverTick = true;
|
|
PrimaryComponentTick.bStartWithTickEnabled = false;
|
|
}
|
|
|
|
void URadialMenuController::OpenMenu()
|
|
{
|
|
if (bIsOpen || Entries.Num() == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
APlayerController* PC = Cast<APlayerController>(
|
|
UGameplayStatics::GetPlayerController(this, 0));
|
|
if (!PC || !WidgetClass)
|
|
{
|
|
return;
|
|
}
|
|
|
|
bIsOpen = true;
|
|
HoveredIndex = -1;
|
|
OpenRealTimeSeconds = FApp::GetCurrentTime(); // undilated wall-ish time
|
|
|
|
// Dilate time. CustomTimeDilation on the actor would only affect us; we want
|
|
// the whole world to slow, so we use global dilation and cache the old value.
|
|
CachedTimeDilation = UGameplayStatics::GetGlobalTimeDilation(this);
|
|
SetTimeDilation(OpenTimeDilation);
|
|
|
|
// Show + center the cursor so the player has a clear anchor to aim from.
|
|
PC->bShowMouseCursor = true;
|
|
int32 ViewX, ViewY;
|
|
PC->GetViewportSize(ViewX, ViewY);
|
|
PC->SetMouseLocation(ViewX / 2, ViewY / 2);
|
|
|
|
ActiveWidget = CreateWidget<URadialMenuWidget>(PC, WidgetClass);
|
|
if (ActiveWidget)
|
|
{
|
|
ActiveWidget->InitializeFromController(this, Entries);
|
|
|
|
// CommonUI activatable widgets belong on an activatable stack/layer so
|
|
// the Back action and activation lifecycle work. If you have a layer set
|
|
// up, push there instead of the viewport (see PushToLayer below). For a
|
|
// standalone setup we add to viewport and activate manually.
|
|
ActiveWidget->AddToViewport(100); // high Z so it sits above the HUD
|
|
ActiveWidget->ActivateWidget(); // triggers NativeOnActivated -> binds input
|
|
}
|
|
|
|
SetComponentTickEnabled(true);
|
|
}
|
|
|
|
void URadialMenuController::CloseAndConfirm()
|
|
{
|
|
if (!bIsOpen)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Tap-to-open guard: if a confirm arrives within the guard window of opening,
|
|
// treat it as noise from the opening press rather than a real selection.
|
|
// We swallow it entirely (menu stays open) so the player can still choose.
|
|
if (OpenInputGuardSeconds > 0.f
|
|
&& (FApp::GetCurrentTime() - OpenRealTimeSeconds) < OpenInputGuardSeconds)
|
|
{
|
|
return;
|
|
}
|
|
|
|
const int32 Confirmed =
|
|
(HoveredIndex >= 0 && Entries.IsValidIndex(HoveredIndex)
|
|
&& Entries[HoveredIndex].bEnabled)
|
|
? HoveredIndex
|
|
: -1;
|
|
|
|
// Tear down first so listeners see a clean closed state when they react.
|
|
const int32 IndexToBroadcast = Confirmed;
|
|
CloseAndCancel();
|
|
OnSelectionConfirmed.Broadcast(IndexToBroadcast);
|
|
}
|
|
|
|
void URadialMenuController::CloseAndCancel()
|
|
{
|
|
if (!bIsOpen)
|
|
{
|
|
return;
|
|
}
|
|
|
|
bIsOpen = false;
|
|
SetComponentTickEnabled(false);
|
|
SetTimeDilation(CachedTimeDilation);
|
|
|
|
if (APlayerController* PC = Cast<APlayerController>(
|
|
UGameplayStatics::GetPlayerController(this, 0)))
|
|
{
|
|
PC->bShowMouseCursor = false;
|
|
}
|
|
|
|
if (ActiveWidget)
|
|
{
|
|
// DeactivateWidget triggers NativeOnDeactivated, which unregisters the
|
|
// Confirm/Back bindings. Then remove it from the viewport/layer.
|
|
ActiveWidget->DeactivateWidget();
|
|
ActiveWidget->RemoveFromParent();
|
|
ActiveWidget = nullptr;
|
|
}
|
|
|
|
HoveredIndex = -1;
|
|
}
|
|
|
|
void URadialMenuController::TickComponent(float DeltaTime, ELevelTick TickType,
|
|
FActorComponentTickFunction* ThisTickFunction)
|
|
{
|
|
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
|
|
if (bIsOpen)
|
|
{
|
|
UpdateHoverFromInput();
|
|
}
|
|
}
|
|
|
|
void URadialMenuController::UpdateHoverFromInput()
|
|
{
|
|
APlayerController* PC = Cast<APlayerController>(
|
|
UGameplayStatics::GetPlayerController(this, 0));
|
|
if (!PC)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// --- Gamepad right stick takes priority if it's pushed past the dead zone --
|
|
float StickX = 0.f, StickY = 0.f;
|
|
PC->GetInputAnalogStickState(EControllerAnalogStick::CAS_RightStick, StickX, StickY);
|
|
const float StickMag = FVector2D(StickX, StickY).Size();
|
|
|
|
float DirX, DirY;
|
|
bool bHaveDirection = false;
|
|
|
|
if (StickMag >= SelectionDeadZone)
|
|
{
|
|
DirX = StickX;
|
|
DirY = StickY;
|
|
bHaveDirection = true;
|
|
}
|
|
else
|
|
{
|
|
// --- Fall back to mouse: direction from screen center to cursor -------
|
|
float MouseX, MouseY;
|
|
if (PC->GetMousePosition(MouseX, MouseY))
|
|
{
|
|
int32 ViewX, ViewY;
|
|
PC->GetViewportSize(ViewX, ViewY);
|
|
const FVector2D Center(ViewX * 0.5f, ViewY * 0.5f);
|
|
const FVector2D Offset = FVector2D(MouseX, MouseY) - Center;
|
|
|
|
// Dead zone in pixels scaled to viewport so it's resolution-stable.
|
|
const float DeadPx = SelectionDeadZone * (ViewY * 0.25f);
|
|
if (Offset.Size() >= DeadPx)
|
|
{
|
|
DirX = Offset.X;
|
|
DirY = -Offset.Y; // screen Y is down; flip so up = +Y
|
|
bHaveDirection = true;
|
|
}
|
|
else
|
|
{
|
|
DirX = DirY = 0.f;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
DirX = DirY = 0.f;
|
|
}
|
|
}
|
|
|
|
if (!bHaveDirection)
|
|
{
|
|
// Inside dead zone: keep whatever was hovered (no jitter, no deselect).
|
|
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.
|
|
float AngleDeg = FMath::RadiansToDegrees(FMath::Atan2(DirX, DirY));
|
|
if (AngleDeg < 0.f)
|
|
{
|
|
AngleDeg += 360.f;
|
|
}
|
|
|
|
const int32 NewIndex = AngleToSegment(AngleDeg);
|
|
if (NewIndex != HoveredIndex)
|
|
{
|
|
HoveredIndex = NewIndex;
|
|
if (ActiveWidget)
|
|
{
|
|
ActiveWidget->SetHoveredSegment(HoveredIndex);
|
|
}
|
|
}
|
|
}
|
|
|
|
int32 URadialMenuController::AngleToSegment(float AngleDegrees) const
|
|
{
|
|
const int32 Count = Entries.Num();
|
|
if (Count <= 0)
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
const float SliceSize = 360.f / Count;
|
|
|
|
// Offset by half a slice so that slice 0 is *centered* on 12 o'clock
|
|
// rather than starting there — matches the GTA layout.
|
|
const float Shifted = FMath::Fmod(AngleDegrees + SliceSize * 0.5f, 360.f);
|
|
return FMath::Clamp(FMath::FloorToInt(Shifted / SliceSize), 0, Count - 1);
|
|
}
|
|
|
|
void URadialMenuController::SetTimeDilation(float Scale)
|
|
{
|
|
UGameplayStatics::SetGlobalTimeDilation(this, Scale);
|
|
} |