forked from cgvr/DeltaVR
		
	
		
			
				
	
	
		
			324 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
			
		
		
	
	
			324 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
using System;
 | 
						|
using UnityEngine.Events;
 | 
						|
using UnityEngine.XR.Interaction.Toolkit;
 | 
						|
 | 
						|
namespace UnityEngine.XR.Content.Interaction
 | 
						|
{
 | 
						|
    /// <summary>
 | 
						|
    /// An interactable joystick that can move side to side, and forward and back by a direct interactor
 | 
						|
    /// </summary>
 | 
						|
    public class XRJoystick : XRBaseInteractable
 | 
						|
    {
 | 
						|
        const float k_MaxDeadZonePercent = 0.9f;
 | 
						|
 | 
						|
        public enum JoystickType
 | 
						|
        {
 | 
						|
            BothCircle,
 | 
						|
            BothSquare,
 | 
						|
            FrontBack,
 | 
						|
            LeftRight,
 | 
						|
        }
 | 
						|
 | 
						|
        [Serializable]
 | 
						|
        public class ValueChangeEvent : UnityEvent<float> { }
 | 
						|
 | 
						|
        [Tooltip("Controls how the joystick moves")]
 | 
						|
        [SerializeField]
 | 
						|
        JoystickType m_JoystickMotion = JoystickType.BothCircle;
 | 
						|
 | 
						|
        [SerializeField]
 | 
						|
        [Tooltip("The object that is visually grabbed and manipulated")]
 | 
						|
        Transform m_Handle = null;
 | 
						|
 | 
						|
        [SerializeField]
 | 
						|
        [Tooltip("The value of the joystick")]
 | 
						|
        Vector2 m_Value = Vector2.zero;
 | 
						|
 | 
						|
        [SerializeField]
 | 
						|
        [Tooltip("If true, the joystick will return to center on release")]
 | 
						|
        bool m_RecenterOnRelease = true;
 | 
						|
 | 
						|
        [SerializeField]
 | 
						|
        [Tooltip("Maximum angle the joystick can move")]
 | 
						|
        [Range(1.0f, 90.0f)]
 | 
						|
        float m_MaxAngle = 60.0f;
 | 
						|
 | 
						|
        [SerializeField]
 | 
						|
        [Tooltip("Minimum amount the joystick must move off the center to register changes")]
 | 
						|
        [Range(1.0f, 90.0f)]
 | 
						|
        float m_DeadZoneAngle = 10.0f;
 | 
						|
 | 
						|
        [SerializeField]
 | 
						|
        [Tooltip("Events to trigger when the joystick's x value changes")]
 | 
						|
        ValueChangeEvent m_OnValueChangeX = new ValueChangeEvent();
 | 
						|
 | 
						|
        [SerializeField]
 | 
						|
        [Tooltip("Events to trigger when the joystick's y value changes")]
 | 
						|
        ValueChangeEvent m_OnValueChangeY = new ValueChangeEvent();
 | 
						|
 | 
						|
        IXRSelectInteractor m_Interactor;
 | 
						|
 | 
						|
        /// <summary>
 | 
						|
        /// Controls how the joystick moves
 | 
						|
        /// </summary>
 | 
						|
        public JoystickType joystickMotion
 | 
						|
        {
 | 
						|
            get => m_JoystickMotion;
 | 
						|
            set => m_JoystickMotion = value;
 | 
						|
        }
 | 
						|
 | 
						|
        /// <summary>
 | 
						|
        /// The object that is visually grabbed and manipulated
 | 
						|
        /// </summary>
 | 
						|
        public Transform handle
 | 
						|
        {
 | 
						|
            get => m_Handle;
 | 
						|
            set => m_Handle = value;
 | 
						|
        }
 | 
						|
 | 
						|
        /// <summary>
 | 
						|
        /// The value of the joystick
 | 
						|
        /// </summary>
 | 
						|
        public Vector2 value
 | 
						|
        {
 | 
						|
            get => m_Value;
 | 
						|
            set
 | 
						|
            {
 | 
						|
                if (!m_RecenterOnRelease)
 | 
						|
                {
 | 
						|
                    SetValue(value);
 | 
						|
                    SetHandleAngle(value * m_MaxAngle);
 | 
						|
                }
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        /// <summary>
 | 
						|
        /// If true, the joystick will return to center on release
 | 
						|
        /// </summary>
 | 
						|
        public bool recenterOnRelease
 | 
						|
        {
 | 
						|
            get => m_RecenterOnRelease;
 | 
						|
            set => m_RecenterOnRelease = value;
 | 
						|
        }
 | 
						|
 | 
						|
        /// <summary>
 | 
						|
        /// Maximum angle the joystick can move
 | 
						|
        /// </summary>
 | 
						|
        public float maxAngle
 | 
						|
        {
 | 
						|
            get => m_MaxAngle;
 | 
						|
            set => m_MaxAngle = value;
 | 
						|
        }
 | 
						|
 | 
						|
        /// <summary>
 | 
						|
        /// Minimum amount the joystick must move off the center to register changes
 | 
						|
        /// </summary>
 | 
						|
        public float deadZoneAngle
 | 
						|
        {
 | 
						|
            get => m_DeadZoneAngle;
 | 
						|
            set => m_DeadZoneAngle = value;
 | 
						|
        }
 | 
						|
 | 
						|
        /// <summary>
 | 
						|
        /// Events to trigger when the joystick's x value changes
 | 
						|
        /// </summary>
 | 
						|
        public ValueChangeEvent onValueChangeX => m_OnValueChangeX;
 | 
						|
 | 
						|
        /// <summary>
 | 
						|
        /// Events to trigger when the joystick's y value changes
 | 
						|
        /// </summary>
 | 
						|
        public ValueChangeEvent onValueChangeY => m_OnValueChangeY;
 | 
						|
 | 
						|
        void Start()
 | 
						|
        {
 | 
						|
            if (m_RecenterOnRelease)
 | 
						|
                SetHandleAngle(Vector2.zero);
 | 
						|
        }
 | 
						|
 | 
						|
        protected override void OnEnable()
 | 
						|
        {
 | 
						|
            base.OnEnable();
 | 
						|
            selectEntered.AddListener(StartGrab);
 | 
						|
            selectExited.AddListener(EndGrab);
 | 
						|
        }
 | 
						|
 | 
						|
        protected override void OnDisable()
 | 
						|
        {
 | 
						|
            selectEntered.RemoveListener(StartGrab);
 | 
						|
            selectExited.RemoveListener(EndGrab);
 | 
						|
            base.OnDisable();
 | 
						|
        }
 | 
						|
 | 
						|
        private void StartGrab(SelectEnterEventArgs args)
 | 
						|
        {
 | 
						|
            m_Interactor = args.interactorObject;
 | 
						|
        }
 | 
						|
 | 
						|
        private void EndGrab(SelectExitEventArgs arts)
 | 
						|
        {
 | 
						|
            UpdateValue();
 | 
						|
 | 
						|
            if (m_RecenterOnRelease)
 | 
						|
            {
 | 
						|
                SetHandleAngle(Vector2.zero);
 | 
						|
                SetValue(Vector2.zero);
 | 
						|
            }
 | 
						|
 | 
						|
            m_Interactor = null;
 | 
						|
        }
 | 
						|
 | 
						|
        public override void ProcessInteractable(XRInteractionUpdateOrder.UpdatePhase updatePhase)
 | 
						|
        {
 | 
						|
            base.ProcessInteractable(updatePhase);
 | 
						|
 | 
						|
            if (updatePhase == XRInteractionUpdateOrder.UpdatePhase.Dynamic)
 | 
						|
            {
 | 
						|
                if (isSelected)
 | 
						|
                {
 | 
						|
                    UpdateValue();
 | 
						|
                }
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        Vector3 GetLookDirection()
 | 
						|
        {
 | 
						|
            Vector3 direction = m_Interactor.GetAttachTransform(this).position - m_Handle.position;
 | 
						|
            direction = transform.InverseTransformDirection(direction);
 | 
						|
            switch (m_JoystickMotion)
 | 
						|
            {
 | 
						|
                case JoystickType.FrontBack:
 | 
						|
                    direction.x = 0;
 | 
						|
                    break;
 | 
						|
                case JoystickType.LeftRight:
 | 
						|
                    direction.z = 0;
 | 
						|
                    break;
 | 
						|
            }
 | 
						|
 | 
						|
            direction.y = Mathf.Clamp(direction.y, 0.01f, 1.0f);
 | 
						|
            return direction.normalized;
 | 
						|
        }
 | 
						|
 | 
						|
        void UpdateValue()
 | 
						|
        {
 | 
						|
            var lookDirection = GetLookDirection();
 | 
						|
 | 
						|
            // Get up/down angle and left/right angle
 | 
						|
            var upDownAngle = Mathf.Atan2(lookDirection.z, lookDirection.y) * Mathf.Rad2Deg;
 | 
						|
            var leftRightAngle = Mathf.Atan2(lookDirection.x, lookDirection.y) * Mathf.Rad2Deg;
 | 
						|
 | 
						|
            // Extract signs
 | 
						|
            var signX = Mathf.Sign(leftRightAngle);
 | 
						|
            var signY = Mathf.Sign(upDownAngle);
 | 
						|
 | 
						|
            upDownAngle = Mathf.Abs(upDownAngle);
 | 
						|
            leftRightAngle = Mathf.Abs(leftRightAngle);
 | 
						|
 | 
						|
            var stickValue = new Vector2(leftRightAngle, upDownAngle) * (1.0f / m_MaxAngle);
 | 
						|
 | 
						|
            // Clamp the stick value between 0 and 1 when doing everything but circular stick motion
 | 
						|
            if (m_JoystickMotion != JoystickType.BothCircle)
 | 
						|
            {
 | 
						|
                stickValue.x = Mathf.Clamp01(stickValue.x);
 | 
						|
                stickValue.y = Mathf.Clamp01(stickValue.y);
 | 
						|
            }
 | 
						|
            else
 | 
						|
            {
 | 
						|
                // With circular motion, if the stick value is greater than 1, we normalize
 | 
						|
                // This way, an extremely strong value in one direction will influence the overall stick direction
 | 
						|
                if (stickValue.magnitude > 1.0f)
 | 
						|
                {
 | 
						|
                    stickValue.Normalize();
 | 
						|
                }
 | 
						|
            }
 | 
						|
 | 
						|
            // Rebuild the angle values for visuals
 | 
						|
            leftRightAngle = stickValue.x * signX * m_MaxAngle;
 | 
						|
            upDownAngle = stickValue.y * signY * m_MaxAngle;
 | 
						|
 | 
						|
            // Apply deadzone and sign back to the logical stick value
 | 
						|
            var deadZone = m_DeadZoneAngle / m_MaxAngle;
 | 
						|
            var aliveZone = (1.0f - deadZone);
 | 
						|
            stickValue.x = Mathf.Clamp01((stickValue.x - deadZone)) / aliveZone;
 | 
						|
            stickValue.y = Mathf.Clamp01((stickValue.y - deadZone)) / aliveZone;
 | 
						|
 | 
						|
            // Re-apply signs
 | 
						|
            stickValue.x *= signX;
 | 
						|
            stickValue.y *= signY;
 | 
						|
 | 
						|
            SetHandleAngle(new Vector2(leftRightAngle, upDownAngle));
 | 
						|
            SetValue(stickValue);
 | 
						|
        }
 | 
						|
 | 
						|
        void SetValue(Vector2 value)
 | 
						|
        {
 | 
						|
            m_Value = value;
 | 
						|
            m_OnValueChangeX.Invoke(m_Value.x);
 | 
						|
            m_OnValueChangeY.Invoke(m_Value.y);
 | 
						|
        }
 | 
						|
 | 
						|
        void SetHandleAngle(Vector2 angles)
 | 
						|
        {
 | 
						|
            if (m_Handle == null)
 | 
						|
                return;
 | 
						|
 | 
						|
            var xComp = Mathf.Tan(angles.x * Mathf.Deg2Rad);
 | 
						|
            var zComp = Mathf.Tan(angles.y * Mathf.Deg2Rad);
 | 
						|
            var largerComp = Mathf.Max(Mathf.Abs(xComp), Mathf.Abs(zComp));
 | 
						|
            var yComp = Mathf.Sqrt(1.0f - largerComp * largerComp);
 | 
						|
 | 
						|
            m_Handle.up = (transform.up * yComp) + (transform.right * xComp) + (transform.forward * zComp);
 | 
						|
        }
 | 
						|
 | 
						|
        void OnDrawGizmosSelected()
 | 
						|
        {
 | 
						|
            var angleStartPoint = transform.position;
 | 
						|
 | 
						|
            if (m_Handle != null)
 | 
						|
                angleStartPoint = m_Handle.position;
 | 
						|
 | 
						|
            const float k_AngleLength = 0.25f;
 | 
						|
 | 
						|
            if (m_JoystickMotion != JoystickType.LeftRight)
 | 
						|
            {
 | 
						|
                Gizmos.color = Color.green;
 | 
						|
                var axisPoint1 = angleStartPoint + transform.TransformDirection(Quaternion.Euler(m_MaxAngle, 0.0f, 0.0f) * Vector3.up) * k_AngleLength;
 | 
						|
                var axisPoint2 = angleStartPoint + transform.TransformDirection(Quaternion.Euler(-m_MaxAngle, 0.0f, 0.0f) * Vector3.up) * k_AngleLength;
 | 
						|
                Gizmos.DrawLine(angleStartPoint, axisPoint1);
 | 
						|
                Gizmos.DrawLine(angleStartPoint, axisPoint2);
 | 
						|
 | 
						|
                if (m_DeadZoneAngle > 0.0f)
 | 
						|
                {
 | 
						|
                    Gizmos.color = Color.red;
 | 
						|
                    axisPoint1 = angleStartPoint + transform.TransformDirection(Quaternion.Euler(m_DeadZoneAngle, 0.0f, 0.0f) * Vector3.up) * k_AngleLength;
 | 
						|
                    axisPoint2 = angleStartPoint + transform.TransformDirection(Quaternion.Euler(-m_DeadZoneAngle, 0.0f, 0.0f) * Vector3.up) * k_AngleLength;
 | 
						|
                    Gizmos.DrawLine(angleStartPoint, axisPoint1);
 | 
						|
                    Gizmos.DrawLine(angleStartPoint, axisPoint2);
 | 
						|
                }
 | 
						|
            }
 | 
						|
 | 
						|
            if (m_JoystickMotion != JoystickType.FrontBack)
 | 
						|
            {
 | 
						|
                Gizmos.color = Color.green;
 | 
						|
                var axisPoint1 = angleStartPoint + transform.TransformDirection(Quaternion.Euler(0.0f, 0.0f, m_MaxAngle) * Vector3.up) * k_AngleLength;
 | 
						|
                var axisPoint2 = angleStartPoint + transform.TransformDirection(Quaternion.Euler(0.0f, 0.0f, -m_MaxAngle) * Vector3.up) * k_AngleLength;
 | 
						|
                Gizmos.DrawLine(angleStartPoint, axisPoint1);
 | 
						|
                Gizmos.DrawLine(angleStartPoint, axisPoint2);
 | 
						|
 | 
						|
                if (m_DeadZoneAngle > 0.0f)
 | 
						|
                {
 | 
						|
                    Gizmos.color = Color.red;
 | 
						|
                    axisPoint1 = angleStartPoint + transform.TransformDirection(Quaternion.Euler(0.0f, 0.0f, m_DeadZoneAngle) * Vector3.up) * k_AngleLength;
 | 
						|
                    axisPoint2 = angleStartPoint + transform.TransformDirection(Quaternion.Euler(0.0f, 0.0f, -m_DeadZoneAngle) * Vector3.up) * k_AngleLength;
 | 
						|
                    Gizmos.DrawLine(angleStartPoint, axisPoint1);
 | 
						|
                    Gizmos.DrawLine(angleStartPoint, axisPoint2);
 | 
						|
                }
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        void OnValidate()
 | 
						|
        {
 | 
						|
            m_DeadZoneAngle = Mathf.Min(m_DeadZoneAngle, m_MaxAngle * k_MaxDeadZonePercent);
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 |