Нема описа
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496
  1. #if PACKAGE_DOCS_GENERATION || UNITY_INPUT_SYSTEM_ENABLE_UI
  2. using System;
  3. using System.Collections.Generic;
  4. using UnityEngine.EventSystems;
  5. using UnityEngine.Serialization;
  6. using UnityEngine.InputSystem.Layouts;
  7. using UnityEngine.InputSystem.Utilities;
  8. using UnityEngine.UI;
  9. #if UNITY_EDITOR
  10. using UnityEditor;
  11. using UnityEditor.AnimatedValues;
  12. #endif
  13. ////TODO: custom icon for OnScreenStick component
  14. namespace UnityEngine.InputSystem.OnScreen
  15. {
  16. /// <summary>
  17. /// A stick control displayed on screen and moved around by touch or other pointer
  18. /// input.
  19. /// </summary>
  20. /// <remarks>
  21. /// The <see cref="OnScreenStick"/> works by simulating events from the device specified in the <see cref="OnScreenControl.controlPath"/>
  22. /// property. Some parts of the Input System, such as the <see cref="PlayerInput"/> component, can be set up to
  23. /// auto-switch to a new device when input from them is detected. When a device is switched, any currently running
  24. /// inputs from the previously active device are cancelled. In the case of <see cref="OnScreenStick"/>, this can mean that the
  25. /// <see cref="IPointerUpHandler.OnPointerUp"/> method will be called and the stick will jump back to center, even though
  26. /// the pointer input has not physically been released.
  27. ///
  28. /// To avoid this situation, set the <see cref="useIsolatedInputActions"/> property to true. This will create a set of local
  29. /// Input Actions to drive the stick that are not cancelled when device switching occurs.
  30. /// </remarks>
  31. [AddComponentMenu("Input/On-Screen Stick")]
  32. [HelpURL(InputSystem.kDocUrl + "/manual/OnScreen.html#on-screen-sticks")]
  33. public class OnScreenStick : OnScreenControl, IPointerDownHandler, IPointerUpHandler, IDragHandler
  34. {
  35. private const string kDynamicOriginClickable = "DynamicOriginClickable";
  36. /// <summary>
  37. /// Callback to handle OnPointerDown UI events.
  38. /// </summary>
  39. public void OnPointerDown(PointerEventData eventData)
  40. {
  41. if (m_UseIsolatedInputActions)
  42. return;
  43. if (eventData == null)
  44. throw new System.ArgumentNullException(nameof(eventData));
  45. BeginInteraction(eventData.position, eventData.pressEventCamera);
  46. }
  47. /// <summary>
  48. /// Callback to handle OnDrag UI events.
  49. /// </summary>
  50. public void OnDrag(PointerEventData eventData)
  51. {
  52. if (m_UseIsolatedInputActions)
  53. return;
  54. if (eventData == null)
  55. throw new System.ArgumentNullException(nameof(eventData));
  56. MoveStick(eventData.position, eventData.pressEventCamera);
  57. }
  58. /// <summary>
  59. /// Callback to handle OnPointerUp UI events.
  60. /// </summary>
  61. public void OnPointerUp(PointerEventData eventData)
  62. {
  63. if (m_UseIsolatedInputActions)
  64. return;
  65. EndInteraction();
  66. }
  67. private void Start()
  68. {
  69. if (m_UseIsolatedInputActions)
  70. {
  71. // avoid allocations every time the pointer down event fires by allocating these here
  72. // and re-using them
  73. m_RaycastResults = new List<RaycastResult>();
  74. m_PointerEventData = new PointerEventData(EventSystem.current);
  75. // if the pointer actions have no bindings (the default), add some
  76. if (m_PointerDownAction == null || m_PointerDownAction.bindings.Count == 0)
  77. {
  78. if (m_PointerDownAction == null)
  79. m_PointerDownAction = new InputAction();
  80. m_PointerDownAction.AddBinding("<Mouse>/leftButton");
  81. m_PointerDownAction.AddBinding("<Pen>/tip");
  82. m_PointerDownAction.AddBinding("<Touchscreen>/touch*/press");
  83. m_PointerDownAction.AddBinding("<XRController>/trigger");
  84. }
  85. if (m_PointerMoveAction == null || m_PointerMoveAction.bindings.Count == 0)
  86. {
  87. if (m_PointerMoveAction == null)
  88. m_PointerMoveAction = new InputAction();
  89. m_PointerMoveAction.AddBinding("<Mouse>/position");
  90. m_PointerMoveAction.AddBinding("<Pen>/position");
  91. m_PointerMoveAction.AddBinding("<Touchscreen>/touch*/position");
  92. }
  93. m_PointerDownAction.started += OnPointerDown;
  94. m_PointerDownAction.canceled += OnPointerUp;
  95. m_PointerDownAction.Enable();
  96. m_PointerMoveAction.Enable();
  97. }
  98. m_StartPos = ((RectTransform)transform).anchoredPosition;
  99. if (m_Behaviour != Behaviour.ExactPositionWithDynamicOrigin) return;
  100. m_PointerDownPos = m_StartPos;
  101. var dynamicOrigin = new GameObject(kDynamicOriginClickable, typeof(Image));
  102. dynamicOrigin.transform.SetParent(transform);
  103. var image = dynamicOrigin.GetComponent<Image>();
  104. image.color = new Color(1, 1, 1, 0);
  105. var rectTransform = (RectTransform)dynamicOrigin.transform;
  106. rectTransform.sizeDelta = new Vector2(m_DynamicOriginRange * 2, m_DynamicOriginRange * 2);
  107. rectTransform.localScale = new Vector3(1, 1, 0);
  108. rectTransform.anchoredPosition3D = Vector3.zero;
  109. image.sprite = SpriteUtilities.CreateCircleSprite(16, new Color32(255, 255, 255, 255));
  110. image.alphaHitTestMinimumThreshold = 0.5f;
  111. }
  112. private void BeginInteraction(Vector2 pointerPosition, Camera uiCamera)
  113. {
  114. var canvasRect = transform.parent?.GetComponentInParent<RectTransform>();
  115. if (canvasRect == null)
  116. {
  117. Debug.LogError("OnScreenStick needs to be attached as a child to a UI Canvas to function properly.");
  118. return;
  119. }
  120. switch (m_Behaviour)
  121. {
  122. case Behaviour.RelativePositionWithStaticOrigin:
  123. RectTransformUtility.ScreenPointToLocalPointInRectangle(canvasRect, pointerPosition, uiCamera, out m_PointerDownPos);
  124. break;
  125. case Behaviour.ExactPositionWithStaticOrigin:
  126. RectTransformUtility.ScreenPointToLocalPointInRectangle(canvasRect, pointerPosition, uiCamera, out m_PointerDownPos);
  127. MoveStick(pointerPosition, uiCamera);
  128. break;
  129. case Behaviour.ExactPositionWithDynamicOrigin:
  130. RectTransformUtility.ScreenPointToLocalPointInRectangle(canvasRect, pointerPosition, uiCamera, out var pointerDown);
  131. m_PointerDownPos = ((RectTransform)transform).anchoredPosition = pointerDown;
  132. break;
  133. }
  134. }
  135. private void MoveStick(Vector2 pointerPosition, Camera uiCamera)
  136. {
  137. var canvasRect = transform.parent?.GetComponentInParent<RectTransform>();
  138. if (canvasRect == null)
  139. {
  140. Debug.LogError("OnScreenStick needs to be attached as a child to a UI Canvas to function properly.");
  141. return;
  142. }
  143. RectTransformUtility.ScreenPointToLocalPointInRectangle(canvasRect, pointerPosition, uiCamera, out var position);
  144. var delta = position - m_PointerDownPos;
  145. switch (m_Behaviour)
  146. {
  147. case Behaviour.RelativePositionWithStaticOrigin:
  148. delta = Vector2.ClampMagnitude(delta, movementRange);
  149. ((RectTransform)transform).anchoredPosition = (Vector2)m_StartPos + delta;
  150. break;
  151. case Behaviour.ExactPositionWithStaticOrigin:
  152. delta = position - (Vector2)m_StartPos;
  153. delta = Vector2.ClampMagnitude(delta, movementRange);
  154. ((RectTransform)transform).anchoredPosition = (Vector2)m_StartPos + delta;
  155. break;
  156. case Behaviour.ExactPositionWithDynamicOrigin:
  157. delta = Vector2.ClampMagnitude(delta, movementRange);
  158. ((RectTransform)transform).anchoredPosition = m_PointerDownPos + delta;
  159. break;
  160. }
  161. var newPos = new Vector2(delta.x / movementRange, delta.y / movementRange);
  162. SendValueToControl(newPos);
  163. }
  164. private void EndInteraction()
  165. {
  166. ((RectTransform)transform).anchoredPosition = m_PointerDownPos = m_StartPos;
  167. SendValueToControl(Vector2.zero);
  168. }
  169. private void OnPointerDown(InputAction.CallbackContext ctx)
  170. {
  171. Debug.Assert(EventSystem.current != null);
  172. var screenPosition = Vector2.zero;
  173. if (ctx.control?.device is Pointer pointer)
  174. screenPosition = pointer.position.ReadValue();
  175. m_PointerEventData.position = screenPosition;
  176. EventSystem.current.RaycastAll(m_PointerEventData, m_RaycastResults);
  177. if (m_RaycastResults.Count == 0)
  178. return;
  179. var stickSelected = false;
  180. foreach (var result in m_RaycastResults)
  181. {
  182. if (result.gameObject != gameObject) continue;
  183. stickSelected = true;
  184. break;
  185. }
  186. if (!stickSelected)
  187. return;
  188. BeginInteraction(screenPosition, GetCameraFromCanvas());
  189. m_PointerMoveAction.performed += OnPointerMove;
  190. }
  191. private void OnPointerMove(InputAction.CallbackContext ctx)
  192. {
  193. // only pointer devices are allowed
  194. Debug.Assert(ctx.control?.device is Pointer);
  195. var screenPosition = ((Pointer)ctx.control.device).position.ReadValue();
  196. MoveStick(screenPosition, GetCameraFromCanvas());
  197. }
  198. private void OnPointerUp(InputAction.CallbackContext ctx)
  199. {
  200. EndInteraction();
  201. m_PointerMoveAction.performed -= OnPointerMove;
  202. }
  203. private Camera GetCameraFromCanvas()
  204. {
  205. var canvas = GetComponentInParent<Canvas>();
  206. var renderMode = canvas?.renderMode;
  207. if (renderMode == RenderMode.ScreenSpaceOverlay
  208. || (renderMode == RenderMode.ScreenSpaceCamera && canvas?.worldCamera == null))
  209. return null;
  210. return canvas?.worldCamera ?? Camera.main;
  211. }
  212. private void OnDrawGizmosSelected()
  213. {
  214. Gizmos.matrix = ((RectTransform)transform.parent).localToWorldMatrix;
  215. var startPos = ((RectTransform)transform).anchoredPosition;
  216. if (Application.isPlaying)
  217. startPos = m_StartPos;
  218. Gizmos.color = new Color32(84, 173, 219, 255);
  219. var center = startPos;
  220. if (Application.isPlaying && m_Behaviour == Behaviour.ExactPositionWithDynamicOrigin)
  221. center = m_PointerDownPos;
  222. DrawGizmoCircle(center, m_MovementRange);
  223. if (m_Behaviour != Behaviour.ExactPositionWithDynamicOrigin) return;
  224. Gizmos.color = new Color32(158, 84, 219, 255);
  225. DrawGizmoCircle(startPos, m_DynamicOriginRange);
  226. }
  227. private void DrawGizmoCircle(Vector2 center, float radius)
  228. {
  229. for (var i = 0; i < 32; i++)
  230. {
  231. var radians = i / 32f * Mathf.PI * 2;
  232. var nextRadian = (i + 1) / 32f * Mathf.PI * 2;
  233. Gizmos.DrawLine(
  234. new Vector3(center.x + Mathf.Cos(radians) * radius, center.y + Mathf.Sin(radians) * radius, 0),
  235. new Vector3(center.x + Mathf.Cos(nextRadian) * radius, center.y + Mathf.Sin(nextRadian) * radius, 0));
  236. }
  237. }
  238. private void UpdateDynamicOriginClickableArea()
  239. {
  240. var dynamicOriginTransform = transform.Find(kDynamicOriginClickable);
  241. if (dynamicOriginTransform)
  242. {
  243. var rectTransform = (RectTransform)dynamicOriginTransform;
  244. rectTransform.sizeDelta = new Vector2(m_DynamicOriginRange * 2, m_DynamicOriginRange * 2);
  245. }
  246. }
  247. /// <summary>
  248. /// The distance from the onscreen control's center of origin, around which the control can move.
  249. /// </summary>
  250. public float movementRange
  251. {
  252. get => m_MovementRange;
  253. set => m_MovementRange = value;
  254. }
  255. /// <summary>
  256. /// Defines the circular region where the onscreen control may have it's origin placed.
  257. /// </summary>
  258. /// <remarks>
  259. /// This only applies if <see cref="behaviour"/> is set to <see cref="Behaviour.ExactPositionWithDynamicOrigin"/>.
  260. /// 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.
  261. /// Otherwise, if pressed outside of this region the control will ignore it.
  262. /// This property defines the radius of the circular region. The center point being defined by the component position in the scene.
  263. /// </remarks>
  264. public float dynamicOriginRange
  265. {
  266. get => m_DynamicOriginRange;
  267. set
  268. {
  269. // ReSharper disable once CompareOfFloatsByEqualityOperator
  270. if (m_DynamicOriginRange != value)
  271. {
  272. m_DynamicOriginRange = value;
  273. UpdateDynamicOriginClickableArea();
  274. }
  275. }
  276. }
  277. /// <summary>
  278. /// Prevents stick interactions from getting cancelled due to device switching.
  279. /// </summary>
  280. /// <remarks>
  281. /// This property is useful for scenarios where the active device switches automatically
  282. /// based on the most recently actuated device. A common situation where this happens is
  283. /// when using a <see cref="PlayerInput"/> component with Auto-switch set to true. Imagine
  284. /// a mobile game where an on-screen stick simulates the left stick of a gamepad device.
  285. /// When the on-screen stick is moved, the Input System will see an input event from a gamepad
  286. /// and switch the active device to it. This causes any active actions to be cancelled, including
  287. /// the pointer action driving the on screen stick, which results in the stick jumping back to
  288. /// the center as though it had been released.
  289. ///
  290. /// In isolated mode, the actions driving the stick are not cancelled because they are
  291. /// unique Input Action instances that don't share state with any others.
  292. /// </remarks>
  293. public bool useIsolatedInputActions
  294. {
  295. get => m_UseIsolatedInputActions;
  296. set => m_UseIsolatedInputActions = value;
  297. }
  298. [FormerlySerializedAs("movementRange")]
  299. [SerializeField]
  300. [Min(0)]
  301. private float m_MovementRange = 50;
  302. [SerializeField]
  303. [Tooltip("Defines the circular region where the onscreen control may have it's origin placed.")]
  304. [Min(0)]
  305. private float m_DynamicOriginRange = 100;
  306. [InputControl(layout = "Vector2")]
  307. [SerializeField]
  308. private string m_ControlPath;
  309. [SerializeField]
  310. [Tooltip("Choose how the onscreen stick will move relative to it's origin and the press position.\n\n" +
  311. "RelativePositionWithStaticOrigin: The control's center of origin is fixed. " +
  312. "The control will begin un-actuated at it's centered position and then move relative to the pointer or finger motion.\n\n" +
  313. "ExactPositionWithStaticOrigin: The control's center of origin is fixed. The stick will immediately jump to the " +
  314. "exact position of the click or touch and begin tracking motion from there.\n\n" +
  315. "ExactPositionWithDynamicOrigin: The control's center of origin is determined by the initial press position. " +
  316. "The stick will begin un-actuated at this center position and then track the current pointer or finger position.")]
  317. private Behaviour m_Behaviour;
  318. [SerializeField]
  319. [Tooltip("Set this to true to prevent cancellation of pointer events due to device switching. Cancellation " +
  320. "will appear as the stick jumping back and forth between the pointer position and the stick center.")]
  321. private bool m_UseIsolatedInputActions;
  322. [SerializeField]
  323. [Tooltip("The action that will be used to detect pointer down events on the stick control. Note that if no bindings " +
  324. "are set, default ones will be provided.")]
  325. private InputAction m_PointerDownAction;
  326. [SerializeField]
  327. [Tooltip("The action that will be used to detect pointer movement on the stick control. Note that if no bindings " +
  328. "are set, default ones will be provided.")]
  329. private InputAction m_PointerMoveAction;
  330. private Vector3 m_StartPos;
  331. private Vector2 m_PointerDownPos;
  332. [NonSerialized]
  333. private List<RaycastResult> m_RaycastResults;
  334. [NonSerialized]
  335. private PointerEventData m_PointerEventData;
  336. protected override string controlPathInternal
  337. {
  338. get => m_ControlPath;
  339. set => m_ControlPath = value;
  340. }
  341. /// <summary>Defines how the onscreen stick will move relative to it's origin and the press position.</summary>
  342. public Behaviour behaviour
  343. {
  344. get => m_Behaviour;
  345. set => m_Behaviour = value;
  346. }
  347. /// <summary>Defines how the onscreen stick will move relative to it's center of origin and the press position.</summary>
  348. public enum Behaviour
  349. {
  350. /// <summary>The control's center of origin is fixed in the scene.
  351. /// The control will begin un-actuated at it's centered position and then move relative to the press motion.</summary>
  352. RelativePositionWithStaticOrigin,
  353. /// <summary>The control's center of origin is fixed in the scene.
  354. /// The control may begin from an actuated position to ensure it is always tracking the current press position.</summary>
  355. ExactPositionWithStaticOrigin,
  356. /// <summary>The control's center of origin is determined by the initial press position.
  357. /// The control will begin unactuated at this center position and then track the current press position.</summary>
  358. ExactPositionWithDynamicOrigin
  359. }
  360. #if UNITY_EDITOR
  361. [CustomEditor(typeof(OnScreenStick))]
  362. internal class OnScreenStickEditor : UnityEditor.Editor
  363. {
  364. private AnimBool m_ShowDynamicOriginOptions;
  365. private AnimBool m_ShowIsolatedInputActions;
  366. private SerializedProperty m_UseIsolatedInputActions;
  367. private SerializedProperty m_Behaviour;
  368. private SerializedProperty m_ControlPathInternal;
  369. private SerializedProperty m_MovementRange;
  370. private SerializedProperty m_DynamicOriginRange;
  371. private SerializedProperty m_PointerDownAction;
  372. private SerializedProperty m_PointerMoveAction;
  373. public void OnEnable()
  374. {
  375. m_ShowDynamicOriginOptions = new AnimBool(false);
  376. m_ShowIsolatedInputActions = new AnimBool(false);
  377. m_UseIsolatedInputActions = serializedObject.FindProperty(nameof(OnScreenStick.m_UseIsolatedInputActions));
  378. m_Behaviour = serializedObject.FindProperty(nameof(OnScreenStick.m_Behaviour));
  379. m_ControlPathInternal = serializedObject.FindProperty(nameof(OnScreenStick.m_ControlPath));
  380. m_MovementRange = serializedObject.FindProperty(nameof(OnScreenStick.m_MovementRange));
  381. m_DynamicOriginRange = serializedObject.FindProperty(nameof(OnScreenStick.m_DynamicOriginRange));
  382. m_PointerDownAction = serializedObject.FindProperty(nameof(OnScreenStick.m_PointerDownAction));
  383. m_PointerMoveAction = serializedObject.FindProperty(nameof(OnScreenStick.m_PointerMoveAction));
  384. }
  385. public override void OnInspectorGUI()
  386. {
  387. EditorGUILayout.PropertyField(m_MovementRange);
  388. EditorGUILayout.PropertyField(m_ControlPathInternal);
  389. EditorGUILayout.PropertyField(m_Behaviour);
  390. m_ShowDynamicOriginOptions.target = ((OnScreenStick)target).behaviour ==
  391. Behaviour.ExactPositionWithDynamicOrigin;
  392. if (EditorGUILayout.BeginFadeGroup(m_ShowDynamicOriginOptions.faded))
  393. {
  394. EditorGUI.indentLevel++;
  395. EditorGUI.BeginChangeCheck();
  396. EditorGUILayout.PropertyField(m_DynamicOriginRange);
  397. if (EditorGUI.EndChangeCheck())
  398. {
  399. ((OnScreenStick)target).UpdateDynamicOriginClickableArea();
  400. }
  401. EditorGUI.indentLevel--;
  402. }
  403. EditorGUILayout.EndFadeGroup();
  404. EditorGUILayout.PropertyField(m_UseIsolatedInputActions);
  405. m_ShowIsolatedInputActions.target = m_UseIsolatedInputActions.boolValue;
  406. if (EditorGUILayout.BeginFadeGroup(m_ShowIsolatedInputActions.faded))
  407. {
  408. EditorGUI.indentLevel++;
  409. EditorGUILayout.PropertyField(m_PointerDownAction);
  410. EditorGUILayout.PropertyField(m_PointerMoveAction);
  411. EditorGUI.indentLevel--;
  412. }
  413. EditorGUILayout.EndFadeGroup();
  414. serializedObject.ApplyModifiedProperties();
  415. }
  416. }
  417. #endif
  418. }
  419. }
  420. #endif