VR4Medical/ICI/Library/PackageCache/com.unity.xr.interaction.toolkit@42ef3600567b/Runtime/Locomotion/Jump/JumpProvider.cs
2025-07-29 13:45:50 +03:00

491 lines
18 KiB
C#

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
{
/// <summary>
/// Jump Provider allows the player to jump in the scene.
/// This uses a jump force to drive the value of the <see cref="jumpHeight"/> 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.
/// </summary>
[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;
/// <summary>
/// Disable gravity during the jump. This will result in a more floaty jump.
/// </summary>
public bool disableGravityDuringJump
{
get => m_DisableGravityDuringJump;
set => m_DisableGravityDuringJump = value;
}
[SerializeField]
[Tooltip("Allow player to jump without being grounded.")]
bool m_UnlimitedInAirJumps;
/// <summary>
/// Allow player to jump without being grounded.
/// </summary>
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;
/// <summary>
/// The number of times a player can jump before landing.
/// </summary>
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;
/// <summary>
/// The time window after leaving the ground that a jump can still be performed. Sometimes known as coyote time.
/// </summary>
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;
/// <summary>
/// The height (approximately in meters) the player will be when reaching the apex of the jump.
/// </summary>
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;
/// <summary>
/// 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.
/// </summary>
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;
/// <summary>
/// The minimum amount of time the jump will execute for.
/// </summary>
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;
/// <summary>
/// The maximum time a player can hold down the jump button to increase altitude.
/// </summary>
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;
/// <summary>
/// The speed at which the jump will decelerate when the player releases the jump button early.
/// </summary>
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");
/// <summary>
/// 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 <see cref="ButtonControl"/> Control or a Value type action with a Press interaction.
/// </summary>
public XRInputButtonReader jumpInput
{
get => m_JumpInput;
set => XRInputReaderUtility.SetInputProperty(ref m_JumpInput, value, this);
}
/// <summary>
/// The transformation that is used by this component to apply translation movement.
/// </summary>
public XROriginMovement transformation { get; set; } = new XROriginMovement();
bool m_IsJumping;
/// <summary>
/// Returns whether the player is currently jumping.
/// </summary>
/// <remarks>
/// This is only <see langword="true"/> during jump ascent, it is not <see langword="true"/> during the descent.
/// </remarks>
public bool isJumping => m_IsJumping;
/// <inheritdoc />
public bool canProcess => isActiveAndEnabled;
/// <inheritdoc/>
public bool gravityPaused { get; protected set; }
/// <summary>
/// Flag to make sure the player has released the jump button before allowing another jump.
/// </summary>
bool m_HasJumped;
/// <summary>
/// The current jump forgiveness time.
/// </summary>
float m_CurrentJumpForgivenessWindowTime;
/// <summary>
/// The time the player will stop at. Usually it's <see cref="maxJumpHoldTime"/>, but can change when using <see cref="variableHeightJump"/>.
/// </summary>
float m_StoppingJumpTime;
/// <summary>
/// The current jump force being applied this frame.
/// </summary>
float m_CurrentJumpForceThisFrame;
/// <summary>
/// Current jump vector being applied to the player.
/// </summary>
Vector3 m_JumpVector;
/// <summary>
/// Reference to the gravity provider.
/// </summary>
GravityProvider m_GravityProvider;
bool m_HasGravityProvider;
float m_CurrentJumpTimer;
int m_CurrentInAirJumpCount;
/// <summary>
/// See <see cref="MonoBehaviour"/>.
/// </summary>
protected virtual void OnValidate()
{
m_InAirJumpCount = Mathf.Max(0, m_InAirJumpCount);
}
/// <inheritdoc />
protected override void Awake()
{
base.Awake();
m_HasGravityProvider = ComponentLocatorUtility<GravityProvider>.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;
}
}
/// <summary>
/// See <see cref="MonoBehaviour"/>.
/// </summary>
protected virtual void OnEnable()
{
// Enable and disable directly serialized actions with this behavior's enabled lifecycle.
m_JumpInput.EnableDirectActionIfModeUsed();
m_CurrentInAirJumpCount = m_InAirJumpCount;
}
/// <summary>
/// See <see cref="MonoBehaviour"/>.
/// </summary>
protected virtual void OnDisable()
{
m_JumpInput.DisableDirectActionIfModeUsed();
}
/// <summary>
/// See <see cref="MonoBehaviour"/>
/// </summary>
protected virtual void Update()
{
CheckJump();
}
/// <summary>
/// Checks if the player can jump and updates the jump routine.
/// </summary>
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();
}
/// <summary>
/// Initiates the jump routine.
/// </summary>
/// <seealso cref="CanJump"/>
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();
}
/// <summary>
/// Determines if the player can jump.
/// </summary>
/// <returns>Returns whether the player can jump. The <see cref="Jump"/> method will not do anything while this method returns <see langword="false"/>.</returns>
/// <remarks>
/// Returns <see langword="true"/> when any of these conditions are met:<br/>
/// - The player is grounded or within the jump forgiveness time (coyote time).<br/>
/// - The player has remaining in-air jumps.<br/>
/// </remarks>
/// <seealso cref="Jump"/>
public bool CanJump()
{
return m_UnlimitedInAirJumps || m_CurrentInAirJumpCount > 0 || m_GravityProvider.isGrounded || m_CurrentJumpForgivenessWindowTime > 0f;
}
/// <summary>
/// Called every frame while <see cref="isJumping"/> is true.
/// </summary>
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();
}
/// <summary>
/// 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.
/// </summary>
/// <param name="normalizedJumpTime">Normalized value between <see cref="m_CurrentJumpTimer"/> and <see cref="m_StoppingJumpTime"/></param>
/// <returns>The current force to be applied for a jump to reach an approximate height based <see cref="m_JumpHeight"/></returns>
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;
}
/// <summary>
/// Stops the jump routine.
/// </summary>
void StopJump()
{
m_IsJumping = false;
if (m_DisableGravityDuringJump)
RemoveGravityLock();
}
/// <summary>
/// Starts the coyote timer.
/// </summary>
void StartCoyoteTimer()
{
m_CurrentJumpForgivenessWindowTime = m_JumpForgivenessWindow;
}
/// <summary>
/// Whether this <see cref="JumpProvider"/> is currently pausing gravity.
/// </summary>
/// <returns>
/// Returns <see langword="true"/> if <see cref="isJumping"/> and <see cref="disableGravityDuringJump"/>
/// are currently <see langword="true"/>, otherwise returns <see langword="false"/>.
/// </returns>
public bool IsPausingGravity()
{
return m_IsJumping && m_DisableGravityDuringJump;
}
/// <inheritdoc/>
public bool TryLockGravity(GravityOverride gravityOverride)
{
if (m_GravityProvider != null)
return m_GravityProvider.TryLockGravity(this, gravityOverride);
return false;
}
/// <inheritdoc/>
public void RemoveGravityLock()
{
if (m_GravityProvider != null)
m_GravityProvider.UnlockGravity(this);
}
/// <inheritdoc />
void IGravityController.OnGroundedChanged(bool isGrounded) => OnGroundedChanged(isGrounded);
/// <inheritdoc />
void IGravityController.OnGravityLockChanged(GravityOverride gravityOverride) => OnGravityLockChanged(gravityOverride);
/// <summary>
/// Called from <see cref="GravityProvider"/> when the grounded state changes.
/// </summary>
/// <param name="isGrounded">Whether the player is on the ground.</param>
/// <seealso cref="GravityProvider.onGroundedChanged"/>
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();
}
}
/// <summary>
/// Called from <see cref="GravityProvider.TryLockGravity"/> when gravity lock is changed.
/// </summary>
/// <param name="gravityOverride">The <see cref="GravityOverride"/> to apply.</param>
/// <seealso cref="GravityProvider.onGravityLockChanged"/>
protected virtual void OnGravityLockChanged(GravityOverride gravityOverride)
{
if (gravityOverride == GravityOverride.ForcedOn)
gravityPaused = false;
}
}
}