Files
UnrealEngine/Engine/Source/Runtime/Slate/Private/Widgets/Input/SVirtualJoystick.cpp
2025-05-18 13:04:45 +08:00

611 lines
18 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "Widgets/Input/SVirtualJoystick.h"
#include "Rendering/DrawElements.h"
#include "Misc/ConfigCacheIni.h"
#include "Framework/Application/SlateApplication.h"
#include "GenericPlatform/GenericPlatformInputDeviceMapper.h"
const float OPACITY_LERP_RATE = 3.f;
static FORCEINLINE float GetScaleFactor(const FGeometry& Geometry)
{
const float DesiredWidth = 1024.0f;
float UndoDPIScaling = 1.0f / Geometry.Scale;
return (Geometry.GetDrawSize().GetMax() / DesiredWidth) * UndoDPIScaling;
}
FORCEINLINE float SVirtualJoystick::GetBaseOpacity()
{
return (State == State_Active || State == State_CountingDownToInactive) ? ActiveOpacity : InactiveOpacity;
}
void SVirtualJoystick::FControlData::Reset()
{
// snap the visual center back to normal (for controls that have a center on touch)
VisualCenter = CorrectedCenter;
}
void SVirtualJoystick::Construct(const FArguments& InArgs)
{
bVisible = true;
bPreventReCenter = false;
// listen for displaymetrics changes to reposition controls
FSlateApplication::Get().GetPlatformApplication()->OnDisplayMetricsChanged().AddSP(this, &SVirtualJoystick::HandleDisplayMetricsChanged);
}
void SVirtualJoystick::HandleDisplayMetricsChanged(const FDisplayMetrics& NewDisplayMetric)
{
// Mark all controls to be repositioned on next tick
for (int32 ControlIndex = 0; ControlIndex < Controls.Num(); ControlIndex++)
{
Controls[ControlIndex].bHasBeenPositioned = false;
}
}
void SVirtualJoystick::SetGlobalParameters(float InActiveOpacity, float InInactiveOpacity, float InTimeUntilDeactive, float InTimeUntilReset, float InActivationDelay, bool InbPreventReCenter, float InStartupDelay)
{
ActiveOpacity = InActiveOpacity;
InactiveOpacity = InInactiveOpacity;
TimeUntilDeactive = InTimeUntilDeactive;
TimeUntilReset = InTimeUntilReset;
ActivationDelay = InActivationDelay;
StartupDelay = InStartupDelay;
bPreventReCenter = InbPreventReCenter;
if (StartupDelay > 0.f)
{
State = State_WaitForStart;
}
}
bool SVirtualJoystick::ShouldDisplayTouchInterface()
{
bool bAlwaysShowTouchInterface = false;
GConfig->GetBool(TEXT("/Script/Engine.InputSettings"), TEXT("bAlwaysShowTouchInterface"), bAlwaysShowTouchInterface, GInputIni);
// do we want to show virtual joysticks?
return FPlatformMisc::GetUseVirtualJoysticks() || bAlwaysShowTouchInterface || (FSlateApplication::Get().IsFakingTouchEvents() && FPlatformMisc::ShouldDisplayTouchInterfaceOnFakingTouchEvents());
}
static float ResolveRelativePosition(float Position, float RelativeTo, float ScaleFactor)
{
// absolute from edge
if (Position < -1.0f)
{
return RelativeTo + Position * ScaleFactor;
}
// relative from edge
else if (Position < 0.0f)
{
return RelativeTo + Position * RelativeTo;
}
// relative from 0
else if (Position <= 1.0f)
{
return Position * RelativeTo;
}
// absolute from 0
else
{
return Position * ScaleFactor;
}
}
static bool PositionIsInside(const FVector2D& Center, const FVector2D& Position, const FVector2D& BoxSize)
{
return
Position.X >= Center.X - BoxSize.X * 0.5f &&
Position.X <= Center.X + BoxSize.X * 0.5f &&
Position.Y >= Center.Y - BoxSize.Y * 0.5f &&
Position.Y <= Center.Y + BoxSize.Y * 0.5f;
}
int32 SVirtualJoystick::OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const
{
int32 RetLayerId = LayerId;
if (bVisible)
{
FLinearColor ColorAndOpacitySRGB = InWidgetStyle.GetColorAndOpacityTint();
ColorAndOpacitySRGB.A = CurrentOpacity;
for (int32 ControlIndex = 0; ControlIndex < Controls.Num(); ControlIndex++)
{
const FControlData& Control = Controls[ControlIndex];
if (Control.Info.Image2.IsValid())
{
FSlateDrawElement::MakeBox(
OutDrawElements,
RetLayerId++,
AllottedGeometry.ToPaintGeometry(
Control.CorrectedVisualSize,
FSlateLayoutTransform(Control.VisualCenter - FVector2D(Control.CorrectedVisualSize.X * 0.5f, Control.CorrectedVisualSize.Y * 0.5f))
),
Control.Info.Image2->GetSlateBrush(),
ESlateDrawEffect::None,
ColorAndOpacitySRGB
);
}
if (Control.Info.Image1.IsValid())
{
FSlateDrawElement::MakeBox(
OutDrawElements,
RetLayerId++,
AllottedGeometry.ToPaintGeometry(
Control.CorrectedThumbSize,
FSlateLayoutTransform(Control.VisualCenter + Control.ThumbPosition - FVector2D(Control.CorrectedThumbSize.X * 0.5f, Control.CorrectedThumbSize.Y * 0.5f))
),
Control.Info.Image1->GetSlateBrush(),
ESlateDrawEffect::None,
ColorAndOpacitySRGB
);
}
}
}
return RetLayerId;
}
FVector2D SVirtualJoystick::ComputeDesiredSize( float ) const
{
return FVector2D(100, 100);
}
bool SVirtualJoystick::SupportsKeyboardFocus() const
{
return false;
}
FReply SVirtualJoystick::OnTouchStarted(const FGeometry& MyGeometry, const FPointerEvent& Event)
{
// UE_LOG(LogTemp, Log, TEXT("Pointer index: %d"), Event.GetPointerIndex());
FVector2D LocalCoord = MyGeometry.AbsoluteToLocal(Event.GetScreenSpacePosition());
for (int32 ControlIndex = 0; ControlIndex < Controls.Num(); ControlIndex++)
{
FControlData& Control = Controls[ControlIndex];
// skip controls already in use
if (Control.CapturedPointerIndex == -1)
{
if (PositionIsInside(Control.CorrectedCenter, LocalCoord, Control.CorrectedInteractionSize))
{
// Align Joystick inside of Screen
AlignBoxIntoScreen(LocalCoord, Control.CorrectedVisualSize, MyGeometry.GetLocalSize());
Control.CapturedPointerIndex = Event.GetPointerIndex();
if (ActivationDelay == 0.f)
{
CurrentOpacity = ActiveOpacity;
if (!bPreventReCenter)
{
Control.VisualCenter = LocalCoord;
}
if (HandleTouch(ControlIndex, LocalCoord, MyGeometry.GetLocalSize())) // Never fail!
{
return FReply::Handled().CaptureMouse(SharedThis(this));
}
}
else
{
Control.bNeedUpdatedCenter = true;
Control.ElapsedTime = 0.f;
Control.NextCenter = LocalCoord;
return FReply::Unhandled();
}
}
}
}
// CapturedPointerIndex[CapturedJoystick] = -1;
return FReply::Unhandled();
}
FReply SVirtualJoystick::OnTouchMoved(const FGeometry& MyGeometry, const FPointerEvent& Event)
{
FVector2D LocalCoord = MyGeometry.AbsoluteToLocal( Event.GetScreenSpacePosition() );
for (int32 ControlIndex = 0; ControlIndex < Controls.Num(); ControlIndex++)
{
FControlData& Control = Controls[ControlIndex];
// is this control the one captured to this pointer?
if (Control.CapturedPointerIndex == Event.GetPointerIndex())
{
if (Control.bNeedUpdatedCenter)
{
return FReply::Unhandled();
}
else if (HandleTouch(ControlIndex, LocalCoord, MyGeometry.GetLocalSize()))
{
return FReply::Handled();
}
}
}
return FReply::Unhandled();
}
FReply SVirtualJoystick::OnTouchEnded(const FGeometry& MyGeometry, const FPointerEvent& Event)
{
for (int32 ControlIndex = 0; ControlIndex < Controls.Num(); ControlIndex++)
{
FControlData& Control = Controls[ControlIndex];
// is this control the one captured to this pointer?
if (Control.CapturedPointerIndex == Event.GetPointerIndex())
{
// release and center the joystick
Control.ThumbPosition = FVector2D(0, 0);
Control.CapturedPointerIndex = -1;
// send one more joystick update for the centering
Control.bSendOneMoreEvent = true;
// Pass event as unhandled if time is too short
if (Control.bNeedUpdatedCenter)
{
Control.bNeedUpdatedCenter = false;
return FReply::Unhandled();
}
return FReply::Handled().ReleaseMouseCapture();
}
}
return FReply::Unhandled();
}
FVector2D SVirtualJoystick::ComputeThumbPosition(int32 ControlIndex, const FVector2D& LocalCoord, float* OutDistanceToTouchSqr, float* OutDistanceToEdgeSqr)
{
float DistanceToTouchSqr = 0.0f;
float DistanceToEdgeSqr = 0.0f;
FVector2D Position;
const FControlData& Control = Controls[ControlIndex];
// figure out position around center
FVector2D Offset = LocalCoord - Control.VisualCenter;
// only do work if we aren't at the center
if (Offset == FVector2D(0, 0))
{
Position = Offset;
}
else
{
// clamp to the ellipse of the stick (snaps to the visual size, so, the art should go all the way to the edge of the texture)
DistanceToTouchSqr = Offset.SizeSquared();
float Angle = FMath::Atan2(Offset.Y, Offset.X);
// length along line to ellipse: L = 1.0 / sqrt(((sin(T)/Rx)^2 + (cos(T)/Ry)^2))
float CosAngle = FMath::Cos(Angle);
float SinAngle = FMath::Sin(Angle);
float XTerm = CosAngle / (Control.CorrectedVisualSize.X * 0.5f);
float YTerm = SinAngle / (Control.CorrectedVisualSize.Y * 0.5f);
float XYTermSqr = XTerm * XTerm + YTerm * YTerm;
DistanceToEdgeSqr = 1.0f / XYTermSqr;
// only clamp
if (DistanceToTouchSqr > DistanceToEdgeSqr)
{
float DistanceToEdge = FMath::InvSqrt(XYTermSqr);
Position = FVector2D(DistanceToEdge * CosAngle, DistanceToEdge * SinAngle);
}
else
{
Position = Offset;
}
}
if (OutDistanceToTouchSqr != nullptr)
{
*OutDistanceToTouchSqr = DistanceToTouchSqr;
}
if (OutDistanceToEdgeSqr != nullptr)
{
*OutDistanceToEdgeSqr = DistanceToEdgeSqr;
}
return Position;
}
bool SVirtualJoystick::HandleTouch(int32 ControlIndex, const FVector2D& LocalCoord, const FVector2D& ScreenSize)
{
FControlData& Control = Controls[ControlIndex];
Control.ThumbPosition = ComputeThumbPosition(ControlIndex, LocalCoord);
FVector2D AbsoluteThumbPos = Control.ThumbPosition + Controls[ControlIndex].VisualCenter;
AlignBoxIntoScreen(AbsoluteThumbPos, Control.CorrectedThumbSize, ScreenSize);
Control.ThumbPosition = AbsoluteThumbPos - Controls[ControlIndex].VisualCenter;
return true;
}
void SVirtualJoystick::Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime)
{
if (State == State_WaitForStart || State == State_CountingDownToStart)
{
CurrentOpacity = 0.f;
}
else
{
// lerp to the desired opacity based on whether the user is interacting with the joystick
CurrentOpacity = FMath::Lerp(CurrentOpacity, GetBaseOpacity(), OPACITY_LERP_RATE * InDeltaTime);
}
// count how many controls are active
int32 NumActiveControls = 0;
// figure out how much to scale the control sizes
float ScaleFactor = GetScaleFactor(AllottedGeometry);
for (int32 ControlIndex = 0; ControlIndex < Controls.Num(); ControlIndex++)
{
FControlData& Control = Controls[ControlIndex];
if (Control.bNeedUpdatedCenter)
{
Control.ElapsedTime += InDeltaTime;
if (Control.ElapsedTime > ActivationDelay)
{
Control.bNeedUpdatedCenter = false;
CurrentOpacity = ActiveOpacity;
if (!bPreventReCenter)
{
Control.VisualCenter = Control.NextCenter;
}
HandleTouch(ControlIndex, Control.NextCenter, AllottedGeometry.GetLocalSize());
}
}
// calculate absolute positions based on geometry
// @todo: Need to manage geometry changing!
if (!Control.bHasBeenPositioned || ScaleFactor != PreviousScalingFactor)
{
const FControlInfo& ControlInfo = Control.Info;
// update all the sizes
Control.CorrectedCenter = FVector2D(ResolveRelativePosition(ControlInfo.Center.X, AllottedGeometry.GetLocalSize().X, ScaleFactor), ResolveRelativePosition(ControlInfo.Center.Y, AllottedGeometry.GetLocalSize().Y, ScaleFactor));
Control.VisualCenter = Control.CorrectedCenter;
Control.CorrectedVisualSize = FVector2D(ResolveRelativePosition(ControlInfo.VisualSize.X, AllottedGeometry.GetLocalSize().X, ScaleFactor), ResolveRelativePosition(ControlInfo.VisualSize.Y, AllottedGeometry.GetLocalSize().Y, ScaleFactor));
Control.CorrectedInteractionSize = FVector2D(ResolveRelativePosition(ControlInfo.InteractionSize.X, AllottedGeometry.GetLocalSize().X, ScaleFactor), ResolveRelativePosition(ControlInfo.InteractionSize.Y, AllottedGeometry.GetLocalSize().Y, ScaleFactor));
Control.CorrectedThumbSize = FVector2D(ResolveRelativePosition(ControlInfo.ThumbSize.X, AllottedGeometry.GetLocalSize().X, ScaleFactor), ResolveRelativePosition(ControlInfo.ThumbSize.Y, AllottedGeometry.GetLocalSize().Y, ScaleFactor));
Control.CorrectedInputScale = ControlInfo.InputScale; // *ScaleFactor;
Control.bHasBeenPositioned = true;
}
if (Control.CapturedPointerIndex >= 0 || Control.bSendOneMoreEvent)
{
// cache the key released state so we can send input pressed/released events later
bool bButtonPressed = !Control.bSendOneMoreEvent;
Control.bSendOneMoreEvent = false;
// Get the corrected thumb offset scale (now allows ellipse instead of assuming square)
FVector2D ThumbScaledOffset = FVector2D(Control.ThumbPosition.X * 2.0f / Control.CorrectedVisualSize.X, Control.ThumbPosition.Y * 2.0f / Control.CorrectedVisualSize.Y);
float ThumbSquareSum = ThumbScaledOffset.X * ThumbScaledOffset.X + ThumbScaledOffset.Y * ThumbScaledOffset.Y;
float ThumbMagnitude = FMath::Sqrt(ThumbSquareSum);
FVector2D ThumbNormalized = FVector2D(0.f, 0.f);
if (ThumbSquareSum > SMALL_NUMBER)
{
const float Scale = 1.0f / ThumbMagnitude;
ThumbNormalized = FVector2D(ThumbScaledOffset.X * Scale, ThumbScaledOffset.Y * Scale);
}
// Find the scale to apply to ThumbNormalized vector to project onto unit square
float ToSquareScale = fabs(ThumbNormalized.Y) > fabs(ThumbNormalized.X) ? FMath::Sqrt((ThumbNormalized.X * ThumbNormalized.X) / (ThumbNormalized.Y * ThumbNormalized.Y) + 1.0f)
: ThumbNormalized.X == 0.0f ? 1.0f : FMath::Sqrt((ThumbNormalized.Y * ThumbNormalized.Y) / (ThumbNormalized.X * ThumbNormalized.X) + 1.0f);
// Apply proportional offset corrected for projection to unit square
FVector2D NormalizedOffset = ThumbNormalized * Control.CorrectedInputScale * ThumbMagnitude * ToSquareScale;
// now pass the fake joystick events to the game
const FGamepadKeyNames::Type XAxis = (Control.Info.MainInputKey.IsValid() ? Control.Info.MainInputKey.GetFName() : (ControlIndex == 0 ? FGamepadKeyNames::LeftAnalogX : FGamepadKeyNames::RightAnalogX));
const FGamepadKeyNames::Type YAxis = (Control.Info.AltInputKey.IsValid() ? Control.Info.AltInputKey.GetFName() : (ControlIndex == 0 ? FGamepadKeyNames::LeftAnalogY : FGamepadKeyNames::RightAnalogY));
FSlateApplication::Get().SetAllUserFocusToGameViewport();
FInputDeviceId PrimaryInputDevice = IPlatformInputDeviceMapper::Get().GetPrimaryInputDeviceForUser(FSlateApplicationBase::SlateAppPrimaryPlatformUser);
auto ApplyInput = [PrimaryInputDevice](const FGamepadKeyNames::Type KeyName, float Delta, bool bTreatAsButton, bool bPressed)
{
FKey Key(KeyName);
if (Key.IsAnalog())
{
FSlateApplication::Get().OnControllerAnalog(KeyName, FSlateApplicationBase::SlateAppPrimaryPlatformUser, PrimaryInputDevice, Delta);
}
else if (bTreatAsButton)
{
if (bPressed)
{
FSlateApplication::Get().OnControllerButtonPressed(KeyName, FSlateApplicationBase::SlateAppPrimaryPlatformUser, PrimaryInputDevice, false);
}
else
{
FSlateApplication::Get().OnControllerButtonReleased(KeyName, FSlateApplicationBase::SlateAppPrimaryPlatformUser, PrimaryInputDevice, false);
}
}
else if (Delta != 0.0f)
{
FSlateApplication::Get().OnControllerButtonPressed(KeyName, FSlateApplicationBase::SlateAppPrimaryPlatformUser, PrimaryInputDevice, false);
}
else
{
FSlateApplication::Get().OnControllerButtonReleased(KeyName, FSlateApplicationBase::SlateAppPrimaryPlatformUser, PrimaryInputDevice, false);
}
};
ApplyInput(XAxis, NormalizedOffset.X, Control.Info.bTreatAsButton, bButtonPressed);
// ignore the Y axis if this is a button
if (!Control.Info.bTreatAsButton)
{
ApplyInput(YAxis, -NormalizedOffset.Y, false, bButtonPressed);
}
}
// is this active?
if (Control.CapturedPointerIndex != -1)
{
NumActiveControls++;
}
}
// we need to store the computed scale factor so we can compare it with the value computed in the following frame and, if necessary, recompute widget position
PreviousScalingFactor = ScaleFactor;
// STATE MACHINE!
if (NumActiveControls > 0 || bPreventReCenter)
{
// any active control snaps the state to active immediately
State = State_Active;
}
else
{
switch (State)
{
case State_WaitForStart:
{
State = State_CountingDownToStart;
Countdown = StartupDelay;
}
break;
case State_CountingDownToStart:
// update the countdown
Countdown -= InDeltaTime;
if (Countdown <= 0.0f)
{
State = State_Inactive;
}
break;
case State_Active:
if (NumActiveControls == 0)
{
// start going to inactive
State = State_CountingDownToInactive;
Countdown = TimeUntilDeactive;
}
break;
case State_CountingDownToInactive:
// update the countdown
Countdown -= InDeltaTime;
if (Countdown <= 0.0f)
{
// should we start counting down to a control reset?
if (TimeUntilReset > 0.0f)
{
State = State_CountingDownToReset;
Countdown = TimeUntilReset;
}
else
{
// if not, then just go inactive
State = State_Inactive;
}
}
break;
case State_CountingDownToReset:
Countdown -= InDeltaTime;
if (Countdown <= 0.0f)
{
// reset all the controls
for (int32 ControlIndex = 0; ControlIndex < Controls.Num(); ControlIndex++)
{
Controls[ControlIndex].Reset();
}
// finally, go inactive
State = State_Inactive;
}
break;
}
}
}
void SVirtualJoystick::SetJoystickVisibility(const bool bInVisible, const bool bInFade)
{
// if we aren't fading, then just set the current opacity to desired
if (!bInFade)
{
if (bInVisible)
{
CurrentOpacity = GetBaseOpacity();
}
else
{
CurrentOpacity = 0.f;
}
}
bVisible = bInVisible;
}
void SVirtualJoystick::AddControl(const FControlInfo& Control)
{
FControlData& ControlData = Controls.AddDefaulted_GetRef();
ControlData.Info = Control;
}
void SVirtualJoystick::ClearControls()
{
Controls.Empty();
}
void SVirtualJoystick::SetControls(const TArray<FControlInfo>& InControls)
{
ClearControls();
for (const FControlInfo& ControlInfo : InControls)
{
AddControl(ControlInfo);
}
}
void SVirtualJoystick::AlignBoxIntoScreen(FVector2D& Position, const FVector2D& Size, const FVector2D& ScreenSize)
{
if (Size.X > ScreenSize.X || Size.Y > ScreenSize.Y)
{
return;
}
// Align box to fit into screen
if (Position.X - Size.X * 0.5f < 0.f)
{
Position.X = Size.X * 0.5f;
}
if (Position.X + Size.X * 0.5f > ScreenSize.X)
{
Position.X = ScreenSize.X - Size.X * 0.5f;
}
if (Position.Y - Size.Y * 0.5f < 0.f)
{
Position.Y = Size.Y * 0.5f;
}
if (Position.Y + Size.Y * 0.5f > ScreenSize.Y)
{
Position.Y = ScreenSize.Y - Size.Y * 0.5f;
}
}