#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 { /// /// A stick control displayed on screen and moved around by touch or other pointer /// input. /// /// /// The works by simulating events from the device specified in the /// property. Some parts of the Input System, such as the 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 , this can mean that the /// 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 property to true. This will create a set of local /// Input Actions to drive the stick that are not cancelled when device switching occurs. /// [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"; /// /// Callback to handle OnPointerDown UI events. /// public void OnPointerDown(PointerEventData eventData) { if (m_UseIsolatedInputActions) return; if (eventData == null) throw new System.ArgumentNullException(nameof(eventData)); BeginInteraction(eventData.position, eventData.pressEventCamera); } /// /// Callback to handle OnDrag UI events. /// public void OnDrag(PointerEventData eventData) { if (m_UseIsolatedInputActions) return; if (eventData == null) throw new System.ArgumentNullException(nameof(eventData)); MoveStick(eventData.position, eventData.pressEventCamera); } /// /// Callback to handle OnPointerUp UI events. /// 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(); 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("/leftButton"); m_PointerDownAction.AddBinding("/tip"); m_PointerDownAction.AddBinding("/touch*/press"); m_PointerDownAction.AddBinding("/trigger"); } if (m_PointerMoveAction == null || m_PointerMoveAction.bindings.Count == 0) { if (m_PointerMoveAction == null) m_PointerMoveAction = new InputAction(); m_PointerMoveAction.AddBinding("/position"); m_PointerMoveAction.AddBinding("/position"); m_PointerMoveAction.AddBinding("/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.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(); 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(); 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(); 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); } } /// /// The distance from the onscreen control's center of origin, around which the control can move. /// public float movementRange { get => m_MovementRange; set => m_MovementRange = value; } /// /// Defines the circular region where the onscreen control may have it's origin placed. /// /// /// This only applies if is set to . /// 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. /// public float dynamicOriginRange { get => m_DynamicOriginRange; set { // ReSharper disable once CompareOfFloatsByEqualityOperator if (m_DynamicOriginRange != value) { m_DynamicOriginRange = value; UpdateDynamicOriginClickableArea(); } } } /// /// Prevents stick interactions from getting cancelled due to device switching. /// /// /// 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 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. /// 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 m_RaycastResults; [NonSerialized] private PointerEventData m_PointerEventData; protected override string controlPathInternal { get => m_ControlPath; set => m_ControlPath = value; } /// Defines how the onscreen stick will move relative to it's origin and the press position. public Behaviour behaviour { get => m_Behaviour; set => m_Behaviour = value; } /// Defines how the onscreen stick will move relative to it's center of origin and the press position. public enum Behaviour { /// 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. RelativePositionWithStaticOrigin, /// 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. ExactPositionWithStaticOrigin, /// 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. 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