Maze / Library /PackageCache /com.unity.inputsystem@1.6.1 /InputSystem /Plugins /OnScreen /OnScreenStick.cs
| using System; | |
| using System.Collections.Generic; | |
| using UnityEngine.EventSystems; | |
| using UnityEngine.Serialization; | |
| using UnityEngine.InputSystem.Layouts; | |
| using UnityEngine.InputSystem.Utilities; | |
| using UnityEngine.UI; | |
| using UnityEditor; | |
| using UnityEditor.AnimatedValues; | |
| ////TODO: custom icon for OnScreenStick component | |
| namespace UnityEngine.InputSystem.OnScreen | |
| { | |
| /// <summary> | |
| /// A stick control displayed on screen and moved around by touch or other pointer | |
| /// input. | |
| /// </summary> | |
| /// <remarks> | |
| /// The <see cref="OnScreenStick"/> works by simulating events from the device specified in the <see cref="OnScreenControl.controlPath"/> | |
| /// property. Some parts of the Input System, such as the <see cref="PlayerInput"/> component, can be set up to | |
| /// auto-switch to a new device when input from them is detected. When a device is switched, any currently running | |
| /// inputs from the previously active device are cancelled. In the case of <see cref="OnScreenStick"/>, this can mean that the | |
| /// <see cref="IPointerUpHandler.OnPointerUp"/> method will be called and the stick will jump back to center, even though | |
| /// the pointer input has not physically been released. | |
| /// | |
| /// To avoid this situation, set the <see cref="useIsolatedInputActions"/> property to true. This will create a set of local | |
| /// Input Actions to drive the stick that are not cancelled when device switching occurs. | |
| /// </remarks> | |
| [] | |
| [] | |
| public class OnScreenStick : OnScreenControl, IPointerDownHandler, IPointerUpHandler, IDragHandler | |
| { | |
| private const string kDynamicOriginClickable = "DynamicOriginClickable"; | |
| /// <summary> | |
| /// Callback to handle OnPointerDown UI events. | |
| /// </summary> | |
| public void OnPointerDown(PointerEventData eventData) | |
| { | |
| if (m_UseIsolatedInputActions) | |
| return; | |
| if (eventData == null) | |
| throw new System.ArgumentNullException(nameof(eventData)); | |
| BeginInteraction(eventData.position, eventData.pressEventCamera); | |
| } | |
| /// <summary> | |
| /// Callback to handle OnDrag UI events. | |
| /// </summary> | |
| public void OnDrag(PointerEventData eventData) | |
| { | |
| if (m_UseIsolatedInputActions) | |
| return; | |
| if (eventData == null) | |
| throw new System.ArgumentNullException(nameof(eventData)); | |
| MoveStick(eventData.position, eventData.pressEventCamera); | |
| } | |
| /// <summary> | |
| /// Callback to handle OnPointerUp UI events. | |
| /// </summary> | |
| public void OnPointerUp(PointerEventData eventData) | |
| { | |
| if (m_UseIsolatedInputActions) | |
| return; | |
| EndInteraction(); | |
| } | |
| private void Start() | |
| { | |
| if (m_UseIsolatedInputActions) | |
| { | |
| // avoid allocations every time the pointer down event fires by allocating these here | |
| // and re-using them | |
| m_RaycastResults = new List<RaycastResult>(); | |
| m_PointerEventData = new PointerEventData(EventSystem.current); | |
| // if the pointer actions have no bindings (the default), add some | |
| if (m_PointerDownAction == null || m_PointerDownAction.bindings.Count == 0) | |
| { | |
| if (m_PointerDownAction == null) | |
| m_PointerDownAction = new InputAction(); | |
| m_PointerDownAction.AddBinding("<Mouse>/leftButton"); | |
| m_PointerDownAction.AddBinding("<Pen>/tip"); | |
| m_PointerDownAction.AddBinding("<Touchscreen>/touch*/press"); | |
| m_PointerDownAction.AddBinding("<XRController>/trigger"); | |
| } | |
| if (m_PointerMoveAction == null || m_PointerMoveAction.bindings.Count == 0) | |
| { | |
| if (m_PointerMoveAction == null) | |
| m_PointerMoveAction = new InputAction(); | |
| m_PointerMoveAction.AddBinding("<Mouse>/position"); | |
| m_PointerMoveAction.AddBinding("<Pen>/position"); | |
| m_PointerMoveAction.AddBinding("<Touchscreen>/touch*/position"); | |
| } | |
| m_PointerDownAction.started += OnPointerDown; | |
| m_PointerDownAction.canceled += OnPointerUp; | |
| m_PointerDownAction.Enable(); | |
| m_PointerMoveAction.Enable(); | |
| } | |
| m_StartPos = ((RectTransform)transform).anchoredPosition; | |
| if (m_Behaviour != Behaviour.ExactPositionWithDynamicOrigin) return; | |
| m_PointerDownPos = m_StartPos; | |
| var dynamicOrigin = new GameObject(kDynamicOriginClickable, typeof(Image)); | |
| dynamicOrigin.transform.SetParent(transform); | |
| var image = dynamicOrigin.GetComponent<Image>(); | |
| image.color = new Color(1, 1, 1, 0); | |
| var rectTransform = (RectTransform)dynamicOrigin.transform; | |
| rectTransform.sizeDelta = new Vector2(m_DynamicOriginRange * 2, m_DynamicOriginRange * 2); | |
| rectTransform.localScale = new Vector3(1, 1, 0); | |
| rectTransform.anchoredPosition3D = Vector3.zero; | |
| image.sprite = SpriteUtilities.CreateCircleSprite(16, new Color32(255, 255, 255, 255)); | |
| image.alphaHitTestMinimumThreshold = 0.5f; | |
| } | |
| private void BeginInteraction(Vector2 pointerPosition, Camera uiCamera) | |
| { | |
| var canvasRect = transform.parent?.GetComponentInParent<RectTransform>(); | |
| if (canvasRect == null) | |
| { | |
| Debug.LogError("OnScreenStick needs to be attached as a child to a UI Canvas to function properly."); | |
| return; | |
| } | |
| switch (m_Behaviour) | |
| { | |
| case Behaviour.RelativePositionWithStaticOrigin: | |
| RectTransformUtility.ScreenPointToLocalPointInRectangle(canvasRect, pointerPosition, uiCamera, out m_PointerDownPos); | |
| break; | |
| case Behaviour.ExactPositionWithStaticOrigin: | |
| RectTransformUtility.ScreenPointToLocalPointInRectangle(canvasRect, pointerPosition, uiCamera, out m_PointerDownPos); | |
| MoveStick(pointerPosition, uiCamera); | |
| break; | |
| case Behaviour.ExactPositionWithDynamicOrigin: | |
| RectTransformUtility.ScreenPointToLocalPointInRectangle(canvasRect, pointerPosition, uiCamera, out var pointerDown); | |
| m_PointerDownPos = ((RectTransform)transform).anchoredPosition = pointerDown; | |
| break; | |
| } | |
| } | |
| private void MoveStick(Vector2 pointerPosition, Camera uiCamera) | |
| { | |
| var canvasRect = transform.parent?.GetComponentInParent<RectTransform>(); | |
| if (canvasRect == null) | |
| { | |
| Debug.LogError("OnScreenStick needs to be attached as a child to a UI Canvas to function properly."); | |
| return; | |
| } | |
| RectTransformUtility.ScreenPointToLocalPointInRectangle(canvasRect, pointerPosition, uiCamera, out var position); | |
| var delta = position - m_PointerDownPos; | |
| switch (m_Behaviour) | |
| { | |
| case Behaviour.RelativePositionWithStaticOrigin: | |
| delta = Vector2.ClampMagnitude(delta, movementRange); | |
| ((RectTransform)transform).anchoredPosition = (Vector2)m_StartPos + delta; | |
| break; | |
| case Behaviour.ExactPositionWithStaticOrigin: | |
| delta = position - (Vector2)m_StartPos; | |
| delta = Vector2.ClampMagnitude(delta, movementRange); | |
| ((RectTransform)transform).anchoredPosition = (Vector2)m_StartPos + delta; | |
| break; | |
| case Behaviour.ExactPositionWithDynamicOrigin: | |
| delta = Vector2.ClampMagnitude(delta, movementRange); | |
| ((RectTransform)transform).anchoredPosition = m_PointerDownPos + delta; | |
| break; | |
| } | |
| var newPos = new Vector2(delta.x / movementRange, delta.y / movementRange); | |
| SendValueToControl(newPos); | |
| } | |
| private void EndInteraction() | |
| { | |
| ((RectTransform)transform).anchoredPosition = m_PointerDownPos = m_StartPos; | |
| SendValueToControl(Vector2.zero); | |
| } | |
| private void OnPointerDown(InputAction.CallbackContext ctx) | |
| { | |
| Debug.Assert(EventSystem.current != null); | |
| var screenPosition = Vector2.zero; | |
| if (ctx.control?.device is Pointer pointer) | |
| screenPosition = pointer.position.ReadValue(); | |
| m_PointerEventData.position = screenPosition; | |
| EventSystem.current.RaycastAll(m_PointerEventData, m_RaycastResults); | |
| if (m_RaycastResults.Count == 0) | |
| return; | |
| var stickSelected = false; | |
| foreach (var result in m_RaycastResults) | |
| { | |
| if (result.gameObject != gameObject) continue; | |
| stickSelected = true; | |
| break; | |
| } | |
| if (!stickSelected) | |
| return; | |
| BeginInteraction(screenPosition, GetCameraFromCanvas()); | |
| m_PointerMoveAction.performed += OnPointerMove; | |
| } | |
| private void OnPointerMove(InputAction.CallbackContext ctx) | |
| { | |
| // only pointer devices are allowed | |
| Debug.Assert(ctx.control?.device is Pointer); | |
| var screenPosition = ((Pointer)ctx.control.device).position.ReadValue(); | |
| MoveStick(screenPosition, GetCameraFromCanvas()); | |
| } | |
| private void OnPointerUp(InputAction.CallbackContext ctx) | |
| { | |
| EndInteraction(); | |
| m_PointerMoveAction.performed -= OnPointerMove; | |
| } | |
| private Camera GetCameraFromCanvas() | |
| { | |
| var canvas = GetComponentInParent<Canvas>(); | |
| var renderMode = canvas?.renderMode; | |
| if (renderMode == RenderMode.ScreenSpaceOverlay | |
| || (renderMode == RenderMode.ScreenSpaceCamera && canvas?.worldCamera == null)) | |
| return null; | |
| return canvas?.worldCamera ?? Camera.main; | |
| } | |
| private void OnDrawGizmosSelected() | |
| { | |
| Gizmos.matrix = ((RectTransform)transform.parent).localToWorldMatrix; | |
| var startPos = ((RectTransform)transform).anchoredPosition; | |
| if (Application.isPlaying) | |
| startPos = m_StartPos; | |
| Gizmos.color = new Color32(84, 173, 219, 255); | |
| var center = startPos; | |
| if (Application.isPlaying && m_Behaviour == Behaviour.ExactPositionWithDynamicOrigin) | |
| center = m_PointerDownPos; | |
| DrawGizmoCircle(center, m_MovementRange); | |
| if (m_Behaviour != Behaviour.ExactPositionWithDynamicOrigin) return; | |
| Gizmos.color = new Color32(158, 84, 219, 255); | |
| DrawGizmoCircle(startPos, m_DynamicOriginRange); | |
| } | |
| private void DrawGizmoCircle(Vector2 center, float radius) | |
| { | |
| for (var i = 0; i < 32; i++) | |
| { | |
| var radians = i / 32f * Mathf.PI * 2; | |
| var nextRadian = (i + 1) / 32f * Mathf.PI * 2; | |
| Gizmos.DrawLine( | |
| new Vector3(center.x + Mathf.Cos(radians) * radius, center.y + Mathf.Sin(radians) * radius, 0), | |
| new Vector3(center.x + Mathf.Cos(nextRadian) * radius, center.y + Mathf.Sin(nextRadian) * radius, 0)); | |
| } | |
| } | |
| private void UpdateDynamicOriginClickableArea() | |
| { | |
| var dynamicOriginTransform = transform.Find(kDynamicOriginClickable); | |
| if (dynamicOriginTransform) | |
| { | |
| var rectTransform = (RectTransform)dynamicOriginTransform; | |
| rectTransform.sizeDelta = new Vector2(m_DynamicOriginRange * 2, m_DynamicOriginRange * 2); | |
| } | |
| } | |
| /// <summary> | |
| /// The distance from the onscreen control's center of origin, around which the control can move. | |
| /// </summary> | |
| public float movementRange | |
| { | |
| get => m_MovementRange; | |
| set => m_MovementRange = value; | |
| } | |
| /// <summary> | |
| /// Defines the circular region where the onscreen control may have it's origin placed. | |
| /// </summary> | |
| /// <remarks> | |
| /// This only applies if <see cref="behaviour"/> is set to <see cref="Behaviour.ExactPositionWithDynamicOrigin"/>. | |
| /// When the first press is within this region, then the control will appear at that position and have it's origin of motion placed there. | |
| /// Otherwise, if pressed outside of this region the control will ignore it. | |
| /// This property defines the radius of the circular region. The center point being defined by the component position in the scene. | |
| /// </remarks> | |
| public float dynamicOriginRange | |
| { | |
| get => m_DynamicOriginRange; | |
| set | |
| { | |
| // ReSharper disable once CompareOfFloatsByEqualityOperator | |
| if (m_DynamicOriginRange != value) | |
| { | |
| m_DynamicOriginRange = value; | |
| UpdateDynamicOriginClickableArea(); | |
| } | |
| } | |
| } | |
| /// <summary> | |
| /// Prevents stick interactions from getting cancelled due to device switching. | |
| /// </summary> | |
| /// <remarks> | |
| /// This property is useful for scenarios where the active device switches automatically | |
| /// based on the most recently actuated device. A common situation where this happens is | |
| /// when using a <see cref="PlayerInput"/> component with Auto-switch set to true. Imagine | |
| /// a mobile game where an on-screen stick simulates the left stick of a gamepad device. | |
| /// When the on-screen stick is moved, the Input System will see an input event from a gamepad | |
| /// and switch the active device to it. This causes any active actions to be cancelled, including | |
| /// the pointer action driving the on screen stick, which results in the stick jumping back to | |
| /// the center as though it had been released. | |
| /// | |
| /// In isolated mode, the actions driving the stick are not cancelled because they are | |
| /// unique Input Action instances that don't share state with any others. | |
| /// </remarks> | |
| public bool useIsolatedInputActions | |
| { | |
| get => m_UseIsolatedInputActions; | |
| set => m_UseIsolatedInputActions = value; | |
| } | |
| [] | |
| [] | |
| [] | |
| private float m_MovementRange = 50; | |
| [] | |
| [] | |
| [] | |
| private float m_DynamicOriginRange = 100; | |
| [] | |
| [] | |
| private string m_ControlPath; | |
| [] | |
| [ | |
| ] | |
| private Behaviour m_Behaviour; | |
| [] | |
| [ | |
| ] | |
| private bool m_UseIsolatedInputActions; | |
| [] | |
| [ | |
| ] | |
| private InputAction m_PointerDownAction; | |
| [] | |
| [ | |
| ] | |
| private InputAction m_PointerMoveAction; | |
| private Vector3 m_StartPos; | |
| private Vector2 m_PointerDownPos; | |
| [] | |
| private List<RaycastResult> m_RaycastResults; | |
| [] | |
| private PointerEventData m_PointerEventData; | |
| protected override string controlPathInternal | |
| { | |
| get => m_ControlPath; | |
| set => m_ControlPath = value; | |
| } | |
| /// <summary>Defines how the onscreen stick will move relative to it's origin and the press position.</summary> | |
| public Behaviour behaviour | |
| { | |
| get => m_Behaviour; | |
| set => m_Behaviour = value; | |
| } | |
| /// <summary>Defines how the onscreen stick will move relative to it's center of origin and the press position.</summary> | |
| public enum Behaviour | |
| { | |
| /// <summary>The control's center of origin is fixed in the scene. | |
| /// The control will begin un-actuated at it's centered position and then move relative to the press motion.</summary> | |
| RelativePositionWithStaticOrigin, | |
| /// <summary>The control's center of origin is fixed in the scene. | |
| /// The control may begin from an actuated position to ensure it is always tracking the current press position.</summary> | |
| ExactPositionWithStaticOrigin, | |
| /// <summary>The control's center of origin is determined by the initial press position. | |
| /// The control will begin unactuated at this center position and then track the current press position.</summary> | |
| ExactPositionWithDynamicOrigin | |
| } | |
| [] | |
| internal class OnScreenStickEditor : UnityEditor.Editor | |
| { | |
| private AnimBool m_ShowDynamicOriginOptions; | |
| private AnimBool m_ShowIsolatedInputActions; | |
| private SerializedProperty m_UseIsolatedInputActions; | |
| private SerializedProperty m_Behaviour; | |
| private SerializedProperty m_ControlPathInternal; | |
| private SerializedProperty m_MovementRange; | |
| private SerializedProperty m_DynamicOriginRange; | |
| private SerializedProperty m_PointerDownAction; | |
| private SerializedProperty m_PointerMoveAction; | |
| public void OnEnable() | |
| { | |
| m_ShowDynamicOriginOptions = new AnimBool(false); | |
| m_ShowIsolatedInputActions = new AnimBool(false); | |
| m_UseIsolatedInputActions = serializedObject.FindProperty(nameof(OnScreenStick.m_UseIsolatedInputActions)); | |
| m_Behaviour = serializedObject.FindProperty(nameof(OnScreenStick.m_Behaviour)); | |
| m_ControlPathInternal = serializedObject.FindProperty(nameof(OnScreenStick.m_ControlPath)); | |
| m_MovementRange = serializedObject.FindProperty(nameof(OnScreenStick.m_MovementRange)); | |
| m_DynamicOriginRange = serializedObject.FindProperty(nameof(OnScreenStick.m_DynamicOriginRange)); | |
| m_PointerDownAction = serializedObject.FindProperty(nameof(OnScreenStick.m_PointerDownAction)); | |
| m_PointerMoveAction = serializedObject.FindProperty(nameof(OnScreenStick.m_PointerMoveAction)); | |
| } | |
| public override void OnInspectorGUI() | |
| { | |
| EditorGUILayout.PropertyField(m_MovementRange); | |
| EditorGUILayout.PropertyField(m_ControlPathInternal); | |
| EditorGUILayout.PropertyField(m_Behaviour); | |
| m_ShowDynamicOriginOptions.target = ((OnScreenStick)target).behaviour == | |
| Behaviour.ExactPositionWithDynamicOrigin; | |
| if (EditorGUILayout.BeginFadeGroup(m_ShowDynamicOriginOptions.faded)) | |
| { | |
| EditorGUI.indentLevel++; | |
| EditorGUI.BeginChangeCheck(); | |
| EditorGUILayout.PropertyField(m_DynamicOriginRange); | |
| if (EditorGUI.EndChangeCheck()) | |
| { | |
| ((OnScreenStick)target).UpdateDynamicOriginClickableArea(); | |
| } | |
| EditorGUI.indentLevel--; | |
| } | |
| EditorGUILayout.EndFadeGroup(); | |
| EditorGUILayout.PropertyField(m_UseIsolatedInputActions); | |
| m_ShowIsolatedInputActions.target = m_UseIsolatedInputActions.boolValue; | |
| if (EditorGUILayout.BeginFadeGroup(m_ShowIsolatedInputActions.faded)) | |
| { | |
| EditorGUI.indentLevel++; | |
| EditorGUILayout.PropertyField(m_PointerDownAction); | |
| EditorGUILayout.PropertyField(m_PointerMoveAction); | |
| EditorGUI.indentLevel--; | |
| } | |
| EditorGUILayout.EndFadeGroup(); | |
| serializedObject.ApplyModifiedProperties(); | |
| } | |
| } | |
| } | |
| } | |