#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