using UnityEngine.InputSystem.Controls; using UnityEngine.XR.Interaction.Toolkit.Inputs.Readers; using UnityEngine.XR.Interaction.Toolkit.Locomotion.Gravity; using UnityEngine.XR.Interaction.Toolkit.Utilities; namespace UnityEngine.XR.Interaction.Toolkit.Locomotion.Jump { /// /// Jump Provider allows the player to jump in the scene. /// This uses a jump force to drive the value of the over time /// to allow the player to control how floaty the jump feels. /// The player can hold down the jump button to increase the altitude of the jump. /// This provider handles coyote time to allow jumping to feel more consistent. /// [AddComponentMenu("XR/Locomotion/Jump Provider", 11)] [HelpURL(XRHelpURLConstants.k_JumpProvider)] public class JumpProvider : LocomotionProvider, IGravityController { [SerializeField] [Tooltip("Disable gravity during the jump. This will result in a more floaty jump.")] bool m_DisableGravityDuringJump; /// /// Disable gravity during the jump. This will result in a more floaty jump. /// public bool disableGravityDuringJump { get => m_DisableGravityDuringJump; set => m_DisableGravityDuringJump = value; } [SerializeField] [Tooltip("Allow player to jump without being grounded.")] bool m_UnlimitedInAirJumps; /// /// Allow player to jump without being grounded. /// public bool unlimitedInAirJumps { get => m_UnlimitedInAirJumps; set => m_UnlimitedInAirJumps = value; } [SerializeField] [Tooltip("The number of times a player can jump before landing.")] int m_InAirJumpCount = 1; /// /// The number of times a player can jump before landing. /// public int inAirJumpCount { get => m_InAirJumpCount; set { m_InAirJumpCount = Mathf.Max(0, value); m_CurrentInAirJumpCount = m_InAirJumpCount; } } [SerializeField] [Tooltip("The time window after leaving the ground that a jump can still be performed. Sometimes known as coyote time.")] float m_JumpForgivenessWindow = 0.25f; /// /// The time window after leaving the ground that a jump can still be performed. Sometimes known as coyote time. /// public float jumpForgivenessWindow { get => m_JumpForgivenessWindow; set { m_JumpForgivenessWindow = value; m_CurrentJumpForgivenessWindowTime = m_JumpForgivenessWindow; } } [SerializeField] [Tooltip("The height (approximately in meters) the player will be when reaching the apex of the jump.")] float m_JumpHeight = 1.25f; /// /// The height (approximately in meters) the player will be when reaching the apex of the jump. /// public float jumpHeight { get => m_JumpHeight; set => m_JumpHeight = value; } [SerializeField] [Tooltip("Allow the player to stop their jump early when input is released before reaching the maximum jump height.")] bool m_VariableHeightJump = true; /// /// Whether the jump height is based on how long the player continues to hold the jump button. /// Enable to allow the player to stop their jump early when input is released before reaching the maximum jump height. /// Disable to jump a fixed height. /// public bool variableHeightJump { get => m_VariableHeightJump; set => m_VariableHeightJump = value; } [SerializeField] [Tooltip("The minimum amount of time the jump will execute for.")] float m_MinJumpHoldTime = 0.1f; /// /// The minimum amount of time the jump will execute for. /// public float minJumpHoldTime { get => m_MinJumpHoldTime; set => m_MinJumpHoldTime = value; } [SerializeField] [Tooltip("The maximum time a player can hold down the jump button to increase altitude.")] float m_MaxJumpHoldTime = 0.5f; /// /// The maximum time a player can hold down the jump button to increase altitude. /// public float maxJumpHoldTime { get => m_MaxJumpHoldTime; set => m_MaxJumpHoldTime = value; } [SerializeField] [Tooltip("The speed at which the jump will decelerate when the player releases the jump button early.")] float m_EarlyOutDecelerationSpeed = .1f; /// /// The speed at which the jump will decelerate when the player releases the jump button early. /// public float earlyOutDecelerationSpeed { get => m_EarlyOutDecelerationSpeed; set => m_EarlyOutDecelerationSpeed = value; } [SerializeField] [Tooltip("Input data that will be used to perform a jump.")] XRInputButtonReader m_JumpInput = new XRInputButtonReader("Jump"); /// /// Input data that will be used to perform a jump. /// If the source is an Input Action, it must have a button-like interaction where phase equals performed when pressed. /// Typically a Control or a Value type action with a Press interaction. /// public XRInputButtonReader jumpInput { get => m_JumpInput; set => XRInputReaderUtility.SetInputProperty(ref m_JumpInput, value, this); } /// /// The transformation that is used by this component to apply translation movement. /// public XROriginMovement transformation { get; set; } = new XROriginMovement(); bool m_IsJumping; /// /// Returns whether the player is currently jumping. /// /// /// This is only during jump ascent, it is not during the descent. /// public bool isJumping => m_IsJumping; /// public bool canProcess => isActiveAndEnabled; /// public bool gravityPaused { get; protected set; } /// /// Flag to make sure the player has released the jump button before allowing another jump. /// bool m_HasJumped; /// /// The current jump forgiveness time. /// float m_CurrentJumpForgivenessWindowTime; /// /// The time the player will stop at. Usually it's , but can change when using . /// float m_StoppingJumpTime; /// /// The current jump force being applied this frame. /// float m_CurrentJumpForceThisFrame; /// /// Current jump vector being applied to the player. /// Vector3 m_JumpVector; /// /// Reference to the gravity provider. /// GravityProvider m_GravityProvider; bool m_HasGravityProvider; float m_CurrentJumpTimer; int m_CurrentInAirJumpCount; /// /// See . /// protected virtual void OnValidate() { m_InAirJumpCount = Mathf.Max(0, m_InAirJumpCount); } /// protected override void Awake() { base.Awake(); m_HasGravityProvider = ComponentLocatorUtility.TryFindComponent(out m_GravityProvider); if (!m_HasGravityProvider) { Debug.LogError("Could not find Gravity Provider component which is required by the Jump Provider component. Disabling component.", this); enabled = false; return; } } /// /// See . /// protected virtual void OnEnable() { // Enable and disable directly serialized actions with this behavior's enabled lifecycle. m_JumpInput.EnableDirectActionIfModeUsed(); m_CurrentInAirJumpCount = m_InAirJumpCount; } /// /// See . /// protected virtual void OnDisable() { m_JumpInput.DisableDirectActionIfModeUsed(); } /// /// See /// protected virtual void Update() { CheckJump(); } /// /// Checks if the player can jump and updates the jump routine. /// void CheckJump() { if (!m_HasGravityProvider) return; if (m_CurrentJumpForgivenessWindowTime > 0f) m_CurrentJumpForgivenessWindowTime -= Time.deltaTime; // If the player has jumped and the jump input is no longer being pressed, reset the jump flag. if (m_HasJumped && m_JumpInput.ReadWasCompletedThisFrame()) m_HasJumped = false; if (!m_HasJumped && m_JumpInput.ReadIsPerformed()) Jump(); if (m_IsJumping) UpdateJump(); } /// /// Initiates the jump routine. /// /// public void Jump() { if (!CanJump()) return; if (!m_GravityProvider.isGrounded) m_CurrentInAirJumpCount--; m_HasJumped = true; m_IsJumping = true; m_CurrentJumpTimer = 0f; m_StoppingJumpTime = m_MaxJumpHoldTime; m_CurrentJumpForgivenessWindowTime = 0f; m_CurrentJumpForceThisFrame = m_JumpHeight; if (m_DisableGravityDuringJump) TryLockGravity(GravityOverride.ForcedOff); m_GravityProvider.ResetFallForce(); } /// /// Determines if the player can jump. /// /// Returns whether the player can jump. The method will not do anything while this method returns . /// /// Returns when any of these conditions are met:
/// - The player is grounded or within the jump forgiveness time (coyote time).
/// - The player has remaining in-air jumps.
///
/// public bool CanJump() { return m_UnlimitedInAirJumps || m_CurrentInAirJumpCount > 0 || m_GravityProvider.isGrounded || m_CurrentJumpForgivenessWindowTime > 0f; } /// /// Called every frame while is true. /// void UpdateJump() { var dt = Time.deltaTime; ProcessJumpForce(dt); // Calculate the jump vector based on the current gravity mode. if (m_GravityProvider.useLocalSpaceGravity) m_JumpVector = m_CurrentJumpForceThisFrame * dt * m_GravityProvider.GetCurrentUp(); else m_JumpVector.y = m_CurrentJumpForceThisFrame * dt; TryStartLocomotionImmediately(); if (locomotionState != LocomotionState.Moving) return; transformation.motion = m_JumpVector; TryQueueTransformation(transformation); } void ProcessJumpForce(float dt) { m_CurrentJumpTimer += dt; if (m_StoppingJumpTime == m_MaxJumpHoldTime && (m_MaxJumpHoldTime <= 0 || (m_VariableHeightJump && m_CurrentJumpTimer > m_MinJumpHoldTime && !m_JumpInput.ReadIsPerformed()))) { m_StoppingJumpTime = Mathf.Min(m_CurrentJumpTimer + m_EarlyOutDecelerationSpeed, m_MaxJumpHoldTime); } // Calculate the jump force based on the normalized time (0 to 1) of the jump. m_CurrentJumpForceThisFrame = CalculateJumpForceForFrame(Mathf.Clamp01(m_CurrentJumpTimer / m_StoppingJumpTime)); // If the player has reached the maximum jump time, stop the jump. if (m_CurrentJumpTimer >= m_StoppingJumpTime) StopJump(); } /// /// Calculates the jump force for the current frame based on the normalized time of the current jump. /// This function uses an approximation to convert the jump force to meters for a better UX. /// /// Normalized value between and /// The current force to be applied for a jump to reach an approximate height based float CalculateJumpForceForFrame(float normalizedJumpTime) { // Start and end jump forces for the jump height. This is used to interpolate the jump force based on the current time of the jump. var startJumpForce = 7f; var endJumpForce = 5f; // Any jump height greater than this clamp will use the endJumpForce value. var jumpHeightClamp = 4f; // Approximate the force to meters conversion based on the jump height. var approximateForceToMetersConversion = Mathf.Lerp(startJumpForce, endJumpForce, Mathf.Clamp01(m_JumpHeight / jumpHeightClamp)); // If gravity is disabled during the jump, reduce the force to meters conversion to keep the approximation within an acceptable threshold. if (m_DisableGravityDuringJump) approximateForceToMetersConversion /= 1.5f; // Calculate the jump force based on the normalized time of the jump. return (1 - normalizedJumpTime) * m_JumpHeight * approximateForceToMetersConversion; } /// /// Stops the jump routine. /// void StopJump() { m_IsJumping = false; if (m_DisableGravityDuringJump) RemoveGravityLock(); } /// /// Starts the coyote timer. /// void StartCoyoteTimer() { m_CurrentJumpForgivenessWindowTime = m_JumpForgivenessWindow; } /// /// Whether this is currently pausing gravity. /// /// /// Returns if and /// are currently , otherwise returns . /// public bool IsPausingGravity() { return m_IsJumping && m_DisableGravityDuringJump; } /// 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) { gravityPaused = false; if (!isActiveAndEnabled) return; if (!isGrounded) { // If not jumping, enable coyote time. if (!m_IsJumping) StartCoyoteTimer(); } else { m_CurrentJumpForgivenessWindowTime = 0f; // Reset the jump vector when the player is grounded. m_JumpVector = Vector3.zero; m_CurrentInAirJumpCount = m_InAirJumpCount; if (m_IsJumping) StopJump(); } } /// /// Called from when gravity lock is changed. /// /// The to apply. /// protected virtual void OnGravityLockChanged(GravityOverride gravityOverride) { if (gravityOverride == GravityOverride.ForcedOn) gravityPaused = false; } } }