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