/// Credit Tomasz Schelenz /// Sourced from - https://bitbucket.org/SimonDarksideJ/unity-ui-extensions/issues/46/feature-uiknob#comment-29243988 using System; using UnityEngine.Events; using UnityEngine.EventSystems; /// /// KNOB controller /// /// Fields /// - direction - direction of rotation CW - clockwise CCW - counter clock wise /// - knobValue - Output value of the control /// - 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 /// - loops - how any turns around knob can do /// - clampOutput01 - if true the output knobValue will be clamped between 0 and 1 regardless of number of loops. /// - snapToPosition - snap to step. NOTE: max value will override the step. /// - snapStepsPerLoop - how many snap positions are in one knob loop; /// - OnValueChanged - event that is called every frame while rotating knob, sends argument of knobValue /// NOTES /// - script works only in images rotation on Z axis; /// - while dragging outside of control, the rotation will be canceled /// /// namespace UnityEngine.UI.Extensions { [RequireComponent(typeof(Image))] [AddComponentMenu("UI/Extensions/UI_Knob")] public class UI_Knob : Selectable, IPointerDownHandler, IPointerUpHandler, IPointerEnterHandler, IPointerExitHandler, IDragHandler, IInitializePotentialDragHandler { public enum Direction { CW, CCW }; [Tooltip("Direction of rotation CW - clockwise, CCW - counterClockwise")] public Direction direction = Direction.CW; [HideInInspector] public float KnobValue; [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")] public float MaxValue = 0; [Tooltip("How many rotations knob can do, if higher than max value, the latter will limit max value")] public int Loops = 0; [Tooltip("Clamp output value between 0 and 1, useful with loops > 1")] public bool ClampOutput01 = false; [Tooltip("snap to position?")] public bool SnapToPosition = false; [Tooltip("Number of positions to snap")] public int SnapStepsPerLoop = 10; [Tooltip("Parent touch area to extend the touch radius")] public RectTransform ParentTouchMask; [Tooltip("Default background color of the touch mask. Defaults as transparent")] public Color MaskBackground = new Color(0, 0, 0, 0); [Space(30)] public KnobFloatValueEvent OnValueChanged; private float _currentLoops = 0; private float _previousValue = 0; private float _initAngle; private float _currentAngle; private Vector2 _currentVector; private Quaternion _initRotation; private bool _canDrag = false; private bool _screenSpaceOverlay; protected override void Awake() { _screenSpaceOverlay = GetComponentInParent().rootCanvas.renderMode == RenderMode.ScreenSpaceOverlay; } protected override void Start() { CheckForParentTouchMask(); } private void CheckForParentTouchMask() { if (ParentTouchMask) { Image maskImage = ParentTouchMask.gameObject.GetOrAddComponent(); maskImage.color = MaskBackground; EventTrigger trigger = ParentTouchMask.gameObject.GetOrAddComponent(); trigger.triggers.Clear(); //PointerDownEvent EventTrigger.Entry pointerDownEntry = new EventTrigger.Entry(); pointerDownEntry.eventID = EventTriggerType.PointerDown; pointerDownEntry.callback.AddListener((data) => { OnPointerDown((PointerEventData)data); }); trigger.triggers.Add(pointerDownEntry); //PointerUpEvent EventTrigger.Entry pointerUpEntry = new EventTrigger.Entry(); pointerUpEntry.eventID = EventTriggerType.PointerUp; pointerUpEntry.callback.AddListener((data) => { OnPointerUp((PointerEventData)data); }); trigger.triggers.Add(pointerUpEntry); //PointerEnterEvent EventTrigger.Entry pointerEnterEntry = new EventTrigger.Entry(); pointerEnterEntry.eventID = EventTriggerType.PointerEnter; pointerEnterEntry.callback.AddListener((data) => { OnPointerEnter((PointerEventData)data); }); trigger.triggers.Add(pointerEnterEntry); //PointerExitEvent EventTrigger.Entry pointerExitEntry = new EventTrigger.Entry(); pointerExitEntry.eventID = EventTriggerType.PointerExit; pointerExitEntry.callback.AddListener((data) => { OnPointerExit((PointerEventData)data); }); trigger.triggers.Add(pointerExitEntry); //DragEvent EventTrigger.Entry dragEntry = new EventTrigger.Entry(); dragEntry.eventID = EventTriggerType.Drag; dragEntry.callback.AddListener((data) => { OnDrag((PointerEventData)data); }); trigger.triggers.Add(dragEntry); } } public override void OnPointerUp(PointerEventData eventData) { _canDrag = false; } public override void OnPointerEnter(PointerEventData eventData) { _canDrag = true; } public override void OnPointerExit(PointerEventData eventData) { _canDrag = false; } public override void OnPointerDown(PointerEventData eventData) { _canDrag = true; base.OnPointerDown(eventData); _initRotation = transform.rotation; if (_screenSpaceOverlay) { _currentVector = eventData.position - (Vector2)transform.position; } else { _currentVector = eventData.position - (Vector2)Camera.main.WorldToScreenPoint(transform.position); } _initAngle = Mathf.Atan2(_currentVector.y, _currentVector.x) * Mathf.Rad2Deg; } public void OnDrag(PointerEventData eventData) { //CHECK IF CAN DRAG if (!_canDrag) { return; } if (_screenSpaceOverlay) { _currentVector = eventData.position - (Vector2)transform.position; } else { _currentVector = eventData.position - (Vector2)Camera.main.WorldToScreenPoint(transform.position); } _currentAngle = Mathf.Atan2(_currentVector.y, _currentVector.x) * Mathf.Rad2Deg; Quaternion addRotation = Quaternion.AngleAxis(_currentAngle - _initAngle, this.transform.forward); addRotation.eulerAngles = new Vector3(0, 0, addRotation.eulerAngles.z); Quaternion finalRotation = _initRotation * addRotation; if (direction == Direction.CW) { KnobValue = 1 - (finalRotation.eulerAngles.z / 360f); if (SnapToPosition) { SnapToPositionValue(ref KnobValue); finalRotation.eulerAngles = new Vector3(0, 0, 360 - 360 * KnobValue); } } else { KnobValue = (finalRotation.eulerAngles.z / 360f); if (SnapToPosition) { SnapToPositionValue(ref KnobValue); finalRotation.eulerAngles = new Vector3(0, 0, 360 * KnobValue); } } UpdateKnobValue(); transform.rotation = finalRotation; InvokeEvents(KnobValue + _currentLoops); _previousValue = KnobValue; } private void UpdateKnobValue() { //PREVENT OVERROTATION if (Mathf.Abs(KnobValue - _previousValue) > 0.5f) { if (KnobValue < 0.5f && Loops > 1 && _currentLoops < Loops - 1) { _currentLoops++; } else if (KnobValue > 0.5f && _currentLoops >= 1) { _currentLoops--; } else { if (KnobValue > 0.5f && _currentLoops == 0) { KnobValue = 0; transform.localEulerAngles = Vector3.zero; InvokeEvents(KnobValue + _currentLoops); return; } else if (KnobValue < 0.5f && _currentLoops == Loops - 1) { KnobValue = 1; transform.localEulerAngles = Vector3.zero; InvokeEvents(KnobValue + _currentLoops); return; } } } //CHECK MAX VALUE if (MaxValue > 0) { if (KnobValue + _currentLoops > MaxValue) { KnobValue = MaxValue; float maxAngle = direction == Direction.CW ? 360f - 360f * MaxValue : 360f * MaxValue; transform.localEulerAngles = new Vector3(0, 0, maxAngle); InvokeEvents(KnobValue); return; } } } public void SetKnobValue(float value, int loops = 0) { Quaternion newRoation = Quaternion.identity; KnobValue = value; _currentLoops = loops; if (SnapToPosition) { SnapToPositionValue(ref KnobValue); } if (direction == Direction.CW) { newRoation.eulerAngles = new Vector3(0, 0, 360 - 360 * KnobValue); } else { newRoation.eulerAngles = new Vector3(0, 0, 360 * KnobValue); } UpdateKnobValue(); transform.rotation = newRoation; InvokeEvents(KnobValue + _currentLoops); _previousValue = KnobValue; } private void SnapToPositionValue(ref float knobValue) { float snapStep = 1 / (float)SnapStepsPerLoop; float newValue = Mathf.Round(knobValue / snapStep) * snapStep; knobValue = newValue; } private void InvokeEvents(float value) { if (ClampOutput01) value /= Loops; OnValueChanged.Invoke(value); } public virtual void OnInitializePotentialDrag(PointerEventData eventData) { eventData.useDragThreshold = false; } } [System.Serializable] public class KnobFloatValueEvent : UnityEvent { } }