286 lines
11 KiB
C#
286 lines
11 KiB
C#
/// 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;
|
|
|
|
/// <summary>
|
|
/// 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 <float> argument of knobValue
|
|
/// NOTES
|
|
/// - script works only in images rotation on Z axis;
|
|
/// - while dragging outside of control, the rotation will be canceled
|
|
/// </summary>
|
|
///
|
|
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<Canvas>().rootCanvas.renderMode == RenderMode.ScreenSpaceOverlay;
|
|
}
|
|
|
|
protected override void Start()
|
|
{
|
|
CheckForParentTouchMask();
|
|
}
|
|
|
|
private void CheckForParentTouchMask()
|
|
{
|
|
if (ParentTouchMask)
|
|
{
|
|
Image maskImage = ParentTouchMask.gameObject.GetOrAddComponent<Image>();
|
|
maskImage.color = MaskBackground;
|
|
EventTrigger trigger = ParentTouchMask.gameObject.GetOrAddComponent<EventTrigger>();
|
|
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<float> { }
|
|
|
|
} |