using System;
using System.Collections.Generic;
using Unity.XR.CoreUtils;
using UnityEngine.XR.Interaction.Toolkit.Interactables;
using UnityEngine.XR.Interaction.Toolkit.Interactors;
using UnityEngine.XR.Interaction.Toolkit.Locomotion;
using UnityEngine.XR.Interaction.Toolkit.Locomotion.Teleportation;
using UnityEngine.XR.Interaction.Toolkit.Locomotion.Turning;
using UnityEngine.XR.Interaction.Toolkit.Utilities.Pooling;
namespace UnityEngine.XR.Interaction.Toolkit.Utilities
{
///
/// Use this class to maintain a list of Interactors that are potentially influenced by discontinuous locomotion
/// of the rig, such as teleportation and snap turn. Uses the events invoked by
/// components to detect locomotion.
///
///
/// Used by the XR Grab Interactable to cancel out the effect of the teleportation from its tracked velocity
/// so it does not release at unintentionally high energy. Snap turning is treated similar to a teleport
/// where the turn velocity should also be canceled out.
///
///
class TeleportationMonitor
{
class PoseContainer
{
public Pose beforePose;
public Pose afterPose;
public Pose deltaPose;
// Used to avoid repeated work capturing or calculating the poses
int m_BeforeFrame = -1;
int m_AfterFrame = -1;
int m_DeltaFrame = -1;
public void CaptureBeforePose(XRBodyTransformer bodyTransformer)
{
// If the origin pose has already been captured this frame, we don't need to do it again
// since locomotion application is only done once per frame.
var currentFrame = Time.frameCount;
if (m_BeforeFrame == currentFrame)
return;
if (!LocomotionUtility.TryGetOriginTransform(bodyTransformer, out var originTransform))
return;
m_BeforeFrame = currentFrame;
beforePose = originTransform.GetWorldPose();
}
public void CaptureAfterPose(XRBodyTransformer bodyTransformer)
{
// If the origin pose has already been captured this frame, we don't need to do it again
// since locomotion application is only done once per frame.
var currentFrame = Time.frameCount;
if (m_AfterFrame == currentFrame)
return;
if (!LocomotionUtility.TryGetOriginTransform(bodyTransformer, out var originTransform))
return;
m_AfterFrame = currentFrame;
afterPose = originTransform.GetWorldPose();
}
public void CalculateDeltaPose()
{
var currentFrame = Time.frameCount;
if (m_DeltaFrame == currentFrame)
return;
var translated = afterPose.position - beforePose.position;
var rotated = afterPose.rotation * Quaternion.Inverse(beforePose.rotation);
m_DeltaFrame = currentFrame;
deltaPose = new Pose(translated, rotated);
}
}
abstract class ProviderMonitor
{
public abstract void AddInteractor(IXRInteractor interactor);
public abstract void RemoveInteractor(IXRInteractor interactor);
///
/// The of the rig before and after locomotion.
/// Used to calculate the locomotion delta.
///
protected static Dictionary s_OriginPoses;
}
class ProviderMonitor : ProviderMonitor
where T : LocomotionProvider
{
public event Action providerStepped;
///
/// The list of interactors monitored that are influenced by locomotion.
/// Consists of those that are a child GameObject of the rig.
///
///
/// There will typically only ever be one in the scene.
///
Dictionary> m_ProviderInteractors;
///
/// References to provider instances found.
///
static List s_Providers;
static readonly LinkedPool>> s_ProviderInteractorsPool =
new LinkedPool>>(() => new Dictionary>());
public static void InitializeProvidersList()
{
if (s_Providers != null)
return;
s_Providers = new List();
foreach (var provider in LocomotionProvider.locomotionProviders)
{
if (provider == null)
continue;
if (provider is T providerT)
s_Providers.Add(providerT);
}
LocomotionProvider.locomotionProvidersChanged += OnLocomotionProvidersChanged;
return;
void OnLocomotionProvidersChanged(LocomotionProvider provider)
{
if (provider is T providerT)
s_Providers.Add(providerT);
// Prune the list as new locomotion providers are added so that it doesn't infinitely grow in size.
// It's likely if a new locomotion provider is added, the old rig with providers may have been destroyed.
s_Providers.RemoveAll(p => p == null);
}
}
public override void AddInteractor(IXRInteractor interactor)
{
if (interactor == null)
throw new ArgumentNullException(nameof(interactor));
var interactorTransform = interactor.transform;
if (interactorTransform == null)
return;
if (s_Providers == null)
{
InitializeProvidersList();
Debug.Assert(s_Providers != null);
}
foreach (var provider in s_Providers)
{
if (provider == null)
continue;
if (!LocomotionUtility.TryGetOriginTransform(provider, out var originTransform))
continue;
if (!interactorTransform.IsChildOf(originTransform))
continue;
m_ProviderInteractors ??= s_ProviderInteractorsPool.Get();
if (!m_ProviderInteractors.TryGetValue(provider, out var interactors))
{
interactors = new List();
m_ProviderInteractors.Add(provider, interactors);
}
Debug.Assert(!interactors.Contains(interactor));
interactors.Add(interactor);
if (interactors.Count == 1)
{
provider.beforeStepLocomotion += OnBeforeStepLocomotion;
provider.afterStepLocomotion += OnAfterStepLocomotion;
}
}
}
public override void RemoveInteractor(IXRInteractor interactor)
{
if (interactor == null)
throw new ArgumentNullException(nameof(interactor));
var totalInteractors = 0;
if (m_ProviderInteractors != null)
{
foreach (var kvp in m_ProviderInteractors)
{
var provider = kvp.Key;
var interactors = kvp.Value;
if (provider == null)
continue;
if (interactors.Remove(interactor) && interactors.Count == 0)
{
provider.beforeStepLocomotion -= OnBeforeStepLocomotion;
provider.afterStepLocomotion -= OnAfterStepLocomotion;
}
totalInteractors += interactors.Count;
}
}
// Release back to the pool
if (totalInteractors == 0 && m_ProviderInteractors != null)
{
s_ProviderInteractorsPool.Release(m_ProviderInteractors);
m_ProviderInteractors = null;
}
}
static void CaptureOriginPoseBefore(XRBodyTransformer bodyTransformer)
{
s_OriginPoses ??= new Dictionary();
if (!s_OriginPoses.TryGetValue(bodyTransformer, out var poseContainer))
{
poseContainer = new PoseContainer();
s_OriginPoses[bodyTransformer] = poseContainer;
}
poseContainer.CaptureBeforePose(bodyTransformer);
}
static PoseContainer CaptureOriginPoseAfter(XRBodyTransformer bodyTransformer)
{
s_OriginPoses ??= new Dictionary();
if (!s_OriginPoses.TryGetValue(bodyTransformer, out var poseContainer))
{
poseContainer = new PoseContainer();
s_OriginPoses[bodyTransformer] = poseContainer;
}
poseContainer.CaptureAfterPose(bodyTransformer);
return poseContainer;
}
static void OnBeforeStepLocomotion(LocomotionProvider provider)
{
if (provider.mediator == null)
return;
CaptureOriginPoseBefore(provider.mediator.bodyTransformer);
}
void OnAfterStepLocomotion(LocomotionProvider provider)
{
if (provider.mediator == null)
return;
var poseContainer = CaptureOriginPoseAfter(provider.mediator.bodyTransformer);
providerStepped?.Invoke(poseContainer);
}
}
///
/// Calls the methods in its invocation list when one of the Interactors monitored has been influenced by teleportation.
/// The event args represents the amount the rig was translated and rotated.
///
public event Action teleported;
int m_TeleportedFrame = -1;
ProviderMonitor[] m_Monitors;
void Initialize()
{
var teleportMonitor = new ProviderMonitor();
teleportMonitor.providerStepped += OnTeleportedAlways;
var snapMonitor = new ProviderMonitor();
snapMonitor.providerStepped += OnTeleportedAlways;
var turnMonitor = new ProviderMonitor();
turnMonitor.providerStepped += OnTeleportedTurnAround;
ProviderMonitor.InitializeProvidersList();
ProviderMonitor.InitializeProvidersList();
ProviderMonitor.InitializeProvidersList();
m_Monitors = new ProviderMonitor[] { teleportMonitor, snapMonitor, turnMonitor, };
}
///
/// Adds to monitor. If it is a child of the XR Origin,
/// will be invoked when the player teleports (or snap turns).
///
/// The Interactor to add.
///
public void AddInteractor(IXRInteractor interactor)
{
if (m_Monitors == null)
{
Initialize();
Debug.Assert(m_Monitors != null);
}
foreach (var monitor in m_Monitors)
{
monitor.AddInteractor(interactor);
}
}
///
/// Removes from monitor.
///
/// The Interactor to remove.
///
public void RemoveInteractor(IXRInteractor interactor)
{
foreach (var monitor in m_Monitors)
{
monitor.RemoveInteractor(interactor);
}
}
void OnTeleportedAlways(PoseContainer poseContainer)
{
// Ensure that the public event is only invoked once across all monitors
var currentFrame = Time.frameCount;
if (m_TeleportedFrame == currentFrame)
return;
m_TeleportedFrame = currentFrame;
poseContainer.CalculateDeltaPose();
teleported?.Invoke(poseContainer.beforePose, poseContainer.afterPose, poseContainer.deltaPose);
}
void OnTeleportedTurnAround(PoseContainer poseContainer)
{
// Ensure that the public event is only invoked once across all monitors
var currentFrame = Time.frameCount;
if (m_TeleportedFrame == currentFrame)
return;
// Only consider a Turn Around as a teleport
if (Vector3.Dot(poseContainer.beforePose.forward, poseContainer.afterPose.forward) >= 0)
return;
m_TeleportedFrame = currentFrame;
poseContainer.CalculateDeltaPose();
teleported?.Invoke(poseContainer.beforePose, poseContainer.afterPose, poseContainer.deltaPose);
}
}
}