Files
Naked-Desire/Source/NakedDesire/UI/RadialMenu/RadialMenuController.cpp
T
2026-06-03 15:16:23 +03:00

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);
}