using UnityEngine; using UnityEngine.Events; using System.Collections.Generic; using Lean.Common; using FSA = UnityEngine.Serialization.FormerlySerializedAsAttribute; namespace Lean.Touch { /// This script allows you to twist the selected object around like a dial or knob. [ExecuteInEditMode] [HelpURL(LeanTouch.PlusHelpUrlPrefix + "LeanSelectableDial")] [AddComponentMenu(LeanTouch.ComponentPathPrefix + "Selectable Dial")] public class LeanSelectableDial : LeanSelectableByFingerBehaviour { [System.Serializable] public class Trigger { [Tooltip("The central Angle of this trigger in degrees.")] public float Angle; [Tooltip("The angle range of this trigger in degrees.\n\n90 = Quarter circle.\n180 = Half circle.")] public float Arc; [HideInInspector] public bool Inside; public UnityEvent OnEnter { get { if (onEnter == null) onEnter = new UnityEvent(); return onEnter; } } [SerializeField] private UnityEvent onEnter; public UnityEvent OnExit { get { if (onExit == null) onExit = new UnityEvent(); return onExit; } } [SerializeField] private UnityEvent onExit; public bool IsInside(float angle, bool clamp) { var range = Arc * 0.5f; if (clamp == false) { var delta = Mathf.Abs(Mathf.DeltaAngle(Angle, angle)); return delta < range; } return angle >= Angle - range && angle <= Angle + range; } } [System.Serializable] public class FloatEvent : UnityEvent {} /// The camera this component will calculate using. /// None/null = MainCamera. public Camera Camera { set { _camera = value; } get { return _camera; } } [FSA("Camera")] [SerializeField] private Camera _camera; /// The base rotation in local space. public Vector3 Tilt { set { tilt = value; } get { return tilt; } } [FSA("Tilt")] [SerializeField] private Vector3 tilt; /// The axis of the rotation in local space. public Vector3 Axis { set { axis = value; } get { return axis; } } [FSA("Axis")] [SerializeField] private Vector3 axis = Vector3.up; /// The angle of the dial in degrees. public float Angle { set { var newAngle = value; if (clamp == true) { newAngle = Mathf.Clamp(newAngle, clampMin, clampMax); } if (angle != newAngle) { angle = newAngle; if (onAngleChanged != null) onAngleChanged.Invoke(angle); } } get { return angle; } } [FSA("Angle")] [SerializeField] private float angle; /// Should the Angle value be clamped? public bool Clamp { set { clamp = value; } get { return clamp; } } [FSA("Clamp")] [SerializeField] private bool clamp; /// The minimum Angle value. public float ClampMin { set { clampMin = value; } get { return clampMin; } } [FSA("ClampMin")] [SerializeField] private float clampMin = -45.0f; /// The maximum Angle value. public float ClampMax { set { clampMax = value; } get { return clampMax; } } [FSA("ClampMax")] [SerializeField] private float clampMax = 45.0f; /// This allows you to perform a custom event when the dial is within a specified angle range. public List Triggers { get { if (triggers == null) triggers = new List(); return triggers; } } [FSA("Triggers")] [SerializeField] private List triggers; /// This event is invoked when the Angle changes. /// Float = Current Angle. public FloatEvent OnAngleChanged { get { if (onAngleChanged == null) onAngleChanged = new FloatEvent(); return onAngleChanged; } } [SerializeField] private FloatEvent onAngleChanged; private Vector2 oldPoint; private bool oldPointSet; /// This method allows you to increase the Angle value from an external event (e.g. UI button click). public void IncrementAngle(float delta) { Angle += delta; } #if UNITY_EDITOR protected virtual void OnDrawGizmosSelected() { Gizmos.DrawLine(transform.position, transform.TransformPoint(axis)); } #endif protected virtual void Update() { var newAngle = angle; // Reset rotation and get axis transform.localEulerAngles = tilt; // Is this GameObject selected? if (Selectable != null && Selectable.IsSelected == true) { // Does it have a selected finger? var finger = Selectable.SelectingFinger; if (finger != null) { var newPoint = GetPoint(finger.ScreenPosition); if (oldPointSet == true) { newAngle -= Vector2.SignedAngle(newPoint, oldPoint); } oldPoint = newPoint; oldPointSet = true; } } else { oldPointSet = false; } if (clamp == true) { newAngle = Mathf.Clamp(newAngle, clampMin, clampMax); } transform.Rotate(axis, angle, Space.Self); if (triggers != null) { for (var i = 0; i < triggers.Count; i++) { var trigger = triggers[i]; if (trigger.IsInside(angle, clamp) == true) { if (trigger.Inside == false) { trigger.Inside = true; trigger.OnEnter.Invoke(); } } else { if (trigger.Inside == true) { trigger.Inside = false; trigger.OnExit.Invoke(); } } } } Angle = newAngle; } private Vector2 GetPoint(Vector2 screenPoint) { // Make sure the camera exists var camera = LeanHelper.GetCamera(_camera, gameObject); if (camera != null) { var rectTransform = transform as RectTransform; if (rectTransform != null) { var worldPoint = default(Vector3); if (RectTransformUtility.ScreenPointToWorldPointInRectangle(rectTransform, screenPoint, camera, out worldPoint) == true) { return Quaternion.LookRotation(axis) * transform.InverseTransformPoint(worldPoint); } } else { var ray = camera.ScreenPointToRay(screenPoint); var plane = new Plane(transform.TransformDirection(axis), transform.position); var distance = default(float); if (plane.Raycast(ray, out distance) == true) { return Quaternion.Inverse(Quaternion.LookRotation(axis)) * transform.InverseTransformPoint(ray.GetPoint(distance)); } } } else { Debug.LogError("Failed to find camera. Either tag your cameras MainCamera, or set one in this component.", this); } return oldPoint; } } } #if UNITY_EDITOR namespace Lean.Touch.Editor { using TARGET = LeanSelectableDial; [UnityEditor.CanEditMultipleObjects] [UnityEditor.CustomEditor(typeof(TARGET))] public class LeanSelectableDial_Editor : LeanEditor { protected override void OnInspector() { TARGET tgt; TARGET[] tgts; GetTargets(out tgt, out tgts); Draw("_camera", "The camera we will be used.\n\nNone/null = MainCamera."); Draw("tilt", "The base rotation in local space."); Draw("axis", "The axis of the rotation in local space."); Draw("angle", "The angle of the dial in degrees."); Separator(); Draw("clamp", "Should the Angle value be clamped?"); BeginIndent(); Draw("clampMin", "The minimum Angle value.", "Min"); Draw("clampMax", "The maximum Angle value.", "Max"); EndIndent(); Separator(); Draw("triggers", "This allows you to perform a custom event when the dial is within a specified angle range."); Separator(); Draw("onAngleChanged"); } } } #endif