using System; using Unity.Mathematics; using UnityEngine.Assertions; using UnityEngine.XR.Interaction.Toolkit.Inputs; using UnityEngine.XR.Interaction.Toolkit.Inputs.Readers; namespace UnityEngine.XR.Interaction.Toolkit.Locomotion.Turning { /// /// Locomotion provider that allows the user to rotate their rig in fixed angle increments /// based on read input values, such as from the controller thumbstick. /// /// /// [AddComponentMenu("XR/Locomotion/Snap Turn Provider", 11)] [HelpURL(XRHelpURLConstants.k_SnapTurnProvider)] public class SnapTurnProvider : LocomotionProvider { [SerializeField] [Tooltip("The number of degrees clockwise to rotate when snap turning clockwise.")] float m_TurnAmount = 45f; /// /// The number of degrees clockwise Unity rotates the rig when snap turning clockwise. /// public float turnAmount { get => m_TurnAmount; set => m_TurnAmount = value; } [SerializeField] [Tooltip("The amount of time that the system will wait before starting another snap turn.")] float m_DebounceTime = 0.5f; /// /// The amount of time that Unity waits before starting another snap turn. /// public float debounceTime { get => m_DebounceTime; set => m_DebounceTime = value; } [SerializeField] [Tooltip("Controls whether to enable left & right snap turns.")] bool m_EnableTurnLeftRight = true; /// /// Controls whether to enable left and right snap turns. /// /// public bool enableTurnLeftRight { get => m_EnableTurnLeftRight; set => m_EnableTurnLeftRight = value; } [SerializeField] [Tooltip("Controls whether to enable 180° snap turns.")] bool m_EnableTurnAround = true; /// /// Controls whether to enable 180° snap turns. /// /// public bool enableTurnAround { get => m_EnableTurnAround; set => m_EnableTurnAround = value; } [SerializeField] [Tooltip("The time (in seconds) to delay the first turn after receiving initial input for the turn.")] float m_DelayTime; /// /// The time (in seconds) to delay the first turn after receiving initial input for the turn. /// Subsequent turns while holding down input are delayed by the , not the delay time. /// This delay can be used, for example, as time to set a tunneling vignette effect as a VR comfort option. /// public float delayTime { get => m_DelayTime; set => m_DelayTime = value; } /// public override bool canStartMoving => m_DelayTime <= 0f || Time.time - m_DelayStartTime >= m_DelayTime; /// /// The transformation that is used by this component to apply turn movement. /// public XRBodyYawRotation transformation { get; set; } = new XRBodyYawRotation(); [SerializeField] [Tooltip("Reads input data from the left hand controller. Input Action must be a Value action type (Vector 2).")] XRInputValueReader m_LeftHandTurnInput = new XRInputValueReader("Left Hand Snap Turn"); /// /// Reads input data from the left hand controller. Input Action must be a Value action type (Vector 2). /// public XRInputValueReader leftHandTurnInput { get => m_LeftHandTurnInput; set => XRInputReaderUtility.SetInputProperty(ref m_LeftHandTurnInput, value, this); } [SerializeField] [Tooltip("Reads input data from the right hand controller. Input Action must be a Value action type (Vector 2).")] XRInputValueReader m_RightHandTurnInput = new XRInputValueReader("Right Hand Snap Turn"); /// /// Reads input data from the right hand controller. Input Action must be a Value action type (Vector 2). /// public XRInputValueReader rightHandTurnInput { get => m_RightHandTurnInput; set => XRInputReaderUtility.SetInputProperty(ref m_RightHandTurnInput, value, this); } float m_CurrentTurnAmount; float m_TimeStarted; float m_DelayStartTime; bool m_TurnAroundActivated; /// /// See . /// protected void OnEnable() { // Enable and disable directly serialized actions with this behavior's enabled lifecycle. m_LeftHandTurnInput.EnableDirectActionIfModeUsed(); m_RightHandTurnInput.EnableDirectActionIfModeUsed(); } /// /// See . /// protected void OnDisable() { m_LeftHandTurnInput.DisableDirectActionIfModeUsed(); m_RightHandTurnInput.DisableDirectActionIfModeUsed(); } /// /// See . /// protected void Update() { // Wait for a certain amount of time before allowing another turn. if (m_TimeStarted > 0f && (m_TimeStarted + m_DebounceTime < Time.time)) { m_TimeStarted = 0f; return; } var input = ReadInput(); var amount = GetTurnAmount(input); if (Mathf.Abs(amount) > 0f) { StartTurn(amount); } else if (Mathf.Approximately(m_CurrentTurnAmount, 0f) && locomotionState == LocomotionState.Moving) { TryEndLocomotion(); } if (locomotionState == LocomotionState.Moving && math.abs(m_CurrentTurnAmount) > 0f) { m_TimeStarted = Time.time; transformation.angleDelta = m_CurrentTurnAmount; TryQueueTransformation(transformation); m_CurrentTurnAmount = 0f; if (Mathf.Approximately(amount, 0f)) TryEndLocomotion(); } if (input == Vector2.zero) m_TurnAroundActivated = false; } Vector2 ReadInput() { var leftHandValue = m_LeftHandTurnInput.ReadValue(); var rightHandValue = m_RightHandTurnInput.ReadValue(); return leftHandValue + rightHandValue; } /// /// Determines the turn amount in degrees for the given vector. /// /// Input vector, such as from a thumbstick. /// Returns the turn amount in degrees for the given vector. protected virtual float GetTurnAmount(Vector2 input) { if (input == Vector2.zero) return 0f; var cardinal = CardinalUtility.GetNearestCardinal(input); switch (cardinal) { case Cardinal.North: break; case Cardinal.South: if (m_EnableTurnAround && !m_TurnAroundActivated) return 180f; break; case Cardinal.East: if (m_EnableTurnLeftRight) return m_TurnAmount; break; case Cardinal.West: if (m_EnableTurnLeftRight) return -m_TurnAmount; break; default: Assert.IsTrue(false, $"Unhandled {nameof(Cardinal)}={cardinal}"); break; } return 0f; } /// /// Begins turning locomotion. /// /// Amount to turn. protected void StartTurn(float amount) { if (m_TimeStarted > 0f) return; if (Mathf.Approximately(amount, 180f)) m_TurnAroundActivated = true; if (locomotionState == LocomotionState.Idle) { if (m_DelayTime > 0f) { if (TryPrepareLocomotion()) m_DelayStartTime = Time.time; } else { TryStartLocomotionImmediately(); } } // We set the m_CurrentTurnAmount here so we can still trigger the turn // in the case where the input is released before the delay timeout happens. if (math.abs(amount) > 0f) m_CurrentTurnAmount = amount; } } }