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