using UnityEngine.XR.Interaction.Toolkit.Inputs.Readers; using UnityEngine.XR.Interaction.Toolkit.Locomotion.Turning; using UnityEngine.XR.Interaction.Toolkit.Locomotion.Gravity; using UnityEngine.XR.Interaction.Toolkit.Utilities; namespace UnityEngine.XR.Interaction.Toolkit.Locomotion.Movement { /// /// Locomotion provider that allows the user to smoothly move their rig continuously over time /// based on read input values, such as from the controller thumbstick. /// /// /// [AddComponentMenu("XR/Locomotion/Continuous Move Provider", 11)] [HelpURL(XRHelpURLConstants.k_ContinuousMoveProvider)] public partial class ContinuousMoveProvider : LocomotionProvider, IGravityController { [SerializeField] [Tooltip("The speed, in units per second, to move forward.")] float m_MoveSpeed = 1f; /// /// The speed, in units per second, to move forward. /// public float moveSpeed { get => m_MoveSpeed; set => m_MoveSpeed = value; } [SerializeField] [Tooltip("Determines how much control the player has while in the air (0 = no control, 1 = full control).")] float m_InAirControlModifier = 0.5f; /// /// Determines how much control the player has while in the air (0 = no control, 1 = full control). /// public float inAirControlModifier { get => m_InAirControlModifier; set => m_InAirControlModifier = value; } [SerializeField] [Tooltip("Controls whether to enable strafing (sideways movement).")] bool m_EnableStrafe = true; /// /// Controls whether to enable strafing (sideways movement). /// public bool enableStrafe { get => m_EnableStrafe; set => m_EnableStrafe = value; } [SerializeField] [Tooltip("Controls whether to enable flying (unconstrained movement). This overrides the use of gravity.")] bool m_EnableFly; /// /// Controls whether to enable flying (unconstrained movement). This overrides the use of gravity. /// public bool enableFly { get => m_EnableFly; set => m_EnableFly = value; } [SerializeField] [Tooltip("The source Transform to define the forward direction.")] Transform m_ForwardSource; /// /// The source that defines the forward direction. /// public Transform forwardSource { get => m_ForwardSource; set => m_ForwardSource = value; } /// /// The transformation that is used by this component to apply translation movement. /// public XROriginMovement transformation { get; set; } = new XROriginMovement(); [SerializeField] [Tooltip("Reads input data from the left hand controller. Input Action must be a Value action type (Vector 2).")] XRInputValueReader m_LeftHandMoveInput = new XRInputValueReader("Left Hand Move"); /// /// Reads input data from the left hand controller. Input Action must be a Value action type (Vector 2). /// public XRInputValueReader leftHandMoveInput { get => m_LeftHandMoveInput; set => XRInputReaderUtility.SetInputProperty(ref m_LeftHandMoveInput, value, this); } [SerializeField] [Tooltip("Reads input data from the right hand controller. Input Action must be a Value action type (Vector 2).")] XRInputValueReader m_RightHandMoveInput = new XRInputValueReader("Right Hand Move"); /// /// Reads input data from the right hand controller. Input Action must be a Value action type (Vector 2). /// public XRInputValueReader rightHandMoveInput { get => m_RightHandMoveInput; set => XRInputReaderUtility.SetInputProperty(ref m_RightHandMoveInput, value, this); } /// public bool canProcess => isActiveAndEnabled; /// public bool gravityPaused => m_EnableFly; GravityProvider m_GravityProvider; CharacterController m_CharacterController; bool m_AttemptedGetCharacterController; bool m_IsMovingXROrigin; Vector3 m_GravityDrivenVelocity; Vector3 m_InAirVelocity; /// protected override void Awake() { base.Awake(); if (ComponentLocatorUtility.TryFindComponent(out m_GravityProvider)) { #pragma warning disable CS0618 // Type or member is obsolete -- Assist with migration when this component wants gravity disabled if (!m_UseGravity) MigrateUseGravityToGravityProvider(); #pragma warning restore CS0618 } } /// /// See . /// protected void OnEnable() { // Enable and disable directly serialized actions with this behavior's enabled lifecycle. m_LeftHandMoveInput.EnableDirectActionIfModeUsed(); m_RightHandMoveInput.EnableDirectActionIfModeUsed(); m_GravityDrivenVelocity = Vector3.zero; m_InAirVelocity = Vector3.zero; } /// /// See . /// protected void OnDisable() { m_LeftHandMoveInput.DisableDirectActionIfModeUsed(); m_RightHandMoveInput.DisableDirectActionIfModeUsed(); } /// protected override void OnLocomotionStarting() { base.OnLocomotionStarting(); TryLockGravity(m_EnableFly ? GravityOverride.ForcedOff : GravityOverride.ForcedOn); } /// protected override void OnLocomotionEnding() { base.OnLocomotionEnding(); RemoveGravityLock(); } /// /// See . /// protected void Update() { m_IsMovingXROrigin = false; var xrOrigin = mediator.xrOrigin?.Origin; if (xrOrigin == null) return; var input = ReadInput(); var translationInWorldSpace = ComputeDesiredMove(input); if (input != Vector2.zero || m_GravityDrivenVelocity != Vector3.zero || m_InAirVelocity != Vector3.zero) MoveRig(translationInWorldSpace); if (!m_IsMovingXROrigin) TryEndLocomotion(); } Vector2 ReadInput() { var leftHandValue = m_LeftHandMoveInput.ReadValue(); var rightHandValue = m_RightHandMoveInput.ReadValue(); return leftHandValue + rightHandValue; } /// /// Determines how much to slide the rig due to vector. /// /// Input vector, such as from a thumbstick. /// Returns the translation amount in world space to move the rig. protected virtual Vector3 ComputeDesiredMove(Vector2 input) { if (input == Vector2.zero && m_InAirVelocity == Vector3.zero) return Vector3.zero; var xrOrigin = mediator.xrOrigin; if (xrOrigin == null) return Vector3.zero; // Assumes that the input axes are in the range [-1, 1]. // Clamps the magnitude of the input direction to prevent faster speed when moving diagonally, // while still allowing for analog input to move slower (which would be lost if simply normalizing). var inputMove = Vector3.ClampMagnitude(new Vector3(m_EnableStrafe ? input.x : 0f, 0f, input.y), 1f); var deltaTime = Time.deltaTime; // Check if the user is not in the air, and update the input velocity accordingly. // If the user is in the air, update the input velocity with the inAirControlModifier. if (m_GravityProvider == null || m_GravityProvider.isGrounded) m_InAirVelocity = inputMove; else m_InAirVelocity += deltaTime * m_InAirControlModifier * 10 * (inputMove - m_InAirVelocity); // Determine frame of reference for what the input direction is relative to var forwardSourceTransform = m_ForwardSource == null ? xrOrigin.Camera.transform : m_ForwardSource; var inputForwardInWorldSpace = forwardSourceTransform.forward; var originTransform = xrOrigin.Origin.transform; var speedFactor = m_MoveSpeed * deltaTime * originTransform.localScale.x; // Adjust speed with user scale // If flying, just compute move directly from input and forward source if (m_EnableFly) { var inputRightInWorldSpace = forwardSourceTransform.right; var combinedMove = inputMove.x * inputRightInWorldSpace + inputMove.z * inputForwardInWorldSpace; return combinedMove * speedFactor; } var originUp = originTransform.up; if (Mathf.Approximately(Mathf.Abs(Vector3.Dot(inputForwardInWorldSpace, originUp)), 1f)) { // When the input forward direction is parallel with the rig normal, // it will probably feel better for the player to move along the same direction // as if they tilted forward or up some rather than moving in the rig forward direction. // It also will probably be a better experience to at least move in a direction // rather than stopping if the head/controller is oriented such that it is perpendicular with the rig. inputForwardInWorldSpace = -forwardSourceTransform.up; } var inputForwardProjectedInWorldSpace = Vector3.ProjectOnPlane(inputForwardInWorldSpace, originUp); var forwardRotation = Quaternion.FromToRotation(originTransform.forward, inputForwardProjectedInWorldSpace); var translationInRigSpace = forwardRotation * m_InAirVelocity * speedFactor; var translationInWorldSpace = originTransform.TransformDirection(translationInRigSpace); return translationInWorldSpace; } /// /// Creates a locomotion event to move the rig by , /// and optionally applies gravity. /// /// The translation amount in world space to move the rig (pre-gravity). protected virtual void MoveRig(Vector3 translationInWorldSpace) { var xrOrigin = mediator.xrOrigin?.Origin; if (xrOrigin == null) return; FindCharacterController(); var motion = translationInWorldSpace; // Step vertical velocity from gravity, but only if gravity is not being controlled by Gravity Provider if (m_GravityProvider == null && m_CharacterController != null && m_CharacterController.enabled) { #pragma warning disable CS0618 // Type or member is obsolete if (m_CharacterController.isGrounded || !m_UseGravity || m_EnableFly) m_GravityDrivenVelocity = Vector3.zero; else m_GravityDrivenVelocity += Physics.gravity * Time.deltaTime; motion += m_GravityDrivenVelocity * Time.deltaTime; #pragma warning restore CS0618 // Type or member is obsolete } TryStartLocomotionImmediately(); if (locomotionState != LocomotionState.Moving) return; // Note that calling Move even with Vector3.zero will have an effect by causing isGrounded to update m_IsMovingXROrigin = true; transformation.motion = motion; TryQueueTransformation(transformation); } void FindCharacterController() { var xrOrigin = mediator.xrOrigin?.Origin; if (xrOrigin == null) return; // Save a reference to the optional CharacterController on the rig GameObject // that will be used to move instead of modifying the Transform directly. if (m_CharacterController == null && !m_AttemptedGetCharacterController) { // Try on the Origin GameObject first, and then fallback to the XR Origin GameObject (if different) if (!xrOrigin.TryGetComponent(out m_CharacterController) && xrOrigin != mediator.xrOrigin.gameObject) mediator.xrOrigin.TryGetComponent(out m_CharacterController); m_AttemptedGetCharacterController = true; } } /// public bool TryLockGravity(GravityOverride gravityOverride) { if (m_GravityProvider != null) return m_GravityProvider.TryLockGravity(this, gravityOverride); return false; } /// public void RemoveGravityLock() { if (m_GravityProvider != null) m_GravityProvider.UnlockGravity(this); } /// void IGravityController.OnGroundedChanged(bool isGrounded) => OnGroundedChanged(isGrounded); /// void IGravityController.OnGravityLockChanged(GravityOverride gravityOverride) => OnGravityLockChanged(gravityOverride); /// /// Called from when the grounded state changes. /// /// Whether the player is on the ground. /// protected virtual void OnGroundedChanged(bool isGrounded) { } /// /// Called from when gravity lock is changed. /// /// The to apply. /// protected virtual void OnGravityLockChanged(GravityOverride gravityOverride) { } } }