Started radial menu setup
This commit is contained in:
@@ -0,0 +1,225 @@
|
||||
// RadialMenuController.cpp
|
||||
|
||||
#include "RadialMenuController.h"
|
||||
#include "RadialMenuWidget.h"
|
||||
#include "GameFramework/PlayerController.h"
|
||||
#include "Kismet/GameplayStatics.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;
|
||||
}
|
||||
|
||||
// Angle clockwise from straight up (12 o'clock), in [0,360).
|
||||
// atan2(x, y) gives angle from +Y axis turning toward +X = clockwise. Nice.
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user