UI_Knob.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. /// Credit Tomasz Schelenz
  2. /// Sourced from - https://bitbucket.org/SimonDarksideJ/unity-ui-extensions/issues/46/feature-uiknob#comment-29243988
  3. using System;
  4. using UnityEngine.Events;
  5. using UnityEngine.EventSystems;
  6. /// <summary>
  7. /// KNOB controller
  8. ///
  9. /// Fields
  10. /// - direction - direction of rotation CW - clockwise CCW - counter clock wise
  11. /// - knobValue - Output value of the control
  12. /// - maxValue - max value knob can rotate to, if higher than loops value or set to 0 - it will be ignored, and max value will be based on loops
  13. /// - loops - how any turns around knob can do
  14. /// - clampOutput01 - if true the output knobValue will be clamped between 0 and 1 regardless of number of loops.
  15. /// - snapToPosition - snap to step. NOTE: max value will override the step.
  16. /// - snapStepsPerLoop - how many snap positions are in one knob loop;
  17. /// - OnValueChanged - event that is called every frame while rotating knob, sends <float> argument of knobValue
  18. /// NOTES
  19. /// - script works only in images rotation on Z axis;
  20. /// - while dragging outside of control, the rotation will be canceled
  21. /// </summary>
  22. ///
  23. namespace UnityEngine.UI.Extensions
  24. {
  25. [RequireComponent(typeof(Image))]
  26. [AddComponentMenu("UI/Extensions/UI_Knob")]
  27. public class UI_Knob : Selectable, IPointerDownHandler, IPointerUpHandler, IPointerEnterHandler, IPointerExitHandler, IDragHandler, IInitializePotentialDragHandler
  28. {
  29. public enum Direction { CW, CCW };
  30. [Tooltip("Direction of rotation CW - clockwise, CCW - counterClockwise")]
  31. public Direction direction = Direction.CW;
  32. [HideInInspector]
  33. public float KnobValue;
  34. [Tooltip("Max value of the knob, maximum RAW output value knob can reach, overrides snap step, IF set to 0 or higher than loops, max value will be set by loops")]
  35. public float MaxValue = 0;
  36. [Tooltip("How many rotations knob can do, if higher than max value, the latter will limit max value")]
  37. public int Loops = 0;
  38. [Tooltip("Clamp output value between 0 and 1, useful with loops > 1")]
  39. public bool ClampOutput01 = false;
  40. [Tooltip("snap to position?")]
  41. public bool SnapToPosition = false;
  42. [Tooltip("Number of positions to snap")]
  43. public int SnapStepsPerLoop = 10;
  44. [Tooltip("Parent touch area to extend the touch radius")]
  45. public RectTransform ParentTouchMask;
  46. [Tooltip("Default background color of the touch mask. Defaults as transparent")]
  47. public Color MaskBackground = new Color(0, 0, 0, 0);
  48. [Space(30)]
  49. public KnobFloatValueEvent OnValueChanged;
  50. private float _currentLoops = 0;
  51. private float _previousValue = 0;
  52. private float _initAngle;
  53. private float _currentAngle;
  54. private Vector2 _currentVector;
  55. private Quaternion _initRotation;
  56. private bool _canDrag = false;
  57. private bool _screenSpaceOverlay;
  58. protected override void Awake()
  59. {
  60. _screenSpaceOverlay = GetComponentInParent<Canvas>().rootCanvas.renderMode == RenderMode.ScreenSpaceOverlay;
  61. }
  62. protected override void Start()
  63. {
  64. CheckForParentTouchMask();
  65. }
  66. private void CheckForParentTouchMask()
  67. {
  68. if (ParentTouchMask)
  69. {
  70. Image maskImage = ParentTouchMask.gameObject.GetOrAddComponent<Image>();
  71. maskImage.color = MaskBackground;
  72. EventTrigger trigger = ParentTouchMask.gameObject.GetOrAddComponent<EventTrigger>();
  73. trigger.triggers.Clear();
  74. //PointerDownEvent
  75. EventTrigger.Entry pointerDownEntry = new EventTrigger.Entry();
  76. pointerDownEntry.eventID = EventTriggerType.PointerDown;
  77. pointerDownEntry.callback.AddListener((data) => { OnPointerDown((PointerEventData)data); });
  78. trigger.triggers.Add(pointerDownEntry);
  79. //PointerUpEvent
  80. EventTrigger.Entry pointerUpEntry = new EventTrigger.Entry();
  81. pointerUpEntry.eventID = EventTriggerType.PointerUp;
  82. pointerUpEntry.callback.AddListener((data) => { OnPointerUp((PointerEventData)data); });
  83. trigger.triggers.Add(pointerUpEntry);
  84. //PointerEnterEvent
  85. EventTrigger.Entry pointerEnterEntry = new EventTrigger.Entry();
  86. pointerEnterEntry.eventID = EventTriggerType.PointerEnter;
  87. pointerEnterEntry.callback.AddListener((data) => { OnPointerEnter((PointerEventData)data); });
  88. trigger.triggers.Add(pointerEnterEntry);
  89. //PointerExitEvent
  90. EventTrigger.Entry pointerExitEntry = new EventTrigger.Entry();
  91. pointerExitEntry.eventID = EventTriggerType.PointerExit;
  92. pointerExitEntry.callback.AddListener((data) => { OnPointerExit((PointerEventData)data); });
  93. trigger.triggers.Add(pointerExitEntry);
  94. //DragEvent
  95. EventTrigger.Entry dragEntry = new EventTrigger.Entry();
  96. dragEntry.eventID = EventTriggerType.Drag;
  97. dragEntry.callback.AddListener((data) => { OnDrag((PointerEventData)data); });
  98. trigger.triggers.Add(dragEntry);
  99. }
  100. }
  101. public override void OnPointerUp(PointerEventData eventData)
  102. {
  103. _canDrag = false;
  104. }
  105. public override void OnPointerEnter(PointerEventData eventData)
  106. {
  107. _canDrag = true;
  108. }
  109. public override void OnPointerExit(PointerEventData eventData)
  110. {
  111. _canDrag = false;
  112. }
  113. public override void OnPointerDown(PointerEventData eventData)
  114. {
  115. _canDrag = true;
  116. base.OnPointerDown(eventData);
  117. _initRotation = transform.rotation;
  118. if (_screenSpaceOverlay)
  119. {
  120. _currentVector = eventData.position - (Vector2)transform.position;
  121. }
  122. else
  123. {
  124. _currentVector = eventData.position - (Vector2)Camera.main.WorldToScreenPoint(transform.position);
  125. }
  126. _initAngle = Mathf.Atan2(_currentVector.y, _currentVector.x) * Mathf.Rad2Deg;
  127. }
  128. public void OnDrag(PointerEventData eventData)
  129. {
  130. //CHECK IF CAN DRAG
  131. if (!_canDrag)
  132. {
  133. return;
  134. }
  135. if (_screenSpaceOverlay)
  136. {
  137. _currentVector = eventData.position - (Vector2)transform.position;
  138. }
  139. else
  140. {
  141. _currentVector = eventData.position - (Vector2)Camera.main.WorldToScreenPoint(transform.position);
  142. }
  143. _currentAngle = Mathf.Atan2(_currentVector.y, _currentVector.x) * Mathf.Rad2Deg;
  144. Quaternion addRotation = Quaternion.AngleAxis(_currentAngle - _initAngle, this.transform.forward);
  145. addRotation.eulerAngles = new Vector3(0, 0, addRotation.eulerAngles.z);
  146. Quaternion finalRotation = _initRotation * addRotation;
  147. if (direction == Direction.CW)
  148. {
  149. KnobValue = 1 - (finalRotation.eulerAngles.z / 360f);
  150. if (SnapToPosition)
  151. {
  152. SnapToPositionValue(ref KnobValue);
  153. finalRotation.eulerAngles = new Vector3(0, 0, 360 - 360 * KnobValue);
  154. }
  155. }
  156. else
  157. {
  158. KnobValue = (finalRotation.eulerAngles.z / 360f);
  159. if (SnapToPosition)
  160. {
  161. SnapToPositionValue(ref KnobValue);
  162. finalRotation.eulerAngles = new Vector3(0, 0, 360 * KnobValue);
  163. }
  164. }
  165. UpdateKnobValue();
  166. transform.rotation = finalRotation;
  167. InvokeEvents(KnobValue + _currentLoops);
  168. _previousValue = KnobValue;
  169. }
  170. private void UpdateKnobValue()
  171. {
  172. //PREVENT OVERROTATION
  173. if (Mathf.Abs(KnobValue - _previousValue) > 0.5f)
  174. {
  175. if (KnobValue < 0.5f && Loops > 1 && _currentLoops < Loops - 1)
  176. {
  177. _currentLoops++;
  178. }
  179. else if (KnobValue > 0.5f && _currentLoops >= 1)
  180. {
  181. _currentLoops--;
  182. }
  183. else
  184. {
  185. if (KnobValue > 0.5f && _currentLoops == 0)
  186. {
  187. KnobValue = 0;
  188. transform.localEulerAngles = Vector3.zero;
  189. InvokeEvents(KnobValue + _currentLoops);
  190. return;
  191. }
  192. else if (KnobValue < 0.5f && _currentLoops == Loops - 1)
  193. {
  194. KnobValue = 1;
  195. transform.localEulerAngles = Vector3.zero;
  196. InvokeEvents(KnobValue + _currentLoops);
  197. return;
  198. }
  199. }
  200. }
  201. //CHECK MAX VALUE
  202. if (MaxValue > 0)
  203. {
  204. if (KnobValue + _currentLoops > MaxValue)
  205. {
  206. KnobValue = MaxValue;
  207. float maxAngle = direction == Direction.CW ? 360f - 360f * MaxValue : 360f * MaxValue;
  208. transform.localEulerAngles = new Vector3(0, 0, maxAngle);
  209. InvokeEvents(KnobValue);
  210. return;
  211. }
  212. }
  213. }
  214. public void SetKnobValue(float value, int loops = 0)
  215. {
  216. Quaternion newRoation = Quaternion.identity;
  217. KnobValue = value;
  218. _currentLoops = loops;
  219. if (SnapToPosition)
  220. {
  221. SnapToPositionValue(ref KnobValue);
  222. }
  223. if (direction == Direction.CW)
  224. {
  225. newRoation.eulerAngles = new Vector3(0, 0, 360 - 360 * KnobValue);
  226. }
  227. else
  228. {
  229. newRoation.eulerAngles = new Vector3(0, 0, 360 * KnobValue);
  230. }
  231. UpdateKnobValue();
  232. transform.rotation = newRoation;
  233. InvokeEvents(KnobValue + _currentLoops);
  234. _previousValue = KnobValue;
  235. }
  236. private void SnapToPositionValue(ref float knobValue)
  237. {
  238. float snapStep = 1 / (float)SnapStepsPerLoop;
  239. float newValue = Mathf.Round(knobValue / snapStep) * snapStep;
  240. knobValue = newValue;
  241. }
  242. private void InvokeEvents(float value)
  243. {
  244. if (ClampOutput01)
  245. value /= Loops;
  246. OnValueChanged.Invoke(value);
  247. }
  248. public virtual void OnInitializePotentialDrag(PointerEventData eventData)
  249. {
  250. eventData.useDragThreshold = false;
  251. }
  252. }
  253. [System.Serializable]
  254. public class KnobFloatValueEvent : UnityEvent<float> { }
  255. }