Last active
March 6, 2024 13:24
-
-
Save tor4kichi/2d9801372b3638353154e0b7c1b4afc1 to your computer and use it in GitHub Desktop.
UWPで仮想アナログパッド、バーチャルスティックを実装
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <myControls:VirtualAnalogPad X="{x:Bind CanvasTransform.TranslateX, Mode=TwoWay}" | |
| Y="{x:Bind CanvasTransform.TranslateY, Mode=TwoWay}" | |
| MinX="{x:Bind _vm.HalfAndNegative(_vm.DefaultCanvasSize.Width), Mode=OneWay}" | |
| MaxX="{x:Bind _vm.Half(_vm.DefaultCanvasSize.Width), Mode=OneWay}" | |
| MinY="{x:Bind _vm.HalfAndNegative(_vm.DefaultCanvasSize.Height), Mode=OneWay}" | |
| MaxY="{x:Bind _vm.Half(_vm.DefaultCanvasSize.Height), Mode=OneWay}" | |
| BaseScale="{x:Bind CanvasTransform.ScaleX, Mode=OneWay}" | |
| Margin="32 8" | |
| Background="{ThemeResource SystemBaseLowColor}" | |
| BorderBrush="{ThemeResource SystemBaseMediumColor}" | |
| BorderThickness="1" | |
| ThumbRadius="16" | |
| InputPlayNormalized="0.15" | |
| VelocityPerSecond="25" | |
| /> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <UserControl | |
| x:Class="Starryboard.Views.Controls.VirtualAnalogPad" | |
| xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" | |
| xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" | |
| xmlns:local="using:Starryboard.Views.Controls" | |
| xmlns:d="http://schemas.microsoft.com/expression/blend/2008" | |
| xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" | |
| mc:Ignorable="d" | |
| d:DesignHeight="300" | |
| d:DesignWidth="400"> | |
| <Grid> | |
| <Border x:Name="InputAreaBorder" | |
| PointerPressed="InputAreaBorder_PointerPressed" | |
| PointerMoved="InputAreaBorder_PointerMoved" | |
| PointerReleased="InputAreaBorder_PointerReleased" | |
| PointerCanceled="InputAreaBorder_PointerCanceled" | |
| PointerCaptureLost="InputAreaBorder_PointerCaptureLost" | |
| Background="{x:Bind Background, Mode=OneWay}" | |
| BorderBrush="{x:Bind BorderBrush, Mode=OneWay}" | |
| BorderThickness="{x:Bind BorderThickness, Mode=OneWay}" | |
| IsHitTestVisible="True" | |
| > | |
| </Border> | |
| <Border x:Name="ThumbBorder" | |
| Width="{x:Bind ThumbRadius, Mode=OneWay}" | |
| Height="{x:Bind ThumbRadius, Mode=OneWay}" | |
| Background="{x:Bind ThumbColor, Mode=OneWay}" | |
| CornerRadius="1000" | |
| IsHitTestVisible="False" | |
| Opacity="0" | |
| > | |
| <Border.RenderTransform> | |
| <TranslateTransform x:Name="ThumbBorderTransform" /> | |
| </Border.RenderTransform> | |
| </Border> | |
| <Border Background="{x:Bind CenterPointColor, Mode=OneWay}" | |
| Width="1" | |
| Height="1" /> | |
| </Grid> | |
| </UserControl> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| using CommunityToolkit.Diagnostics; | |
| using System; | |
| using System.Collections.Generic; | |
| using System.Diagnostics; | |
| using System.IO; | |
| using System.Linq; | |
| using System.Numerics; | |
| using System.Runtime.InteropServices.WindowsRuntime; | |
| using Windows.Devices.Input; | |
| using Windows.Foundation; | |
| using Windows.Foundation.Collections; | |
| using Windows.System; | |
| using Windows.UI; | |
| using Windows.UI.Xaml; | |
| using Windows.UI.Xaml.Controls; | |
| using Windows.UI.Xaml.Controls.Primitives; | |
| using Windows.UI.Xaml.Data; | |
| using Windows.UI.Xaml.Input; | |
| using Windows.UI.Xaml.Media; | |
| using Windows.UI.Xaml.Navigation; | |
| #nullable enable | |
| namespace Starryboard.Views.Controls; | |
| public sealed partial class VirtualAnalogPad : UserControl | |
| { | |
| public VirtualAnalogPad() | |
| { | |
| this.InitializeComponent(); | |
| Loaded += VirtualJoystick_Loaded; | |
| _PositionUpdateTimer = DispatcherQueue.GetForCurrentThread().CreateTimer(); | |
| _PositionUpdateTimer.Interval = TimeSpan.FromMilliseconds(8); | |
| _PositionUpdateTimer.IsRepeating = true; | |
| _PositionUpdateTimer.Tick += _PositionUpdateTimer_Tick; | |
| } | |
| private void VirtualJoystick_Loaded(object sender, RoutedEventArgs e) | |
| { | |
| double size = InputAreaRadius; | |
| InputAreaBorder.Width = size; | |
| InputAreaBorder.Height = size; | |
| InputAreaBorder.CornerRadius = new CornerRadius(size); | |
| } | |
| #region Properties | |
| public double InputAreaRadius | |
| { | |
| get { return (double)GetValue(InputAreaRadiusProperty); } | |
| set { SetValue(InputAreaRadiusProperty, value); } | |
| } | |
| public static readonly DependencyProperty InputAreaRadiusProperty = | |
| DependencyProperty.Register("InputAreaRadius", typeof(double), typeof(VirtualAnalogPad), new PropertyMetadata(64d, OnRadiusChanged)); | |
| private static void OnRadiusChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) | |
| { | |
| var _this = (VirtualAnalogPad)d; | |
| double size = (double)e.NewValue; | |
| _this.InputAreaBorder.Width = size; | |
| _this.InputAreaBorder.Height = size; | |
| _this.InputAreaBorder.CornerRadius = new CornerRadius(size); | |
| } | |
| public double X | |
| { | |
| get { return (double)GetValue(XProperty); } | |
| set { SetValue(XProperty, value); } | |
| } | |
| public static readonly DependencyProperty XProperty = | |
| DependencyProperty.Register("X", typeof(double), typeof(VirtualAnalogPad), new PropertyMetadata(0d)); | |
| public double Y | |
| { | |
| get { return (double)GetValue(YProperty); } | |
| set { SetValue(YProperty, value); } | |
| } | |
| public static readonly DependencyProperty YProperty = | |
| DependencyProperty.Register("Y", typeof(double), typeof(VirtualAnalogPad), new PropertyMetadata(0d)); | |
| public double MinX | |
| { | |
| get { return (double)GetValue(MinXProperty); } | |
| set { SetValue(MinXProperty, value); } | |
| } | |
| public static readonly DependencyProperty MinXProperty = | |
| DependencyProperty.Register("MinX", typeof(double), typeof(VirtualAnalogPad), new PropertyMetadata(-720d)); | |
| public double MaxX | |
| { | |
| get { return (double)GetValue(MaxXProperty); } | |
| set { SetValue(MaxXProperty, value); } | |
| } | |
| public static readonly DependencyProperty MaxXProperty = | |
| DependencyProperty.Register("MaxX", typeof(double), typeof(VirtualAnalogPad), new PropertyMetadata(720d)); | |
| public double MinY | |
| { | |
| get { return (double)GetValue(MinYProperty); } | |
| set { SetValue(MinYProperty, value); } | |
| } | |
| public static readonly DependencyProperty MinYProperty = | |
| DependencyProperty.Register("MinY", typeof(double), typeof(VirtualAnalogPad), new PropertyMetadata(-480d)); | |
| public double MaxY | |
| { | |
| get { return (double)GetValue(MaxYProperty); } | |
| set { SetValue(MaxYProperty, value); } | |
| } | |
| public static readonly DependencyProperty MaxYProperty = | |
| DependencyProperty.Register("MaxY", typeof(double), typeof(VirtualAnalogPad), new PropertyMetadata(480d)); | |
| public double BaseScale | |
| { | |
| get { return (double)GetValue(BaseScaleProperty); } | |
| set { SetValue(BaseScaleProperty, value); } | |
| } | |
| public static readonly DependencyProperty BaseScaleProperty = | |
| DependencyProperty.Register("BaseScale", typeof(double), typeof(VirtualAnalogPad), new PropertyMetadata(1d)); | |
| // 最大まで傾けた際のX/Yに毎秒加算する移動量を指定します。 | |
| public double VelocityPerSecond | |
| { | |
| get { return (double)GetValue(VelocityPerSecondProperty); } | |
| set { SetValue(VelocityPerSecondProperty, value); } | |
| } | |
| public static readonly DependencyProperty VelocityPerSecondProperty = | |
| DependencyProperty.Register("VelocityPerSecond", typeof(double), typeof(VirtualAnalogPad), new PropertyMetadata(50d)); | |
| // 入力の遊び。スキップする入力量を指定します。 | |
| public double InputPlayNormalized | |
| { | |
| get { return (double)GetValue(InputPlayNormalizedProperty); } | |
| set | |
| { | |
| Guard.IsBetweenOrEqualTo(value, 0d, 1d); | |
| SetValue(InputPlayNormalizedProperty, value); | |
| } | |
| } | |
| public static readonly DependencyProperty InputPlayNormalizedProperty = | |
| DependencyProperty.Register("InputPlayNormalized", typeof(double), typeof(VirtualAnalogPad), new PropertyMetadata(0.05d)); | |
| public double ThumbRadius | |
| { | |
| get { return (double)GetValue(ThumbRadiusProperty); } | |
| set { SetValue(ThumbRadiusProperty, value); } | |
| } | |
| public static readonly DependencyProperty ThumbRadiusProperty = | |
| DependencyProperty.Register("ThumbRadius", typeof(double), typeof(VirtualAnalogPad), new PropertyMetadata(32d)); | |
| public Brush ThumbColor | |
| { | |
| get { return (Brush)GetValue(ThumbColorProperty); } | |
| set { SetValue(ThumbColorProperty, value); } | |
| } | |
| public static readonly DependencyProperty ThumbColorProperty = | |
| DependencyProperty.Register("ThumbColor", typeof(Brush), typeof(VirtualAnalogPad), new PropertyMetadata(new SolidColorBrush(Colors.White))); | |
| public Brush CenterPointColor | |
| { | |
| get { return (Brush)GetValue(CenterPointColorProperty); } | |
| set { SetValue(CenterPointColorProperty, value); } | |
| } | |
| public static readonly DependencyProperty CenterPointColorProperty = | |
| DependencyProperty.Register("CenterPointColor", typeof(Brush), typeof(VirtualAnalogPad), new PropertyMetadata(new SolidColorBrush(Colors.White))); | |
| public bool IsReverseInput | |
| { | |
| get { return (bool)GetValue(IsReverseInputProperty); } | |
| set { SetValue(IsReverseInputProperty, value); } | |
| } | |
| public static readonly DependencyProperty IsReverseInputProperty = | |
| DependencyProperty.Register("IsReverseInput", typeof(bool), typeof(VirtualAnalogPad), new PropertyMetadata(false)); | |
| #endregion | |
| private readonly DispatcherQueueTimer _PositionUpdateTimer; | |
| double _prevX; | |
| double _prevY; | |
| Vector2 _startPointerPos; | |
| bool _isPressed; | |
| Vector2 _thumbStick = Vector2.Zero; | |
| private void InputAreaBorder_PointerPressed(object sender, PointerRoutedEventArgs e) | |
| { | |
| if (InputAreaBorder.CapturePointer(e.Pointer) is false) { return; } | |
| e.Handled = true; | |
| _isPressed = true; | |
| (_prevX, _prevY) = (X, Y); | |
| _startPointerPos = Window.Current.CoreWindow.PointerPosition.ToVector2(); | |
| ThumbBorderTransform.X = 0; | |
| ThumbBorderTransform.Y = 0; | |
| ThumbBorder.Opacity = 1; | |
| _thumbStick = Vector2.Zero; | |
| _PositionUpdateTimer.Start(); | |
| } | |
| private void InputAreaBorder_PointerMoved(object sender, PointerRoutedEventArgs e) | |
| { | |
| if (_isPressed is false) { return; } | |
| UpdateThumbPosition(Window.Current.CoreWindow.PointerPosition.ToVector2()); | |
| } | |
| void UpdateThumbPosition(Vector2 pointerPos) | |
| { | |
| var moved = pointerPos - _startPointerPos; | |
| if (moved is { X : 0, Y : 0 } ) { return; } | |
| var length = moved.Length(); | |
| var dir = Vector2.Normalize(moved); | |
| float radius = (float)InputAreaRadius * 0.5f; | |
| if (length > radius) | |
| { | |
| _thumbStick = dir * radius; | |
| ThumbBorderTransform.X = _thumbStick.X; | |
| ThumbBorderTransform.Y = _thumbStick.Y; | |
| Window.Current.CoreWindow.PointerPosition = (_startPointerPos + _thumbStick).ToPoint(); | |
| } | |
| else | |
| { | |
| ThumbBorderTransform.X = moved.X; | |
| ThumbBorderTransform.Y = moved.Y; | |
| // play amount | |
| // |----|-------------| | |
| // | / | | |
| // | / | | |
| // | / | | |
| // |/ | | |
| // |--scaled amount---| | |
| // | |
| // 遊び値を引いたamountを 0 ~ 1 の値域に射影する | |
| float play = (float)InputPlayNormalized; | |
| float invPlay = 1 / (1 - play); | |
| var amount = length / radius; | |
| if (amount >= play) | |
| { | |
| _thumbStick = Vector2.Lerp(Vector2.Zero, dir, (amount - play) * invPlay) * radius; | |
| } | |
| else | |
| { | |
| _thumbStick = Vector2.Zero; | |
| } | |
| } | |
| } | |
| private void _PositionUpdateTimer_Tick(DispatcherQueueTimer sender, object args) | |
| { | |
| if (_thumbStick != default) | |
| { | |
| float reverseFactor = IsReverseInput ? 1 : -1; | |
| double speed = VelocityPerSecond * sender.Interval.TotalSeconds * reverseFactor; | |
| X = Math.Clamp(X + _thumbStick.X * speed, MinX * BaseScale, MaxX * BaseScale); | |
| Y = Math.Clamp(Y + _thumbStick.Y * speed, MinY * BaseScale, MaxY * BaseScale); | |
| } | |
| } | |
| private void InputAreaBorder_PointerReleased(object sender, PointerRoutedEventArgs e) | |
| { | |
| _isPressed = false; | |
| e.Handled = true; | |
| InputAreaBorder.ReleasePointerCapture(e.Pointer); | |
| _PositionUpdateTimer.Stop(); | |
| ThumbBorder.Opacity = 0; | |
| } | |
| private void InputAreaBorder_PointerCanceled(object sender, PointerRoutedEventArgs e) | |
| { | |
| InputAreaBorder.ReleasePointerCapture(e.Pointer); | |
| ThumbBorder.Opacity = 0; | |
| (X, Y) = (_prevX, _prevY); | |
| _isPressed = false; | |
| _PositionUpdateTimer.Stop(); | |
| } | |
| private void InputAreaBorder_PointerCaptureLost(object sender, PointerRoutedEventArgs e) | |
| { | |
| if (_isPressed) | |
| { | |
| InputAreaBorder.ReleasePointerCapture(e.Pointer); | |
| ThumbBorder.Opacity = 0; | |
| (X, Y) = (_prevX, _prevY); | |
| _isPressed = false; | |
| _PositionUpdateTimer.Stop(); | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment