using System; using System.Collections.Generic; using UnityEngine.XR.Interaction.Toolkit.Interactables; namespace UnityEngine.XR.Interaction.Toolkit.Utilities { /// /// Use this class to maintain a list of Colliders being touched in order to determine the set of /// Interactables that are being touched. /// /// /// This class is useful for Interactors that utilize a trigger Collider to determine which objects /// it is coming in contact with. For Interactables with multiple Colliders, this will help handle the /// bookkeeping to know if any of the colliders are still being touched. /// class TriggerContactMonitor { /// /// Calls the methods in its invocation list when an Interactable is being touched. /// /// /// Will only be fired for an Interactable once when any of the colliders associated with it are touched. /// In other words, touching more of its colliders does not cause this to fire again until all of its colliders /// are no longer being touched. /// public event Action contactAdded; /// /// Calls the methods in its invocation list when an Interactable is no longer being touched. /// /// /// Will only be fired for an Interactable once all of the colliders associated with it are no longer touched. /// In other words, leaving just one of its colliders when another one of it is still being touched /// will not fire the event. /// public event Action contactRemoved; /// /// The Interaction Manager used to fetch the Interactable associated with a Collider. /// /// public XRInteractionManager interactionManager { get; set; } readonly Dictionary m_EnteredColliders = new Dictionary(); readonly HashSet m_UnorderedInteractables = new HashSet(); readonly HashSet m_EnteredUnassociatedColliders = new HashSet(); /// /// Reusable temporary list of Collider objects for resolving unassociated colliders. /// static readonly List s_ScratchColliders = new List(); /// /// Reusable temporary list of Collider objects for removing colliders that did not stay during the frame /// but previously entered. /// static readonly List s_ExitedColliders = new List(); /// /// Adds to contact list. /// /// The Collider to add. /// public void AddCollider(Collider collider) { if (interactionManager == null) return; if (!interactionManager.TryGetInteractableForCollider(collider, out var interactable)) { m_EnteredUnassociatedColliders.Add(collider); return; } m_EnteredColliders[collider] = interactable; if (m_UnorderedInteractables.Add(interactable)) contactAdded?.Invoke(interactable); } /// /// Removes from contact list. /// /// The Collider to remove. /// public void RemoveCollider(Collider collider) { if (m_EnteredUnassociatedColliders.Remove(collider)) return; if (m_EnteredColliders.TryGetValue(collider, out var interactable)) { m_EnteredColliders.Remove(collider); if (interactable == null) return; // Don't remove the Interactable if there are still // any of its colliders touching this trigger. // Treat destroyed colliders as no longer touching. foreach (var kvp in m_EnteredColliders) { if (kvp.Value == interactable && kvp.Key != null) return; } if (m_UnorderedInteractables.Remove(interactable)) contactRemoved?.Invoke(interactable); } } /// /// Resolves all unassociated colliders to Interactables if possible. /// /// /// This process is done automatically when Colliders are added, /// but this method can be used to force a refresh. /// public void ResolveUnassociatedColliders() { // Cull destroyed colliders from the set to keep it tidy // since there would be no reason to monitor it anymore. m_EnteredUnassociatedColliders.RemoveWhere(IsDestroyed); if (m_EnteredUnassociatedColliders.Count == 0 || interactionManager == null) return; s_ScratchColliders.Clear(); foreach (var col in m_EnteredUnassociatedColliders) { if (interactionManager.TryGetInteractableForCollider(col, out var interactable)) { // Add to temporary list to remove in a second pass to avoid modifying // the collection being iterated. s_ScratchColliders.Add(col); m_EnteredColliders[col] = interactable; if (m_UnorderedInteractables.Add(interactable)) contactAdded?.Invoke(interactable); } } s_ScratchColliders.ForEach(RemoveFromUnassociatedColliders); s_ScratchColliders.Clear(); } void RemoveFromUnassociatedColliders(Collider col) => m_EnteredUnassociatedColliders.Remove(col); /// /// Resolves the unassociated colliders to if they match. /// /// The Interactable to try to associate with the unassociated colliders. /// /// This process is done automatically when Colliders are added, /// but this method can be used to force a refresh. /// public void ResolveUnassociatedColliders(IXRInteractable interactable) { // Cull destroyed colliders from the set to keep it tidy // since there would be no reason to monitor it anymore. m_EnteredUnassociatedColliders.RemoveWhere(IsDestroyed); if (m_EnteredUnassociatedColliders.Count == 0 || interactionManager == null) return; for (int index = 0, count = interactable.colliders.Count; index < count; ++index) { var col = interactable.colliders[index]; if (col == null) continue; if (m_EnteredUnassociatedColliders.Contains(col) && interactionManager.TryGetInteractableForCollider(col, out var associatedInteractable) && associatedInteractable == interactable) { m_EnteredUnassociatedColliders.Remove(col); m_EnteredColliders[col] = interactable; if (m_UnorderedInteractables.Add(interactable)) contactAdded?.Invoke(interactable); } } } /// /// Remove colliders that no longer stay during this frame but previously entered and /// adds stayed colliders that are not currently tracked. /// /// Colliders that stayed during the fixed update. /// /// Can be called in the fixed update phase by interactors after OnTriggerStay. /// public void UpdateStayedColliders(HashSet stayedColliders) { s_ExitedColliders.Clear(); if (m_EnteredColliders.Count > 0) { foreach (var collider in m_EnteredColliders.Keys) { if (!stayedColliders.Contains(collider)) // Add to temporary list to remove in a second pass to avoid modifying // the collection being iterated. s_ExitedColliders.Add(collider); else // Remove collider that is already synced stayedColliders.Remove(collider); } } if (stayedColliders.Count > 0) { foreach (var collider in stayedColliders) { // Add a stayed Collider to the entered colliders list if the // Collider is not currently tracked AddCollider(collider); } } if (s_ExitedColliders.Count > 0) { s_ExitedColliders.ForEach(RemoveCollider); s_ExitedColliders.Clear(); } } /// /// Checks whether the Interactable is being touched. /// /// The Interactable to check if touching. /// Returns if the Interactable is being touched. Otherwise, returns . public bool IsContacting(IXRInteractable interactable) { return m_UnorderedInteractables.Contains(interactable); } static bool IsDestroyed(Collider col) => col == null; } }