123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496 |
- #if PACKAGE_DOCS_GENERATION || UNITY_INPUT_SYSTEM_ENABLE_UI
- using System;
- using System.Collections.Generic;
- using UnityEngine.EventSystems;
- using UnityEngine.Serialization;
- using UnityEngine.InputSystem.Layouts;
- using UnityEngine.InputSystem.Utilities;
- using UnityEngine.UI;
-
- #if UNITY_EDITOR
- using UnityEditor;
- using UnityEditor.AnimatedValues;
- #endif
- ////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>
- [AddComponentMenu("Input/On-Screen Stick")]
- [HelpURL(InputSystem.kDocUrl + "/manual/OnScreen.html#on-screen-sticks")]
- 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;
- }
-
- [FormerlySerializedAs("movementRange")]
- [SerializeField]
- [Min(0)]
- private float m_MovementRange = 50;
-
- [SerializeField]
- [Tooltip("Defines the circular region where the onscreen control may have it's origin placed.")]
- [Min(0)]
- private float m_DynamicOriginRange = 100;
-
- [InputControl(layout = "Vector2")]
- [SerializeField]
- private string m_ControlPath;
-
- [SerializeField]
- [Tooltip("Choose how the onscreen stick will move relative to it's origin and the press position.\n\n" +
- "RelativePositionWithStaticOrigin: The control's center of origin is fixed. " +
- "The control will begin un-actuated at it's centered position and then move relative to the pointer or finger motion.\n\n" +
- "ExactPositionWithStaticOrigin: The control's center of origin is fixed. The stick will immediately jump to the " +
- "exact position of the click or touch and begin tracking motion from there.\n\n" +
- "ExactPositionWithDynamicOrigin: The control's center of origin is determined by the initial press position. " +
- "The stick will begin un-actuated at this center position and then track the current pointer or finger position.")]
- private Behaviour m_Behaviour;
-
- [SerializeField]
- [Tooltip("Set this to true to prevent cancellation of pointer events due to device switching. Cancellation " +
- "will appear as the stick jumping back and forth between the pointer position and the stick center.")]
- private bool m_UseIsolatedInputActions;
-
- [SerializeField]
- [Tooltip("The action that will be used to detect pointer down events on the stick control. Note that if no bindings " +
- "are set, default ones will be provided.")]
- private InputAction m_PointerDownAction;
-
- [SerializeField]
- [Tooltip("The action that will be used to detect pointer movement on the stick control. Note that if no bindings " +
- "are set, default ones will be provided.")]
- private InputAction m_PointerMoveAction;
-
- private Vector3 m_StartPos;
- private Vector2 m_PointerDownPos;
-
- [NonSerialized]
- private List<RaycastResult> m_RaycastResults;
- [NonSerialized]
- 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
- }
-
- #if UNITY_EDITOR
- [CustomEditor(typeof(OnScreenStick))]
- 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();
- }
- }
- #endif
- }
- }
- #endif
|