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;
}
}
}