using System.Diagnostics; using UnityEngine.Scripting.APIUpdating; using UnityEngine.XR.Interaction.Toolkit.Interactors; using UnityEngine.XR.Interaction.Toolkit.Utilities; using UnityEngine.XR.Interaction.Toolkit.Utilities.Internal; namespace UnityEngine.XR.Interaction.Toolkit.Interactables { /// /// Utility component for supporting interactors snapping to and selecting interactables. /// Add this component to a child GameObject of the interactable. /// /// /// Currently supports one collider as the snapping volume. To support multiple snap colliders for a single interactable, /// add multiple components with each using a different collider. /// [MovedFrom("UnityEngine.XR.Interaction.Toolkit")] [AddComponentMenu("XR/XR Interactable Snap Volume", 11)] [DefaultExecutionOrder(XRInteractionUpdateOrder.k_InteractableSnapVolume)] [HelpURL(XRHelpURLConstants.k_XRInteractableSnapVolume)] public class XRInteractableSnapVolume : MonoBehaviour { [SerializeField] XRInteractionManager m_InteractionManager; /// /// The that this snap volume will communicate with (will find one if ). /// public XRInteractionManager interactionManager { get => m_InteractionManager; set { m_InteractionManager = value; if (Application.isPlaying && isActiveAndEnabled) RegisterWithInteractionManager(); } } [SerializeField] [RequireInterface(typeof(IXRInteractable))] Object m_InteractableObject; /// /// The associated with this serialized as a Unity . /// If not set, Unity will find it up the hierarchy. /// /// /// Use this for Unity Editor scripting. Use to change the interactable at runtime. /// public Object interactableObject { get => m_InteractableObject; set { m_InteractableObject = value; interactable = value as IXRInteractable; } } [SerializeField] Collider m_SnapCollider; /// /// The trigger collider to associate with the interactable when it is hit/collided. /// Rays will snap from this to the . /// /// /// This should be larger than or positioned away from the . /// Changing this value at runtime does not alter the enabled state of the previous collider. /// /// public Collider snapCollider { get => m_SnapCollider; set { if (m_SnapCollider == value) return; if (Application.isPlaying && isActiveAndEnabled) { // Update the collider to snap volume mapping. // Must wait to modify value until after unregistered since the manager currently // requires it to remove the dictionary entry. UnregisterWithInteractionManager(); m_SnapCollider = value; ValidateSnapCollider(); RefreshSnapColliderEnabled(); RegisterWithInteractionManager(); } else { m_SnapCollider = value; } } } [SerializeField] bool m_DisableSnapColliderWhenSelected = true; /// /// Automatically disable or enable the Snap Collider when the interactable is selected or deselected. /// /// /// This behavior will always automatically disable the Snap Collider when this behavior is disabled. /// public bool disableSnapColliderWhenSelected { get => m_DisableSnapColliderWhenSelected; set { m_DisableSnapColliderWhenSelected = value; if (Application.isPlaying && isActiveAndEnabled) RefreshSnapColliderEnabled(); } } [SerializeField] Collider m_SnapToCollider; /// /// (Optional) The collider that will be used to find the closest point to snap to. If this is , /// then the associated transform's position or this GameObject's transform position /// will be used as the snap point. /// /// public Collider snapToCollider { get => m_SnapToCollider; set => m_SnapToCollider = value; } IXRInteractable m_Interactable; /// /// The runtime associated with this . /// public IXRInteractable interactable { get => m_Interactable; set { m_Interactable = value; m_InteractableObject = value as Object; if (Application.isPlaying && isActiveAndEnabled) SetBoundInteractable(value); } } IXRInteractable m_BoundInteractable; IXRSelectInteractable m_BoundSelectInteractable; XRInteractionManager m_RegisteredInteractionManager; /// /// See . /// [Conditional("UNITY_EDITOR")] protected virtual void Reset() { #if UNITY_EDITOR m_InteractableObject = GetComponentInParent() as Object; m_SnapCollider = FindSnapCollider(gameObject); if (m_InteractableObject != null) { // Initialize with a Collider component on the Interactable var col = ((IXRInteractable)m_InteractableObject).transform.GetComponent(); if (col != null && col.enabled && !col.isTrigger) m_SnapToCollider = col; } #endif } /// /// See . /// protected virtual void Awake() { if (m_SnapCollider == null) m_SnapCollider = FindSnapCollider(gameObject); ValidateSnapCollider(); } /// /// See . /// protected virtual void OnEnable() { FindCreateInteractionManager(); RegisterWithInteractionManager(); // Try to find interactable in parent if necessary if (m_InteractableObject != null && m_InteractableObject is IXRInteractable serializedInteractable) interactable = serializedInteractable; else interactable = m_Interactable ??= GetComponentInParent(); } /// /// See . /// protected virtual void OnDisable() { UnregisterWithInteractionManager(); SetBoundInteractable(null); SetSnapColliderEnabled(false); } void FindCreateInteractionManager() { if (m_InteractionManager != null) return; m_InteractionManager = ComponentLocatorUtility.FindOrCreateComponent(); } void RegisterWithInteractionManager() { if (m_RegisteredInteractionManager == m_InteractionManager) return; UnregisterWithInteractionManager(); if (m_InteractionManager != null) { m_InteractionManager.RegisterSnapVolume(this); m_RegisteredInteractionManager = m_InteractionManager; } } void UnregisterWithInteractionManager() { if (m_RegisteredInteractionManager == null) return; m_RegisteredInteractionManager.UnregisterSnapVolume(this); m_RegisteredInteractionManager = null; } /// /// This method is responsible for finding a valid component available. /// available. The first valid collider found will be used as the . /// /// The to find the component for. /// The best snap collider candidate for the provided . /// /// The snap collider must be a trigger collider, so the collider type /// can only be a , , , or convex . /// /// /// protected static Collider FindSnapCollider(GameObject gameObject) { Collider bestCandidate = null; // If multiple colliders are found on this object, take the first valid one as the snap collider. var colliders = gameObject.GetComponents(); for (var index = 0; index < colliders.Length; ++index) { var colliderCandidate = colliders[index]; if (SupportsTriggerCollider(colliderCandidate)) { if (colliderCandidate.isTrigger) return colliderCandidate; if (bestCandidate == null) bestCandidate = colliderCandidate; } } return bestCandidate; } /// /// Returns whether the given collider supports enabling . /// /// The collider to check. /// Returns if the collider supports being a trigger collider. internal static bool SupportsTriggerCollider(Collider col) { return col is BoxCollider || col is SphereCollider || col is CapsuleCollider || col is MeshCollider { convex: true }; } void ValidateSnapCollider() { if (m_SnapCollider == null) { Debug.LogWarning("XR Interactable Snap Volume is missing a Snap Collider assignment.", this); } else if (!SupportsTriggerCollider(m_SnapCollider)) { Debug.LogError("Snap Collider is set to a collider which does not support being a trigger collider." + " Set it to a Box Collider, Sphere Collider, Capsule Collider, or convex Mesh Collider.", this); } else if (!m_SnapCollider.isTrigger) { Debug.LogWarning($"Snap Collider must be trigger collider, updating {m_SnapCollider}.", this); m_SnapCollider.isTrigger = true; } } /// /// Enables or disables the snap volume collider. /// /// Whether to enable if currently set. void SetSnapColliderEnabled(bool enable) { if (m_SnapCollider != null) m_SnapCollider.enabled = enable; } /// /// Tries to get the closest point on the associated snapping collider. If is null, /// it will return the transform position of the associated . If both /// and are null, it will return the transform /// position of this GameObject. /// /// The point for which we are trying to find the the nearest point on the . /// The closest point on the if possible. Defaults to the /// transform position if available, or the transform position of this GameObject. public Vector3 GetClosestPoint(Vector3 point) { if (m_SnapToCollider == null || !m_SnapToCollider.gameObject.activeInHierarchy || !m_SnapToCollider.enabled) { var interactableValid = m_Interactable != null && (!(m_Interactable is Object unityObject) || unityObject != null); return interactableValid ? m_Interactable.transform.position : transform.position; } return m_SnapToCollider.ClosestPoint(point); } /// /// Tries to get the closest point on the associated snapping collider based on the attach transform position /// of the associated . If is null, it will return the /// attach transform position of the associated . If is /// also null in that case, it will return the transform position of this GameObject. /// /// The interacting with the used to get the attach transform. /// The closest point on the if possible. Defaults to the /// attach transform position of the associated if available, or the transform position of this GameObject. public Vector3 GetClosestPointOfAttachTransform(IXRInteractor interactor) { var interactableValid = m_Interactable != null && (!(m_Interactable is Object unityObject) || unityObject != null); var point = interactableValid ? m_Interactable.GetAttachTransform(interactor).position : transform.position; if (m_SnapToCollider == null || !m_SnapToCollider.gameObject.activeInHierarchy || !m_SnapToCollider.enabled) return point; return m_SnapToCollider.ClosestPoint(point); } void SetBoundInteractable(IXRInteractable source) { Debug.Assert(Application.isPlaying); if (m_BoundInteractable == source) return; if (m_BoundSelectInteractable != null) { m_BoundSelectInteractable.firstSelectEntered.RemoveListener(OnFirstSelectEntered); m_BoundSelectInteractable.lastSelectExited.RemoveListener(OnLastSelectExited); } m_BoundInteractable = source; m_BoundSelectInteractable = source as IXRSelectInteractable; if (m_BoundSelectInteractable != null) { m_BoundSelectInteractable.firstSelectEntered.AddListener(OnFirstSelectEntered); m_BoundSelectInteractable.lastSelectExited.AddListener(OnLastSelectExited); } // Refresh the snap collider enabled state (which is what the callbacks do) RefreshSnapColliderEnabled(); } void RefreshSnapColliderEnabled() { var isSelected = m_BoundSelectInteractable != null && m_BoundSelectInteractable.isSelected; if (m_DisableSnapColliderWhenSelected) SetSnapColliderEnabled(!isSelected); else SetSnapColliderEnabled(true); } void OnFirstSelectEntered(SelectEnterEventArgs args) { if (m_DisableSnapColliderWhenSelected) SetSnapColliderEnabled(false); } void OnLastSelectExited(SelectExitEventArgs args) { if (m_DisableSnapColliderWhenSelected) SetSnapColliderEnabled(true); } } }