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

241 lines
10 KiB
C#

using System;
using System.Collections.Generic;
using UnityEngine.XR.Interaction.Toolkit.Interactables;
namespace UnityEngine.XR.Interaction.Toolkit.Utilities
{
/// <summary>
/// Use this class to maintain a list of Colliders being touched in order to determine the set of
/// Interactables that are being touched.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
class TriggerContactMonitor
{
/// <summary>
/// Calls the methods in its invocation list when an Interactable is being touched.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public event Action<IXRInteractable> contactAdded;
/// <summary>
/// Calls the methods in its invocation list when an Interactable is no longer being touched.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public event Action<IXRInteractable> contactRemoved;
/// <summary>
/// The Interaction Manager used to fetch the Interactable associated with a Collider.
/// </summary>
/// <seealso cref="XRInteractionManager.TryGetInteractableForCollider(Collider, out IXRInteractable)"/>
public XRInteractionManager interactionManager { get; set; }
readonly Dictionary<Collider, IXRInteractable> m_EnteredColliders = new Dictionary<Collider, IXRInteractable>();
readonly HashSet<IXRInteractable> m_UnorderedInteractables = new HashSet<IXRInteractable>();
readonly HashSet<Collider> m_EnteredUnassociatedColliders = new HashSet<Collider>();
/// <summary>
/// Reusable temporary list of Collider objects for resolving unassociated colliders.
/// </summary>
static readonly List<Collider> s_ScratchColliders = new List<Collider>();
/// <summary>
/// Reusable temporary list of Collider objects for removing colliders that did not stay during the frame
/// but previously entered.
/// </summary>
static readonly List<Collider> s_ExitedColliders = new List<Collider>();
/// <summary>
/// Adds <paramref name="collider"/> to contact list.
/// </summary>
/// <param name="collider">The Collider to add.</param>
/// <seealso cref="RemoveCollider"/>
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);
}
/// <summary>
/// Removes <paramref name="collider"/> from contact list.
/// </summary>
/// <param name="collider">The Collider to remove.</param>
/// <seealso cref="AddCollider"/>
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);
}
}
/// <summary>
/// Resolves all unassociated colliders to Interactables if possible.
/// </summary>
/// <remarks>
/// This process is done automatically when Colliders are added,
/// but this method can be used to force a refresh.
/// </remarks>
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);
/// <summary>
/// Resolves the unassociated colliders to <paramref name="interactable"/> if they match.
/// </summary>
/// <param name="interactable">The Interactable to try to associate with the unassociated colliders.</param>
/// <remarks>
/// This process is done automatically when Colliders are added,
/// but this method can be used to force a refresh.
/// </remarks>
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);
}
}
}
/// <summary>
/// Remove colliders that no longer stay during this frame but previously entered and
/// adds stayed colliders that are not currently tracked.
/// </summary>
/// <param name="stayedColliders">Colliders that stayed during the fixed update.</param>
/// <remarks>
/// Can be called in the fixed update phase by interactors after OnTriggerStay.
/// </remarks>
public void UpdateStayedColliders(HashSet<Collider> 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();
}
}
/// <summary>
/// Checks whether the Interactable is being touched.
/// </summary>
/// <param name="interactable">The Interactable to check if touching.</param>
/// <returns>Returns <see langword="true"/> if the Interactable is being touched. Otherwise, returns <see langword="false"/>.</returns>
public bool IsContacting(IXRInteractable interactable)
{
return m_UnorderedInteractables.Contains(interactable);
}
static bool IsDestroyed(Collider col) => col == null;
}
}