using System; using System.Collections.Generic; using System.Diagnostics; using Unity.Profiling; using Unity.XR.CoreUtils; using Unity.XR.CoreUtils.Bindings.Variables; using Unity.XR.CoreUtils.Collections; using UnityEngine.Scripting.APIUpdating; using UnityEngine.XR.Interaction.Toolkit.Filtering; using UnityEngine.XR.Interaction.Toolkit.Gaze; using UnityEngine.XR.Interaction.Toolkit.Interactables.Visuals; using UnityEngine.XR.Interaction.Toolkit.Interactors; using UnityEngine.XR.Interaction.Toolkit.Interactors.Visuals; using UnityEngine.XR.Interaction.Toolkit.Utilities; using UnityEngine.XR.Interaction.Toolkit.Utilities.Internal; namespace UnityEngine.XR.Interaction.Toolkit.Interactables { /// /// Abstract base class from which all interactable behaviors derive. /// This class hooks into the interaction system (via ) and provides base virtual methods for handling /// hover, selection, and focus. /// [MovedFrom("UnityEngine.XR.Interaction.Toolkit")] [SelectionBase] [DefaultExecutionOrder(XRInteractionUpdateOrder.k_Interactables)] public abstract partial class XRBaseInteractable : MonoBehaviour, IXRActivateInteractable, IXRHoverInteractable, IXRSelectInteractable, IXRFocusInteractable, IXRInteractionStrengthInteractable, IXROverridesGazeAutoSelect { const float k_InteractionStrengthHover = 0f; const float k_InteractionStrengthSelect = 1f; /// /// Options for how to process and perform movement of an Interactable. /// /// /// Each method of movement has tradeoffs, and different values may be more appropriate /// for each type of Interactable object in a project. /// /// public enum MovementType { #if UNITY_2023_3_OR_NEWER // Change between Rigidbody.linearVelocity and Rigidbody.velocity /// /// Move the Interactable object by setting the velocity and angular velocity of the Rigidbody. /// Use this if you don't want the object to be able to move through other Colliders without a Rigidbody /// as it follows the Interactor, however with the tradeoff that it can appear to lag behind /// and not move as smoothly as . /// /// /// Unity sets the velocity values during the FixedUpdate function. This Interactable will move at the /// framerate-independent interval of the Physics update, which may be slower than the Update rate. /// If the Rigidbody is not set to use interpolation or extrapolation, as the Interactable /// follows the Interactor, it may not visually update position each frame and be a slight distance /// behind the Interactor or controller due to the difference between the Physics update rate /// and the render update rate. /// /// /// #else /// /// Move the Interactable object by setting the velocity and angular velocity of the Rigidbody. /// Use this if you don't want the object to be able to move through other Colliders without a Rigidbody /// as it follows the Interactor, however with the tradeoff that it can appear to lag behind /// and not move as smoothly as . /// /// /// Unity sets the velocity values during the FixedUpdate function. This Interactable will move at the /// framerate-independent interval of the Physics update, which may be slower than the Update rate. /// If the Rigidbody is not set to use interpolation or extrapolation, as the Interactable /// follows the Interactor, it may not visually update position each frame and be a slight distance /// behind the Interactor or controller due to the difference between the Physics update rate /// and the render update rate. /// /// /// #endif VelocityTracking, /// /// Move the Interactable object by moving the kinematic Rigidbody towards the target position and orientation. /// Use this if you want to keep the visual representation synchronized to match its Physics state, /// and if you want to allow the object to be able to move through other Colliders without a Rigidbody /// as it follows the Interactor. /// /// /// Unity will call the movement methods during the FixedUpdate function. This Interactable will move at the /// framerate-independent interval of the Physics update, which may be slower than the Update rate. /// If the Rigidbody is not set to use interpolation or extrapolation, as the Interactable /// follows the Interactor, it may not visually update position each frame and be a slight distance /// behind the Interactor or controller due to the difference between the Physics update rate /// and the render update rate. Collisions will be more accurate as compared to /// since with this method, the Rigidbody will be moved by settings its internal velocity rather than /// instantly teleporting to match the Transform pose. /// /// /// Kinematic, /// /// Move the Interactable object by setting the position and rotation of the Transform every frame. /// Use this if you want the visual representation to be updated each frame, minimizing latency, /// however with the tradeoff that it will be able to move through other Colliders without a Rigidbody /// as it follows the Interactor. /// /// /// Unity will set the Transform values each frame, which may be faster than the framerate-independent /// interval of the Physics update. The Collider of the Interactable object may be a slight distance /// behind the visual as it follows the Interactor due to the difference between the Physics update rate /// and the render update rate. Collisions will not be computed as accurately as /// since with this method, the Rigidbody will be forced to instantly teleport poses to match the Transform pose /// rather than moving the Rigidbody through setting its internal velocity. /// /// /// Instantaneous, } /// /// Options for how to calculate an Interactable distance to a location in world space. /// /// public enum DistanceCalculationMode { /// /// Calculates the distance using the Interactable's transform position. /// This option has low performance cost, but it may have low distance calculation accuracy for some objects. /// TransformPosition, /// /// Calculates the distance using the Interactable's colliders list using the shortest distance to each. /// This option has moderate performance cost and should have moderate distance calculation accuracy for most objects. /// /// ColliderPosition, /// /// Calculates the distance using the Interactable's colliders list using the shortest distance to the closest point of each /// (either on the surface or inside the Collider). /// This option has high performance cost but high distance calculation accuracy. /// /// /// The Interactable's colliders can only be of type , , , or convex . /// /// /// ColliderVolume, } /// public event Action registered; /// public event Action unregistered; /// /// Overriding callback of this object's distance calculation. /// Use this to change the calculation performed in without needing to create a derived class. ///
/// When a callback is assigned to this property, the execution calls it to perform the /// distance calculation instead of using its default calculation (specified by in this base class). /// Assign to this property to restore the default calculation. ///
/// /// The assigned callback will be invoked to calculate and return the distance information of the point on this /// Interactable (the first parameter) closest to the given location (the second parameter). /// The given location and returned distance information are in world space. /// /// /// public Func getDistanceOverride { get; set; } [SerializeField] XRInteractionManager m_InteractionManager; /// /// The that this Interactable will communicate with (will find one if ). /// public XRInteractionManager interactionManager { get => m_InteractionManager; set { m_InteractionManager = value; if (Application.isPlaying && isActiveAndEnabled) RegisterWithInteractionManager(); } } [SerializeField] #pragma warning disable IDE0044 // Add readonly modifier -- readonly fields cannot be serialized by Unity List m_Colliders = new List(); #pragma warning restore IDE0044 /// /// (Read Only) Colliders to use for interaction with this Interactable (if empty, will use any child Colliders). /// public List colliders => m_Colliders; [SerializeField] InteractionLayerMask m_InteractionLayers = 1; /// /// Allows interaction with Interactors whose Interaction Layer Mask overlaps with any Layer in this Interaction Layer Mask. /// /// /// /// /// public InteractionLayerMask interactionLayers { get => m_InteractionLayers; set => m_InteractionLayers = value; } [SerializeField] DistanceCalculationMode m_DistanceCalculationMode = DistanceCalculationMode.ColliderPosition; /// /// Specifies how this Interactable calculates its distance to a location, either using its Transform position, Collider /// position or Collider volume. /// /// /// /// public DistanceCalculationMode distanceCalculationMode { get => m_DistanceCalculationMode; set => m_DistanceCalculationMode = value; } [SerializeField] InteractableSelectMode m_SelectMode = InteractableSelectMode.Single; /// public InteractableSelectMode selectMode { get => m_SelectMode; set => m_SelectMode = value; } [SerializeField] InteractableFocusMode m_FocusMode = InteractableFocusMode.Single; /// public InteractableFocusMode focusMode { get => m_FocusMode; set => m_FocusMode = value; } [SerializeField] GameObject m_CustomReticle; /// /// The reticle that appears at the end of the line when valid. /// public GameObject customReticle { get => m_CustomReticle; set => m_CustomReticle = value; } [SerializeField] bool m_AllowGazeInteraction; /// /// Enables interaction with . /// public bool allowGazeInteraction { get => m_AllowGazeInteraction; set => m_AllowGazeInteraction = value; } [SerializeField] bool m_AllowGazeSelect; /// /// Enables to select this . /// /// public bool allowGazeSelect { get => m_AllowGazeSelect; set => m_AllowGazeSelect = value; } [SerializeField] bool m_OverrideGazeTimeToSelect; /// public bool overrideGazeTimeToSelect { get => m_OverrideGazeTimeToSelect; set => m_OverrideGazeTimeToSelect = value; } [SerializeField] float m_GazeTimeToSelect = 0.5f; /// public float gazeTimeToSelect { get => m_GazeTimeToSelect; set => m_GazeTimeToSelect = value; } [SerializeField] bool m_OverrideTimeToAutoDeselectGaze; /// public bool overrideTimeToAutoDeselectGaze { get => m_OverrideTimeToAutoDeselectGaze; set => m_OverrideTimeToAutoDeselectGaze = value; } [SerializeField] float m_TimeToAutoDeselectGaze = 3f; /// public float timeToAutoDeselectGaze { get => m_TimeToAutoDeselectGaze; set => m_TimeToAutoDeselectGaze = value; } [SerializeField] bool m_AllowGazeAssistance; /// /// Enables gaze assistance with this interactable. /// public bool allowGazeAssistance { get => m_AllowGazeAssistance; set => m_AllowGazeAssistance = value; } [SerializeField] HoverEnterEvent m_FirstHoverEntered = new HoverEnterEvent(); /// public HoverEnterEvent firstHoverEntered { get => m_FirstHoverEntered; set => m_FirstHoverEntered = value; } [SerializeField] HoverExitEvent m_LastHoverExited = new HoverExitEvent(); /// public HoverExitEvent lastHoverExited { get => m_LastHoverExited; set => m_LastHoverExited = value; } [SerializeField] HoverEnterEvent m_HoverEntered = new HoverEnterEvent(); /// public HoverEnterEvent hoverEntered { get => m_HoverEntered; set => m_HoverEntered = value; } [SerializeField] HoverExitEvent m_HoverExited = new HoverExitEvent(); /// public HoverExitEvent hoverExited { get => m_HoverExited; set => m_HoverExited = value; } [SerializeField] SelectEnterEvent m_FirstSelectEntered = new SelectEnterEvent(); /// public SelectEnterEvent firstSelectEntered { get => m_FirstSelectEntered; set => m_FirstSelectEntered = value; } [SerializeField] SelectExitEvent m_LastSelectExited = new SelectExitEvent(); /// public SelectExitEvent lastSelectExited { get => m_LastSelectExited; set => m_LastSelectExited = value; } [SerializeField] SelectEnterEvent m_SelectEntered = new SelectEnterEvent(); /// public SelectEnterEvent selectEntered { get => m_SelectEntered; set => m_SelectEntered = value; } [SerializeField] SelectExitEvent m_SelectExited = new SelectExitEvent(); /// public SelectExitEvent selectExited { get => m_SelectExited; set => m_SelectExited = value; } [SerializeField] FocusEnterEvent m_FirstFocusEntered = new FocusEnterEvent(); /// public FocusEnterEvent firstFocusEntered { get => m_FirstFocusEntered; set => m_FirstFocusEntered = value; } [SerializeField] FocusExitEvent m_LastFocusExited = new FocusExitEvent(); /// public FocusExitEvent lastFocusExited { get => m_LastFocusExited; set => m_LastFocusExited = value; } [SerializeField] FocusEnterEvent m_FocusEntered = new FocusEnterEvent(); /// public FocusEnterEvent focusEntered { get => m_FocusEntered; set => m_FocusEntered = value; } [SerializeField] FocusExitEvent m_FocusExited = new FocusExitEvent(); /// public FocusExitEvent focusExited { get => m_FocusExited; set => m_FocusExited = value; } [SerializeField] ActivateEvent m_Activated = new ActivateEvent(); /// public ActivateEvent activated { get => m_Activated; set => m_Activated = value; } [SerializeField] DeactivateEvent m_Deactivated = new DeactivateEvent(); /// public DeactivateEvent deactivated { get => m_Deactivated; set => m_Deactivated = value; } readonly HashSetList m_InteractorsHovering = new HashSetList(); /// public List interactorsHovering => (List)m_InteractorsHovering.AsList(); /// public bool isHovered { get; private set; } readonly HashSetList m_InteractorsSelecting = new HashSetList(); /// public List interactorsSelecting => (List)m_InteractorsSelecting.AsList(); /// public IXRSelectInteractor firstInteractorSelecting { get; private set; } /// public bool isSelected { get; private set; } readonly HashSetList m_InteractionGroupsFocusing = new HashSetList(); /// public List interactionGroupsFocusing => (List)m_InteractionGroupsFocusing.AsList(); /// public IXRInteractionGroup firstInteractionGroupFocusing { get; private set; } /// public bool isFocused { get; private set; } /// public bool canFocus => m_FocusMode != InteractableFocusMode.None; [SerializeField] [RequireInterface(typeof(IXRHoverFilter))] List m_StartingHoverFilters = new List(); /// /// The hover filters that this object uses to automatically populate the List at /// startup (optional, may be empty). /// All objects in this list should implement the interface. /// /// /// To access and modify the hover filters used after startup, the List should /// be used instead. /// /// public List startingHoverFilters { get => m_StartingHoverFilters; set => m_StartingHoverFilters = value; } readonly ExposedRegistrationList m_HoverFilters = new ExposedRegistrationList { bufferChanges = false }; /// /// The list of hover filters in this object. /// Used as additional hover validations for this Interactable. /// /// /// While processing hover filters, all changes to this list don't have an immediate effect. These changes are /// buffered and applied when the processing is finished. /// Calling in this list will throw an exception when this list is being processed. /// /// public IXRFilterList hoverFilters => m_HoverFilters; [SerializeField] [RequireInterface(typeof(IXRSelectFilter))] List m_StartingSelectFilters = new List(); /// /// The select filters that this object uses to automatically populate the List at /// startup (optional, may be empty). /// All objects in this list should implement the interface. /// /// /// To access and modify the select filters used after startup, the List should /// be used instead. /// /// public List startingSelectFilters { get => m_StartingSelectFilters; set => m_StartingSelectFilters = value; } readonly ExposedRegistrationList m_SelectFilters = new ExposedRegistrationList { bufferChanges = false }; /// /// The list of select filters in this object. /// Used as additional select validations for this Interactable. /// /// /// While processing select filters, all changes to this list don't have an immediate effect. These changes are /// buffered and applied when the processing is finished. /// Calling in this list will throw an exception when this list is being processed. /// /// public IXRFilterList selectFilters => m_SelectFilters; [SerializeField] [RequireInterface(typeof(IXRInteractionStrengthFilter))] List m_StartingInteractionStrengthFilters = new List(); /// /// The interaction strength filters that this object uses to automatically populate the List at /// startup (optional, may be empty). /// All objects in this list should implement the interface. /// /// /// To access and modify the select filters used after startup, the List should /// be used instead. /// /// public List startingInteractionStrengthFilters { get => m_StartingInteractionStrengthFilters; set => m_StartingInteractionStrengthFilters = value; } readonly ExposedRegistrationList m_InteractionStrengthFilters = new ExposedRegistrationList { bufferChanges = false }; /// /// The list of interaction strength filters in this object. /// Used to modify the default interaction strength of an Interactor relative to this Interactable. /// This is useful for interactables that can be poked to report the depth of the poke interactor as a percentage /// while the poke interactor is hovering over this object. /// /// /// While processing interaction strength filters, all changes to this list don't have an immediate effect. These changes are /// buffered and applied when the processing is finished. /// Calling in this list will throw an exception when this list is being processed. /// /// public IXRFilterList interactionStrengthFilters => m_InteractionStrengthFilters; readonly BindableVariable m_LargestInteractionStrength = new BindableVariable(); /// public IReadOnlyBindableVariable largestInteractionStrength => m_LargestInteractionStrength; bool m_ClearedLargestInteractionStrength; readonly Dictionary m_AttachPoseOnSelect = new Dictionary(); readonly Dictionary m_LocalAttachPoseOnSelect = new Dictionary(); readonly Dictionary m_ReticleCache = new Dictionary(); /// /// The set of hovered and/or selected interactors that supports returning a variable select input value, /// which is used as the pre-filtered interaction strength. /// /// /// Uses as the type to get the select input value to use as the pre-filtered /// interaction strength. /// readonly HashSetList m_VariableSelectInteractors = new HashSetList(); readonly Dictionary m_InteractionStrengths = new Dictionary(); XRInteractionManager m_RegisteredInteractionManager; static readonly ProfilerMarker s_ProcessInteractionStrengthMarker = new ProfilerMarker("XRI.ProcessInteractionStrength.Interactables"); static readonly ProfilerMarker s_ProcessInteractionStrengthEventMarker = new ProfilerMarker("XRI.ProcessInteractionStrength.InteractablesEvent"); /// /// See . /// [Conditional("UNITY_EDITOR")] protected virtual void Reset() { #if UNITY_EDITOR // Don't need to do anything; method kept for backwards compatibility. #endif } /// /// See . /// protected virtual void Awake() { // If no colliders were set, populate with children colliders if (m_Colliders.Count == 0) { GetComponentsInChildren(m_Colliders); // Skip any that are trigger colliders since these are usually associated with snap volumes. // If a user wants to use a trigger collider, they must serialize the reference manually. m_Colliders.RemoveAll(col => col.isTrigger); } // Setup the starting filters m_HoverFilters.RegisterReferences(m_StartingHoverFilters, this); m_SelectFilters.RegisterReferences(m_StartingSelectFilters, this); m_InteractionStrengthFilters.RegisterReferences(m_StartingInteractionStrengthFilters, this); // Setup Interaction Manager FindCreateInteractionManager(); } /// /// See . /// protected virtual void OnEnable() { FindCreateInteractionManager(); RegisterWithInteractionManager(); } /// /// See . /// protected virtual void OnDisable() { UnregisterWithInteractionManager(); } /// /// See . /// protected virtual void OnDestroy() { // Don't need to do anything; method kept for backwards compatibility. } 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.RegisterInteractable((IXRInteractable)this); m_RegisteredInteractionManager = m_InteractionManager; } } void UnregisterWithInteractionManager() { if (m_RegisteredInteractionManager != null) { m_RegisteredInteractionManager.UnregisterInteractable((IXRInteractable)this); m_RegisteredInteractionManager = null; } } /// public virtual Transform GetAttachTransform(IXRInteractor interactor) { return transform; } /// public Pose GetAttachPoseOnSelect(IXRSelectInteractor interactor) { return m_AttachPoseOnSelect.TryGetValue(interactor, out var pose) ? pose : Pose.identity; } /// public Pose GetLocalAttachPoseOnSelect(IXRSelectInteractor interactor) { return m_LocalAttachPoseOnSelect.TryGetValue(interactor, out var pose) ? pose : Pose.identity; } /// /// /// This method calls the method to perform the distance calculation. /// public virtual float GetDistanceSqrToInteractor(IXRInteractor interactor) { var interactorAttachTransform = interactor?.GetAttachTransform(this); if (interactorAttachTransform == null) return float.MaxValue; var interactorPosition = interactorAttachTransform.position; var distanceInfo = GetDistance(interactorPosition); return distanceInfo.distanceSqr; } /// /// Gets the distance from this Interactable to the given location. /// This method uses the calculation mode configured in . ///
/// This method can be overridden (without needing to subclass) by assigning a callback to . /// To restore the previous calculation mode configuration, assign to . ///
/// Location in world space to calculate the distance to. /// Returns the distance information (in world space) from this Interactable to the given location. /// /// This method is used by other methods and systems to calculate this Interactable distance to other objects and /// locations (). /// public virtual DistanceInfo GetDistance(Vector3 position) { if (getDistanceOverride != null) return getDistanceOverride(this, position); switch (m_DistanceCalculationMode) { case DistanceCalculationMode.TransformPosition: var thisObjectPosition = transform.position; var offset = thisObjectPosition - position; var distanceInfo = new DistanceInfo { point = thisObjectPosition, distanceSqr = offset.sqrMagnitude }; return distanceInfo; case DistanceCalculationMode.ColliderPosition: XRInteractableUtility.TryGetClosestCollider(this, position, out distanceInfo); return distanceInfo; case DistanceCalculationMode.ColliderVolume: XRInteractableUtility.TryGetClosestPointOnCollider(this, position, out distanceInfo); return distanceInfo; default: Debug.Assert(false, $"Unhandled {nameof(DistanceCalculationMode)}={m_DistanceCalculationMode}.", this); goto case DistanceCalculationMode.TransformPosition; } } /// public float GetInteractionStrength(IXRInteractor interactor) { if (m_InteractionStrengths.TryGetValue(interactor, out var interactionStrength)) return interactionStrength; return 0f; } /// /// Determines if a given Interactor can hover over this Interactable. /// /// Interactor to check for a valid hover state with. /// Returns if hovering is valid this frame. Returns if not. /// public virtual bool IsHoverableBy(IXRHoverInteractor interactor) { return m_AllowGazeInteraction || !(interactor is XRGazeInteractor); } /// /// Determines if a given Interactor can select this Interactable. /// /// Interactor to check for a valid selection with. /// Returns if selection is valid this frame. Returns if not. /// public virtual bool IsSelectableBy(IXRSelectInteractor interactor) { return (m_AllowGazeInteraction && m_AllowGazeSelect) || !(interactor is XRGazeInteractor); } /// /// Determines whether this Interactable is currently being hovered by the Interactor. /// /// Interactor to check. /// Returns if this Interactable is currently being hovered by the Interactor. /// Otherwise, returns . /// /// In other words, returns whether contains . /// /// public bool IsHovered(IXRHoverInteractor interactor) => isHovered && m_InteractorsHovering.Contains(interactor); /// /// Determines whether this Interactable is currently being selected by the Interactor. /// /// Interactor to check. /// Returns if this Interactable is currently being selected by the Interactor. /// Otherwise, returns . /// /// In other words, returns whether contains . /// /// public bool IsSelected(IXRSelectInteractor interactor) => isSelected && m_InteractorsSelecting.Contains(interactor); /// /// Determines whether this Interactable is currently being hovered by the Interactor. /// /// Interactor to check. /// Returns if this Interactable is currently being hovered by the Interactor. /// Otherwise, returns . /// /// In other words, returns whether contains . /// /// /// protected bool IsHovered(IXRInteractor interactor) => interactor is IXRHoverInteractor hoverInteractor && IsHovered(hoverInteractor); /// /// Determines whether this Interactable is currently being selected by the Interactor. /// /// Interactor to check. /// Returns if this Interactable is currently being selected by the Interactor. /// Otherwise, returns . /// /// In other words, returns whether contains . /// /// /// protected bool IsSelected(IXRInteractor interactor) => interactor is IXRSelectInteractor selectInteractor && IsSelected(selectInteractor); /// /// Looks for the current custom reticle that is attached based on a specific Interactor. /// /// Interactor that is interacting with this Interactable. /// Returns that represents the attached custom reticle. /// public virtual GameObject GetCustomReticle(IXRInteractor interactor) { if (m_ReticleCache.TryGetValue(interactor, out var reticle)) { return reticle; } return null; } /// /// Attaches the custom reticle to the Interactor. /// /// Interactor that is interacting with this Interactable. /// /// If the custom reticle has an component, this will call /// on it. /// /// public virtual void AttachCustomReticle(IXRInteractor interactor) { var interactorTransform = interactor?.transform; if (interactorTransform == null) return; // Try and find any attached reticle and swap it var reticleProvider = interactorTransform.GetComponent(); if (reticleProvider != null) { if (m_ReticleCache.TryGetValue(interactor, out var prevReticle)) { Destroy(prevReticle); m_ReticleCache.Remove(interactor); } if (m_CustomReticle != null) { var reticleInstance = Instantiate(m_CustomReticle); m_ReticleCache.Add(interactor, reticleInstance); reticleProvider.AttachCustomReticle(reticleInstance); var customReticleBehavior = reticleInstance.GetComponent(); if (customReticleBehavior != null) customReticleBehavior.OnReticleAttached(this, reticleProvider); } } } /// /// Removes the custom reticle from the Interactor. /// /// Interactor that is no longer interacting with this Interactable. /// /// If the custom reticle has an component, this will call /// on it. /// /// public virtual void RemoveCustomReticle(IXRInteractor interactor) { var interactorTransform = interactor?.transform; if (interactorTransform == null) return; // Try and find any attached reticle and swap it var reticleProvider = interactorTransform.GetComponent(); if (reticleProvider != null) { if (m_ReticleCache.TryGetValue(interactor, out var reticleInstance)) { var customReticleBehavior = reticleInstance.GetComponent(); if (customReticleBehavior != null) customReticleBehavior.OnReticleDetaching(); Destroy(reticleInstance); m_ReticleCache.Remove(interactor); reticleProvider.RemoveCustomReticle(); } } } /// /// Capture the current Attach Transform pose. /// This method is automatically called by Unity to capture the pose during the moment of selection. /// /// The specific Interactor as context to get the attachment point for. /// /// Unity automatically calls this method during /// and should not typically need to be called by a user. /// /// /// /// protected void CaptureAttachPose(IXRSelectInteractor interactor) { var thisAttachTransform = GetAttachTransform(interactor); if (thisAttachTransform != null) { m_AttachPoseOnSelect[interactor] = thisAttachTransform.GetWorldPose(); m_LocalAttachPoseOnSelect[interactor] = thisAttachTransform.GetLocalPose(); } else { m_AttachPoseOnSelect.Remove(interactor); m_LocalAttachPoseOnSelect.Remove(interactor); } } /// public virtual void ProcessInteractable(XRInteractionUpdateOrder.UpdatePhase updatePhase) { } /// void IXRInteractionStrengthInteractable.ProcessInteractionStrength(XRInteractionUpdateOrder.UpdatePhase updatePhase) => ProcessInteractionStrength(updatePhase); /// void IXRInteractable.OnRegistered(InteractableRegisteredEventArgs args) => OnRegistered(args); /// void IXRInteractable.OnUnregistered(InteractableUnregisteredEventArgs args) => OnUnregistered(args); /// void IXRActivateInteractable.OnActivated(ActivateEventArgs args) => OnActivated(args); /// void IXRActivateInteractable.OnDeactivated(DeactivateEventArgs args) => OnDeactivated(args); /// bool IXRHoverInteractable.IsHoverableBy(IXRHoverInteractor interactor) { return IsHoverableBy(interactor) && ProcessHoverFilters(interactor); } /// void IXRHoverInteractable.OnHoverEntering(HoverEnterEventArgs args) => OnHoverEntering(args); /// void IXRHoverInteractable.OnHoverEntered(HoverEnterEventArgs args) => OnHoverEntered(args); /// void IXRHoverInteractable.OnHoverExiting(HoverExitEventArgs args) => OnHoverExiting(args); /// void IXRHoverInteractable.OnHoverExited(HoverExitEventArgs args) => OnHoverExited(args); /// bool IXRSelectInteractable.IsSelectableBy(IXRSelectInteractor interactor) { return IsSelectableBy(interactor) && ProcessSelectFilters(interactor); } /// void IXRSelectInteractable.OnSelectEntering(SelectEnterEventArgs args) => OnSelectEntering(args); /// void IXRSelectInteractable.OnSelectEntered(SelectEnterEventArgs args) => OnSelectEntered(args); /// void IXRSelectInteractable.OnSelectExiting(SelectExitEventArgs args) => OnSelectExiting(args); /// void IXRSelectInteractable.OnSelectExited(SelectExitEventArgs args) => OnSelectExited(args); /// void IXRFocusInteractable.OnFocusEntering(FocusEnterEventArgs args) => OnFocusEntering(args); /// void IXRFocusInteractable.OnFocusEntered(FocusEnterEventArgs args) => OnFocusEntered(args); /// void IXRFocusInteractable.OnFocusExiting(FocusExitEventArgs args) => OnFocusExiting(args); /// void IXRFocusInteractable.OnFocusExited(FocusExitEventArgs args) => OnFocusExited(args); /// /// The calls this method /// when this Interactable is registered with it. /// /// Event data containing the Interaction Manager that registered this Interactable. /// /// is only valid during this method call, do not hold a reference to it. /// /// protected virtual void OnRegistered(InteractableRegisteredEventArgs args) { if (args.manager != m_InteractionManager) Debug.LogWarning($"An Interactable was registered with an unexpected {nameof(XRInteractionManager)}." + $" {this} was expecting to communicate with \"{m_InteractionManager}\" but was registered with \"{args.manager}\".", this); registered?.Invoke(args); } /// /// The calls this method /// when this Interactable is unregistered from it. /// /// Event data containing the Interaction Manager that unregistered this Interactable. /// /// is only valid during this method call, do not hold a reference to it. /// /// protected virtual void OnUnregistered(InteractableUnregisteredEventArgs args) { if (args.manager != m_RegisteredInteractionManager) Debug.LogWarning($"An Interactable was unregistered from an unexpected {nameof(XRInteractionManager)}." + $" {this} was expecting to communicate with \"{m_RegisteredInteractionManager}\" but was unregistered from \"{args.manager}\".", this); unregistered?.Invoke(args); } /// /// The calls this method /// right before the Interactor first initiates hovering over an Interactable /// in a first pass. /// /// Event data containing the Interactor that is initiating the hover. /// /// is only valid during this method call, do not hold a reference to it. /// /// protected virtual void OnHoverEntering(HoverEnterEventArgs args) { if (m_CustomReticle != null) AttachCustomReticle(args.interactorObject); var added = m_InteractorsHovering.Add(args.interactorObject); Debug.Assert(added, "An Interactable received a Hover Enter event for an Interactor that was already hovering over it.", this); isHovered = true; if (args.interactorObject is XRBaseInputInteractor variableSelectInteractor) m_VariableSelectInteractors.Add(variableSelectInteractor); } /// /// The calls this method /// when the Interactor first initiates hovering over an Interactable /// in a second pass. /// /// Event data containing the Interactor that is initiating the hover. /// /// is only valid during this method call, do not hold a reference to it. /// /// protected virtual void OnHoverEntered(HoverEnterEventArgs args) { if (m_InteractorsHovering.Count == 1) m_FirstHoverEntered?.Invoke(args); m_HoverEntered?.Invoke(args); } /// /// The calls this method /// right before the Interactor ends hovering over an Interactable /// in a first pass. /// /// Event data containing the Interactor that is ending the hover. /// /// is only valid during this method call, do not hold a reference to it. /// /// protected virtual void OnHoverExiting(HoverExitEventArgs args) { if (m_CustomReticle != null) RemoveCustomReticle(args.interactorObject); var removed = m_InteractorsHovering.Remove(args.interactorObject); Debug.Assert(removed, "An Interactable received a Hover Exit event for an Interactor that was not hovering over it.", this); if (m_InteractorsHovering.Count == 0) isHovered = false; if (!IsSelected(args.interactorObject)) { if (m_InteractionStrengths.Count > 0) m_InteractionStrengths.Remove(args.interactorObject); if (args.interactorObject is XRBaseInputInteractor variableSelectInteractor) m_VariableSelectInteractors.Remove(variableSelectInteractor); } } /// /// The calls this method /// when the Interactor ends hovering over an Interactable /// in a second pass. /// /// Event data containing the Interactor that is ending the hover. /// /// is only valid during this method call, do not hold a reference to it. /// /// protected virtual void OnHoverExited(HoverExitEventArgs args) { if (!isHovered) m_LastHoverExited?.Invoke(args); m_HoverExited?.Invoke(args); } /// /// The calls this method right /// before the Interactor first initiates selection of an Interactable /// in a first pass. /// /// Event data containing the Interactor that is initiating the selection. /// /// is only valid during this method call, do not hold a reference to it. /// /// protected virtual void OnSelectEntering(SelectEnterEventArgs args) { var added = m_InteractorsSelecting.Add(args.interactorObject); Debug.Assert(added, "An Interactable received a Select Enter event for an Interactor that was already selecting it.", this); isSelected = true; if (args.interactorObject is XRBaseInputInteractor variableSelectInteractor) m_VariableSelectInteractors.Add(variableSelectInteractor); if (m_InteractorsSelecting.Count == 1) firstInteractorSelecting = args.interactorObject; CaptureAttachPose(args.interactorObject); } /// /// The calls this method /// when the Interactor first initiates selection of an Interactable /// in a second pass. /// /// Event data containing the Interactor that is initiating the selection. /// /// is only valid during this method call, do not hold a reference to it. /// /// protected virtual void OnSelectEntered(SelectEnterEventArgs args) { if (m_InteractorsSelecting.Count == 1) m_FirstSelectEntered?.Invoke(args); m_SelectEntered?.Invoke(args); } /// /// The calls this method /// right before the Interactor ends selection of an Interactable /// in a first pass. /// /// Event data containing the Interactor that is ending the selection. /// /// is only valid during this method call, do not hold a reference to it. /// /// protected virtual void OnSelectExiting(SelectExitEventArgs args) { var removed = m_InteractorsSelecting.Remove(args.interactorObject); Debug.Assert(removed, "An Interactable received a Select Exit event for an Interactor that was not selecting it.", this); if (m_InteractorsSelecting.Count == 0) isSelected = false; if (!IsHovered(args.interactorObject)) { if (m_InteractionStrengths.Count > 0) m_InteractionStrengths.Remove(args.interactorObject); if (args.interactorObject is XRBaseInputInteractor variableSelectInteractor) m_VariableSelectInteractors.Remove(variableSelectInteractor); } } /// /// The calls this method /// when the Interactor ends selection of an Interactable /// in a second pass. /// /// Event data containing the Interactor that is ending the selection. /// /// is only valid during this method call, do not hold a reference to it. /// /// protected virtual void OnSelectExited(SelectExitEventArgs args) { if (!isSelected) m_LastSelectExited?.Invoke(args); m_SelectExited?.Invoke(args); // The dictionaries are pruned so that they don't infinitely grow in size as selections are made. if (!isSelected) { firstInteractorSelecting = null; m_AttachPoseOnSelect.Clear(); m_LocalAttachPoseOnSelect.Clear(); } } /// /// The calls this method right /// before the Interaction group first gains focus of an Interactable /// in a first pass. /// /// Event data containing the Interaction group that is initiating focus. /// /// is only valid during this method call, do not hold a reference to it. /// /// protected virtual void OnFocusEntering(FocusEnterEventArgs args) { var added = m_InteractionGroupsFocusing.Add(args.interactionGroup); Debug.Assert(added, "An Interactable received a Focus Enter event for an Interaction group that was already focusing it.", this); isFocused = true; if (m_InteractionGroupsFocusing.Count == 1) firstInteractionGroupFocusing = args.interactionGroup; } /// /// The calls this method /// when the Interaction group first gains focus of an Interactable /// in a second pass. /// /// Event data containing the Interaction group that is initiating the focus. /// /// is only valid during this method call, do not hold a reference to it. /// /// protected virtual void OnFocusEntered(FocusEnterEventArgs args) { if (m_InteractionGroupsFocusing.Count == 1) m_FirstFocusEntered?.Invoke(args); m_FocusEntered?.Invoke(args); } /// /// The calls this method /// right before the Interaction group loses focus of an Interactable /// in a first pass. /// /// Event data containing the Interaction group that is losing focus. /// /// is only valid during this method call, do not hold a reference to it. /// /// protected virtual void OnFocusExiting(FocusExitEventArgs args) { var removed = m_InteractionGroupsFocusing.Remove(args.interactionGroup); Debug.Assert(removed, "An Interactable received a Focus Exit event for an Interaction group that did not have focus of it.", this); if (m_InteractionGroupsFocusing.Count == 0) isFocused = false; } /// /// The calls this method /// when the Interaction group loses focus of an Interactable /// in a second pass. /// /// Event data containing the Interaction group that is losing focus. /// /// is only valid during this method call, do not hold a reference to it. /// /// protected virtual void OnFocusExited(FocusExitEventArgs args) { if (!isFocused) m_LastFocusExited?.Invoke(args); m_FocusExited?.Invoke(args); if (!isFocused) firstInteractionGroupFocusing = null; } /// /// calls this method when the /// Interactor begins an activation event on this Interactable. /// /// Event data containing the Interactor that is sending the activate event. /// /// is only valid during this method call, do not hold a reference to it. /// /// protected virtual void OnActivated(ActivateEventArgs args) { m_Activated?.Invoke(args); } /// /// calls this method when the /// Interactor ends an activation event on this Interactable. /// /// Event data containing the Interactor that is sending the deactivate event. /// /// is only valid during this method call, do not hold a reference to it. /// /// protected virtual void OnDeactivated(DeactivateEventArgs args) { m_Deactivated?.Invoke(args); } /// /// The calls this method to signal to update the interaction strength. /// /// The update phase during which this method is called. /// /// protected virtual void ProcessInteractionStrength(XRInteractionUpdateOrder.UpdatePhase updatePhase) { var maxInteractionStrength = 0f; using (s_ProcessInteractionStrengthMarker.Auto()) { if (!isSelected && !isHovered) { // Optimize to avoid float equality check in Value setter if the value has already been cleared // due to being not selected or hovered. if (m_ClearedLargestInteractionStrength) return; m_LargestInteractionStrength.Value = 0f; m_ClearedLargestInteractionStrength = true; return; } m_ClearedLargestInteractionStrength = false; var hasFilters = m_InteractionStrengthFilters.registeredSnapshot.Count > 0; // Select is checked before Hover to allow process to only be called once per interactor hovering and selecting // using the largest initial interaction strength. if (isSelected) { for (int i = 0, count = m_InteractorsSelecting.Count; i < count; ++i) { var interactor = m_InteractorsSelecting[i]; if (interactor is XRBaseInputInteractor) continue; var interactionStrength = hasFilters ? ProcessInteractionStrengthFilters(interactor, k_InteractionStrengthSelect) : k_InteractionStrengthSelect; m_InteractionStrengths[interactor] = interactionStrength; maxInteractionStrength = Mathf.Max(maxInteractionStrength, interactionStrength); } } if (isHovered) { for (int i = 0, count = m_InteractorsHovering.Count; i < count; ++i) { var interactor = m_InteractorsHovering[i]; if (interactor is XRBaseInputInteractor || IsSelected(interactor)) continue; var interactionStrength = hasFilters ? ProcessInteractionStrengthFilters(interactor, k_InteractionStrengthHover) : k_InteractionStrengthHover; m_InteractionStrengths[interactor] = interactionStrength; maxInteractionStrength = Mathf.Max(maxInteractionStrength, interactionStrength); } } for (int i = 0, count = m_VariableSelectInteractors.Count; i < count; ++i) { var interactor = m_VariableSelectInteractors[i]; var interactionStrength = hasFilters ? ProcessInteractionStrengthFilters(interactor, ReadInteractionStrength(interactor)) : ReadInteractionStrength(interactor); m_InteractionStrengths[interactor] = interactionStrength; maxInteractionStrength = Mathf.Max(maxInteractionStrength, interactionStrength); } } using (s_ProcessInteractionStrengthEventMarker.Auto()) { m_LargestInteractionStrength.Value = maxInteractionStrength; } } float ReadInteractionStrength(XRBaseInputInteractor interactor) { // Use the Select input value as the initial interaction strength. // For interactors that use motion controller input, this is typically the analog trigger or grip press amount. // Fall back to the default values for selected and hovered interactors in the case when the interactor // is misconfigured and is missing the input wrapper or component reference. #pragma warning disable CS0618 // Type or member is obsolete -- Retained for backwards compatibility if (!interactor.forceDeprecatedInput) return interactor.selectInput.ReadValue(); if (interactor.xrController != null) return interactor.xrController.selectInteractionState.value; #pragma warning restore CS0618 return IsSelected(interactor) ? k_InteractionStrengthSelect : k_InteractionStrengthHover; } /// /// Returns the processing value of the filters in for the given Interactor and this /// Interactable. /// /// The Interactor to be validated by the hover filters. /// /// Returns if all processed filters also return , or if /// is empty. Otherwise, returns . /// protected bool ProcessHoverFilters(IXRHoverInteractor interactor) { return XRFilterUtility.Process(m_HoverFilters, interactor, this); } /// /// Returns the processing value of the filters in for the given Interactor and this /// Interactable. /// /// The Interactor to be validated by the select filters. /// /// Returns if all processed filters also return , or if /// is empty. Otherwise, returns . /// protected bool ProcessSelectFilters(IXRSelectInteractor interactor) { return XRFilterUtility.Process(m_SelectFilters, interactor, this); } /// /// Returns the processing value of the interaction strength filters in for the given Interactor and this /// Interactable. /// /// The Interactor to process by the interaction strength filters. /// The interaction strength before processing. /// Returns the modified interaction strength that is the result of passing the interaction strength through each filter. protected float ProcessInteractionStrengthFilters(IXRInteractor interactor, float interactionStrength) { return XRFilterUtility.Process(m_InteractionStrengthFilters, interactor, this, interactionStrength); } } }