using System;
using System.Collections.Generic;
using UnityEngine.Assertions;
using UnityEngine.Scripting.APIUpdating;
using UnityEngine.XR.Interaction.Toolkit.Interactors;
using UnityEngine.XR.Interaction.Toolkit.Locomotion.Gravity;
using UnityEngine.XR.Interaction.Toolkit.Utilities;
namespace UnityEngine.XR.Interaction.Toolkit.Locomotion.Climbing
{
///
/// Locomotion provider that allows the user to climb a by selecting it.
/// Climb locomotion moves the XR Origin counter to movement of the last selecting interactor, with optional
/// movement constraints along each axis of the interactable.
///
///
[AddComponentMenu("XR/Locomotion/Climb Provider", 11)]
[HelpURL(XRHelpURLConstants.k_ClimbProvider)]
[MovedFrom("UnityEngine.XR.Interaction.Toolkit")]
public class ClimbProvider : LocomotionProvider, IGravityController
{
[SerializeField]
[Tooltip("List of providers to disable while climb locomotion is active. If empty, no providers will be disabled by this component while climbing.")]
List m_ProvidersToDisable = new List();
///
/// List of providers to disable while climb locomotion is active. If empty, no providers will be disabled by this component while climbing.
///
public List providersToDisable
{
get => m_ProvidersToDisable;
set => m_ProvidersToDisable = value;
}
[SerializeField]
[Tooltip("Whether to allow falling when climb locomotion ends. Disable to pause gravity when releasing, keeping the user from falling.")]
bool m_EnableGravityOnClimbEnd = true;
///
/// Whether to allow falling when climb locomotion ends. Disable to pause gravity when releasing, keeping the user from falling.
///
public bool enableGravityOnClimbEnd
{
get => m_EnableGravityOnClimbEnd;
set => m_EnableGravityOnClimbEnd = value;
}
[SerializeField]
[Tooltip("Climb locomotion settings. Can be overridden by the Climb Interactable used for locomotion.")]
ClimbSettingsDatumProperty m_ClimbSettings = new ClimbSettingsDatumProperty(new ClimbSettings());
///
/// Climb locomotion settings. Can be overridden by the used for locomotion.
///
public ClimbSettingsDatumProperty climbSettings
{
get => m_ClimbSettings;
set => m_ClimbSettings = value;
}
///
/// The interactable that is currently grabbed and driving movement. This will be if
/// there is no active climb.
///
public ClimbInteractable climbAnchorInteractable
{
get
{
if (m_GrabbedClimbables.Count > 0)
return m_GrabbedClimbables[m_GrabbedClimbables.Count - 1];
return null;
}
}
///
/// The interactor that is currently grabbing and driving movement. This will be if
/// there is no active climb.
///
public IXRSelectInteractor climbAnchorInteractor
{
get
{
if (m_GrabbingInteractors.Count > 0)
return m_GrabbingInteractors[m_GrabbingInteractors.Count - 1];
return null;
}
}
///
/// The transformation that is used by this component to apply climb movement.
///
public XROriginMovement transformation { get; set; } = new XROriginMovement();
///
public bool canProcess => isActiveAndEnabled;
///
public bool gravityPaused { get; protected set; }
///
/// Calls the methods in its invocation list when the provider updates
/// and . This can be invoked from either or
/// . This is not invoked when climb locomotion ends.
///
public event Action climbAnchorUpdated;
///
/// The gravity provider that this component uses to apply gravity when climb locomotion is not active.
///
GravityProvider m_GravityProvider;
// These are parallel lists, where each interactor and its grabbed interactable share the same index in each list.
// The last item in each list represents the most recent selection, which is the only one that actually drives movement.
readonly List m_GrabbingInteractors = new List();
readonly List m_GrabbedClimbables = new List();
Vector3 m_InteractorAnchorWorldPosition;
Vector3 m_InteractorAnchorClimbSpacePosition;
List m_EnabledProvidersToDisable = new List();
///
protected override void Awake()
{
base.Awake();
if (m_ClimbSettings == null || m_ClimbSettings.Value == null)
m_ClimbSettings = new ClimbSettingsDatumProperty(new ClimbSettings());
ComponentLocatorUtility.TryFindComponent(out m_GravityProvider);
}
///
/// See .
///
protected virtual void Update()
{
if (!isLocomotionActive)
return;
// Use the most recent interaction to drive movement
if (m_GrabbingInteractors.Count > 0)
{
if (locomotionState == LocomotionState.Preparing)
TryStartLocomotionImmediately();
Assert.AreEqual(m_GrabbingInteractors.Count, m_GrabbedClimbables.Count);
var lastIndex = m_GrabbingInteractors.Count - 1;
var currentInteractor = m_GrabbingInteractors[lastIndex];
var currentClimbInteractable = m_GrabbedClimbables[lastIndex];
if (currentInteractor == null || currentClimbInteractable == null)
{
FinishLocomotion();
return;
}
StepClimbMovement(currentClimbInteractable, currentInteractor);
}
else
{
FinishLocomotion();
}
}
///
/// Starts a grab as part of climbing , using the position of
/// to drive movement.
///
/// The object to climb.
/// The interactor that initiates the grab and drives movement.
///
/// This puts the in the
/// state if locomotion has not already started. The phase will then enter the
/// state in the next .
///
public void StartClimbGrab(ClimbInteractable climbInteractable, IXRSelectInteractor interactor)
{
var xrOrigin = mediator.xrOrigin?.Origin;
if (xrOrigin == null)
return;
m_GrabbingInteractors.Add(interactor);
m_GrabbedClimbables.Add(climbInteractable);
UpdateClimbAnchor(climbInteractable, interactor);
TryPrepareLocomotion();
TryLockGravity(GravityOverride.ForcedOff);
foreach (var provider in m_ProvidersToDisable)
{
if (provider == null)
continue;
if (provider.enabled)
{
provider.enabled = false;
m_EnabledProvidersToDisable.Add(provider);
}
}
}
///
/// Finishes the grab driven by . If this was the most recent grab then movement
/// will now be driven by the next most recent grab.
///
/// The interactor whose grab to finish.
///
/// If there is no other active grab to fall back on, this will put the
/// in the state in the next .
///
public void FinishClimbGrab(IXRSelectInteractor interactor)
{
var interactionIndex = m_GrabbingInteractors.IndexOf(interactor);
if (interactionIndex < 0)
return;
Assert.AreEqual(m_GrabbingInteractors.Count, m_GrabbedClimbables.Count);
if (interactionIndex > 0 && interactionIndex == m_GrabbingInteractors.Count - 1)
{
// If this was the most recent grab then the interactor driving movement will change,
// so we need to update the anchor position.
var newLastIndex = interactionIndex - 1;
UpdateClimbAnchor(m_GrabbedClimbables[newLastIndex], m_GrabbingInteractors[newLastIndex]);
}
m_GrabbingInteractors.RemoveAt(interactionIndex);
m_GrabbedClimbables.RemoveAt(interactionIndex);
}
void UpdateClimbAnchor(ClimbInteractable climbInteractable, IXRInteractor interactor)
{
var climbTransform = climbInteractable.climbTransform;
m_InteractorAnchorWorldPosition = interactor.transform.position;
m_InteractorAnchorClimbSpacePosition = climbTransform.InverseTransformPoint(m_InteractorAnchorWorldPosition);
climbAnchorUpdated?.Invoke(this);
}
void StepClimbMovement(ClimbInteractable currentClimbInteractable, IXRSelectInteractor currentInteractor)
{
// Move rig such that climb interactor position stays constant
var activeClimbSettings = GetActiveClimbSettings(currentClimbInteractable);
var allowFreeXMovement = activeClimbSettings.allowFreeXMovement;
var allowFreeYMovement = activeClimbSettings.allowFreeYMovement;
var allowFreeZMovement = activeClimbSettings.allowFreeZMovement;
var interactorWorldPosition = currentInteractor.transform.position;
Vector3 movement;
if (allowFreeXMovement && allowFreeYMovement && allowFreeZMovement)
{
// No need to check position relative to climbable object if movement is unconstrained
movement = m_InteractorAnchorWorldPosition - interactorWorldPosition;
}
else
{
var climbTransform = currentClimbInteractable.climbTransform;
var interactorClimbSpacePosition = climbTransform.InverseTransformPoint(interactorWorldPosition);
var movementInClimbSpace = m_InteractorAnchorClimbSpacePosition - interactorClimbSpacePosition;
if (!allowFreeXMovement)
movementInClimbSpace.x = 0f;
if (!allowFreeYMovement)
movementInClimbSpace.y = 0f;
if (!allowFreeZMovement)
movementInClimbSpace.z = 0f;
movement = climbTransform.TransformVector(movementInClimbSpace);
}
transformation.motion = movement;
TryQueueTransformation(transformation);
}
void FinishLocomotion()
{
TryEndLocomotion();
m_GrabbingInteractors.Clear();
m_GrabbedClimbables.Clear();
RemoveGravityLock();
gravityPaused = !m_EnableGravityOnClimbEnd;
foreach (var provider in m_EnabledProvidersToDisable)
{
if (provider == null)
continue;
provider.enabled = true;
}
m_EnabledProvidersToDisable.Clear();
}
ClimbSettings GetActiveClimbSettings(ClimbInteractable climbInteractable)
{
if (climbInteractable.climbSettingsOverride.Value != null)
return climbInteractable.climbSettingsOverride;
return m_ClimbSettings;
}
///
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.
/// This is used to prevent players teleporting to the ground while climbing resulting in gravity failing to unpause.
///
protected virtual void OnGroundedChanged(bool isGrounded)
{
gravityPaused = false;
}
///
/// Called from when gravity lock is changed.
///
/// The to apply.
///
protected virtual void OnGravityLockChanged(GravityOverride gravityOverride)
{
if (gravityOverride == GravityOverride.ForcedOn)
gravityPaused = false;
}
}
}