using System; using System.Collections.Generic; #if UNITY_EDITOR && !UNITY_2021_3_OR_NEWER using System.Text; #endif using Unity.Profiling; #if UNITY_EDITOR && UNITY_2021_3_OR_NEWER using UnityEditor.Search; #elif UNITY_EDITOR && !UNITY_2021_3_OR_NEWER using UnityEditor.Experimental.SceneManagement; #endif using UnityEngine.XR.Interaction.Toolkit.Filtering; using UnityEngine.XR.Interaction.Toolkit.Interactables; using UnityEngine.XR.Interaction.Toolkit.Interactors; using UnityEngine.XR.Interaction.Toolkit.Utilities; using UnityEngine.XR.Interaction.Toolkit.Utilities.Internal; using UnityEngine.XR.Interaction.Toolkit.Utilities.Pooling; #if AR_FOUNDATION_PRESENT using UnityEngine.XR.Interaction.Toolkit.AR; #endif namespace UnityEngine.XR.Interaction.Toolkit { /// /// The Interaction Manager acts as an intermediary between Interactors and Interactables. /// It is possible to have multiple Interaction Managers, each with their own valid set of Interactors and Interactables. /// Upon being enabled, both Interactors and Interactables register themselves with a valid Interaction Manager /// (if a specific one has not already been assigned in the inspector). The loaded scenes must have at least one Interaction Manager /// for Interactors and Interactables to be able to communicate. /// /// /// Many of the methods on the Interactors and Interactables are designed to be called by this Interaction Manager /// rather than being called directly in order to maintain consistency between both targets of an interaction event. /// /// /// [AddComponentMenu("XR/XR Interaction Manager", 11)] [DisallowMultipleComponent] [DefaultExecutionOrder(XRInteractionUpdateOrder.k_InteractionManager)] [HelpURL(XRHelpURLConstants.k_XRInteractionManager)] public partial class XRInteractionManager : MonoBehaviour { /// /// Calls the methods in its invocation list when an is registered. /// /// /// The passed to each listener is only valid while the event is invoked, /// do not hold a reference to it. /// /// /// public event Action interactionGroupRegistered; /// /// Calls the methods in its invocation list when an is unregistered. /// /// /// The passed to each listener is only valid while the event is invoked, /// do not hold a reference to it. /// /// /// public event Action interactionGroupUnregistered; /// /// Calls the methods in its invocation list when an is registered. /// /// /// The passed to each listener is only valid while the event is invoked, /// do not hold a reference to it. /// /// /// public event Action interactorRegistered; /// /// Calls the methods in its invocation list when an is unregistered. /// /// /// The passed to each listener is only valid while the event is invoked, /// do not hold a reference to it. /// /// /// public event Action interactorUnregistered; /// /// Calls the methods in its invocation list when an is registered. /// /// /// The passed to each listener is only valid while the event is invoked, /// do not hold a reference to it. /// /// /// public event Action interactableRegistered; /// /// Calls the methods in its invocation list when an is unregistered. /// /// /// The passed to each listener is only valid while the event is invoked, /// do not hold a reference to it. /// /// /// public event Action interactableUnregistered; /// /// Calls the methods in its invocation list when an gains focus. /// public event Action focusGained; /// /// Calls the methods in its invocation list when an loses focus. /// public event Action focusLost; /// /// Calls the methods in its invocation list when a manager is enabled or disabled. /// /// internal static event Action activeInteractionManagersChanged; [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 global hover filters in this object. /// Used as additional hover validations for this manager. /// /// /// 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 global select filters in this object. /// Used as additional select validations for this manager. /// /// /// 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; /// /// (Read Only) The last that was focused by /// any . /// public IXRFocusInteractable lastFocused { get; protected set; } /// /// (Read Only) List of enabled Interaction Manager instances. /// /// /// Intended to be used by XR Interaction Debugger and analytics. /// /// internal static List activeInteractionManagers { get; } = new List(); /// /// Map of all registered objects to test for colliding. /// readonly Dictionary m_ColliderToInteractableMap = new Dictionary(); /// /// Map of colliders and their associated . /// readonly Dictionary m_ColliderToSnapVolumes = new Dictionary(); /// /// List of registered Interactors. /// readonly RegistrationList m_Interactors = new RegistrationList(); /// /// List of registered Interaction Groups. /// readonly RegistrationList m_InteractionGroups = new RegistrationList(); /// /// List of registered Interactables. /// readonly RegistrationList m_Interactables = new RegistrationList(); /// /// Reusable list of Interactables for retrieving the current hovered Interactables of an Interactor. /// readonly List m_CurrentHovered = new List(); /// /// Reusable list of Interactables for retrieving the current selected Interactables of an Interactor. /// readonly List m_CurrentSelected = new List(); /// /// Map of Interactables that have the highest priority for selection in a frame. /// readonly Dictionary> m_HighestPriorityTargetMap = new Dictionary>(); /// /// Pool of Target Priority Interactor lists. Used by m_HighestPriorityTargetMap. /// static readonly LinkedPool> s_TargetPriorityInteractorListPool = new LinkedPool>(() => new List(), actionOnRelease: list => list.Clear(), collectionCheck: false); /// /// Reusable list of valid targets for an Interactor. /// readonly List m_ValidTargets = new List(); /// /// Reusable set of valid targets for an Interactor. /// readonly HashSet m_UnorderedValidTargets = new HashSet(); /// /// Set of all Interactors that are in an Interaction Group. /// readonly HashSet m_InteractorsInGroup = new HashSet(); /// /// Set of all Interaction Groups that are in an Interaction Group. /// readonly HashSet m_GroupsInGroup = new HashSet(); readonly List m_ScratchInteractionGroups = new List(); readonly List m_ScratchInteractors = new List(); // Reusable event args readonly LinkedPool m_FocusEnterEventArgs = new LinkedPool(() => new FocusEnterEventArgs(), collectionCheck: false); readonly LinkedPool m_FocusExitEventArgs = new LinkedPool(() => new FocusExitEventArgs(), collectionCheck: false); readonly LinkedPool m_SelectEnterEventArgs = new LinkedPool(() => new SelectEnterEventArgs(), collectionCheck: false); readonly LinkedPool m_SelectExitEventArgs = new LinkedPool(() => new SelectExitEventArgs(), collectionCheck: false); readonly LinkedPool m_HoverEnterEventArgs = new LinkedPool(() => new HoverEnterEventArgs(), collectionCheck: false); readonly LinkedPool m_HoverExitEventArgs = new LinkedPool(() => new HoverExitEventArgs(), collectionCheck: false); readonly LinkedPool m_InteractionGroupRegisteredEventArgs = new LinkedPool(() => new InteractionGroupRegisteredEventArgs(), collectionCheck: false); readonly LinkedPool m_InteractionGroupUnregisteredEventArgs = new LinkedPool(() => new InteractionGroupUnregisteredEventArgs(), collectionCheck: false); readonly LinkedPool m_InteractorRegisteredEventArgs = new LinkedPool(() => new InteractorRegisteredEventArgs(), collectionCheck: false); readonly LinkedPool m_InteractorUnregisteredEventArgs = new LinkedPool(() => new InteractorUnregisteredEventArgs(), collectionCheck: false); readonly LinkedPool m_InteractableRegisteredEventArgs = new LinkedPool(() => new InteractableRegisteredEventArgs(), collectionCheck: false); readonly LinkedPool m_InteractableUnregisteredEventArgs = new LinkedPool(() => new InteractableUnregisteredEventArgs(), collectionCheck: false); static readonly ProfilerMarker s_PreprocessInteractorsMarker = new ProfilerMarker("XRI.PreprocessInteractors"); static readonly ProfilerMarker s_ProcessInteractionStrengthMarker = new ProfilerMarker("XRI.ProcessInteractionStrength"); static readonly ProfilerMarker s_ProcessInteractorsMarker = new ProfilerMarker("XRI.ProcessInteractors"); static readonly ProfilerMarker s_ProcessInteractablesMarker = new ProfilerMarker("XRI.ProcessInteractables"); static readonly ProfilerMarker s_UpdateGroupMemberInteractionsMarker = new ProfilerMarker("XRI.UpdateGroupMemberInteractions"); internal static readonly ProfilerMarker s_GetValidTargetsMarker = new ProfilerMarker("XRI.GetValidTargets"); static readonly ProfilerMarker s_FilterRegisteredValidTargetsMarker = new ProfilerMarker("XRI.FilterRegisteredValidTargets"); internal static readonly ProfilerMarker s_EvaluateInvalidFocusMarker = new ProfilerMarker("XRI.EvaluateInvalidFocus"); internal static readonly ProfilerMarker s_EvaluateInvalidSelectionsMarker = new ProfilerMarker("XRI.EvaluateInvalidSelections"); internal static readonly ProfilerMarker s_EvaluateInvalidHoversMarker = new ProfilerMarker("XRI.EvaluateInvalidHovers"); internal static readonly ProfilerMarker s_EvaluateValidSelectionsMarker = new ProfilerMarker("XRI.EvaluateValidSelections"); internal static readonly ProfilerMarker s_EvaluateValidHoversMarker = new ProfilerMarker("XRI.EvaluateValidHovers"); static readonly ProfilerMarker s_FocusEnterMarker = new ProfilerMarker("XRI.FocusEnter"); static readonly ProfilerMarker s_FocusExitMarker = new ProfilerMarker("XRI.FocusExit"); static readonly ProfilerMarker s_SelectEnterMarker = new ProfilerMarker("XRI.SelectEnter"); static readonly ProfilerMarker s_SelectExitMarker = new ProfilerMarker("XRI.SelectExit"); static readonly ProfilerMarker s_HoverEnterMarker = new ProfilerMarker("XRI.HoverEnter"); static readonly ProfilerMarker s_HoverExitMarker = new ProfilerMarker("XRI.HoverExit"); /// /// See . /// protected virtual void Awake() { // Setup the starting filters m_HoverFilters.RegisterReferences(m_StartingHoverFilters, this); m_SelectFilters.RegisterReferences(m_StartingSelectFilters, this); } /// /// See . /// protected virtual void OnEnable() { if (activeInteractionManagers.Count > 0) { var message = "There are multiple active and enabled XR Interaction Manager components in the loaded scenes." + " This is supported, but may not be intended since interactors and interactables are not able to interact with those registered to a different manager." + " You can use the Window > Analysis > XR Interaction Debugger window to verify the interactors and interactables registered with each."; #if UNITY_EDITOR if (ComponentLocatorUtility.componentCache != null) { message += " The default manager that interactors and interactables automatically register with when None is: " + GetHierarchyPath(ComponentLocatorUtility.componentCache.gameObject); } #endif Debug.LogWarning(message, this); } activeInteractionManagers.Add(this); activeInteractionManagersChanged?.Invoke(this, true); Application.onBeforeRender += OnBeforeRender; } /// /// See . /// protected virtual void OnDisable() { Application.onBeforeRender -= OnBeforeRender; activeInteractionManagers.Remove(this); activeInteractionManagersChanged?.Invoke(this, false); ClearPriorityForSelectionMap(); } /// /// See . /// // ReSharper disable PossiblyImpureMethodCallOnReadonlyVariable -- ProfilerMarker.Begin with context object does not have Pure attribute protected virtual void Update() { ClearPriorityForSelectionMap(); FlushRegistration(); using (s_PreprocessInteractorsMarker.Auto()) PreprocessInteractors(XRInteractionUpdateOrder.UpdatePhase.Dynamic); foreach (var interactionGroup in m_InteractionGroups.registeredSnapshot) { if (!m_InteractionGroups.IsStillRegistered(interactionGroup) || m_GroupsInGroup.Contains(interactionGroup)) continue; using (s_EvaluateInvalidFocusMarker.Auto()) ClearInteractionGroupFocus(interactionGroup); using (s_UpdateGroupMemberInteractionsMarker.Auto()) interactionGroup.UpdateGroupMemberInteractions(); } foreach (var interactor in m_Interactors.registeredSnapshot) { if (!m_Interactors.IsStillRegistered(interactor) || m_InteractorsInGroup.Contains(interactor)) continue; using (s_GetValidTargetsMarker.Auto()) GetValidTargets(interactor, m_ValidTargets); var selectInteractor = interactor as IXRSelectInteractor; var hoverInteractor = interactor as IXRHoverInteractor; if (selectInteractor != null) { using (s_EvaluateInvalidSelectionsMarker.Auto()) ClearInteractorSelection(selectInteractor, m_ValidTargets); } if (hoverInteractor != null) { using (s_EvaluateInvalidHoversMarker.Auto()) ClearInteractorHover(hoverInteractor, m_ValidTargets); } if (selectInteractor != null) { using (s_EvaluateValidSelectionsMarker.Auto()) InteractorSelectValidTargets(selectInteractor, m_ValidTargets); } if (hoverInteractor != null) { using (s_EvaluateValidHoversMarker.Auto()) InteractorHoverValidTargets(hoverInteractor, m_ValidTargets); } } using (s_ProcessInteractionStrengthMarker.Auto()) ProcessInteractionStrength(XRInteractionUpdateOrder.UpdatePhase.Dynamic); using (s_ProcessInteractorsMarker.Auto()) ProcessInteractors(XRInteractionUpdateOrder.UpdatePhase.Dynamic); using (s_ProcessInteractablesMarker.Auto()) ProcessInteractables(XRInteractionUpdateOrder.UpdatePhase.Dynamic); } /// /// See . /// protected virtual void LateUpdate() { FlushRegistration(); using (s_ProcessInteractorsMarker.Auto()) ProcessInteractors(XRInteractionUpdateOrder.UpdatePhase.Late); using (s_ProcessInteractablesMarker.Auto()) ProcessInteractables(XRInteractionUpdateOrder.UpdatePhase.Late); } /// /// See . /// protected virtual void FixedUpdate() { FlushRegistration(); using (s_ProcessInteractorsMarker.Auto()) ProcessInteractors(XRInteractionUpdateOrder.UpdatePhase.Fixed); using (s_ProcessInteractablesMarker.Auto()) ProcessInteractables(XRInteractionUpdateOrder.UpdatePhase.Fixed); } /// /// Delegate method used to register for "Just Before Render" input updates for VR devices. /// /// [BeforeRenderOrder(XRInteractionUpdateOrder.k_BeforeRenderOrder)] protected virtual void OnBeforeRender() { FlushRegistration(); using (s_ProcessInteractorsMarker.Auto()) ProcessInteractors(XRInteractionUpdateOrder.UpdatePhase.OnBeforeRender); using (s_ProcessInteractablesMarker.Auto()) ProcessInteractables(XRInteractionUpdateOrder.UpdatePhase.OnBeforeRender); } // ReSharper restore PossiblyImpureMethodCallOnReadonlyVariable /// /// Automatically called each frame to preprocess all interactors registered with this manager. /// /// The update phase. /// /// Please see the documentation for more details on update order. /// /// /// protected virtual void PreprocessInteractors(XRInteractionUpdateOrder.UpdatePhase updatePhase) { foreach (var interactionGroup in m_InteractionGroups.registeredSnapshot) { if (!m_InteractionGroups.IsStillRegistered(interactionGroup) || m_GroupsInGroup.Contains(interactionGroup)) continue; interactionGroup.PreprocessGroupMembers(updatePhase); } foreach (var interactor in m_Interactors.registeredSnapshot) { if (!m_Interactors.IsStillRegistered(interactor) || m_InteractorsInGroup.Contains(interactor)) continue; interactor.PreprocessInteractor(updatePhase); } } /// /// Automatically called each frame to process all interactors registered with this manager. /// /// The update phase. /// /// Please see the documentation for more details on update order. /// /// /// protected virtual void ProcessInteractors(XRInteractionUpdateOrder.UpdatePhase updatePhase) { foreach (var interactionGroup in m_InteractionGroups.registeredSnapshot) { if (!m_InteractionGroups.IsStillRegistered(interactionGroup) || m_GroupsInGroup.Contains(interactionGroup)) continue; interactionGroup.ProcessGroupMembers(updatePhase); } foreach (var interactor in m_Interactors.registeredSnapshot) { if (!m_Interactors.IsStillRegistered(interactor) || m_InteractorsInGroup.Contains(interactor)) continue; interactor.ProcessInteractor(updatePhase); } } /// /// Automatically called each frame to process all interactables registered with this manager. /// /// The update phase. /// /// Please see the documentation for more details on update order. /// /// /// protected virtual void ProcessInteractables(XRInteractionUpdateOrder.UpdatePhase updatePhase) { foreach (var interactable in m_Interactables.registeredSnapshot) { if (!m_Interactables.IsStillRegistered(interactable)) continue; interactable.ProcessInteractable(updatePhase); } } /// /// Automatically called each frame to process interaction strength of interactables and interactors registered with this manager. /// /// The update phase. /// /// /// protected virtual void ProcessInteractionStrength(XRInteractionUpdateOrder.UpdatePhase updatePhase) { // Unlike other processing, with interaction strength, interactables are processed before interactors // since interactables with the ability to be poked dictate the interaction strength. After the // interaction strength of interactables are computed for this frame, they are gathered into // the interactor for use in affordances or within the process step. foreach (var interactable in m_Interactables.registeredSnapshot) { if (!m_Interactables.IsStillRegistered(interactable)) continue; if (interactable is IXRInteractionStrengthInteractable interactionStrengthInteractable) interactionStrengthInteractable.ProcessInteractionStrength(updatePhase); } foreach (var interactor in m_Interactors.registeredSnapshot) { if (!m_Interactors.IsStillRegistered(interactor)) continue; if (interactor is IXRInteractionStrengthInteractor interactionStrengthInteractor) interactionStrengthInteractor.ProcessInteractionStrength(updatePhase); } } /// /// Whether the given Interactor can hover the given Interactable. /// You can extend this method to add global hover validations by code. /// /// The Interactor to check. /// The Interactable to check. /// Returns whether the given Interactor can hover the given Interactable. /// /// You can also extend the global hover validations without needing to create a derived class by adding hover /// filters to this object (see and ). /// /// public virtual bool CanHover(IXRHoverInteractor interactor, IXRHoverInteractable interactable) { return interactor.isHoverActive && IsHoverPossible(interactor, interactable); } /// /// Whether the given Interactor would be able to hover the given Interactable if the Interactor were in a state where it could hover. /// /// The Interactor to check. /// The Interactable to check. /// Returns whether the given Interactor would be able to hover the given Interactable if the Interactor were in a state where it could hover. /// public bool IsHoverPossible(IXRHoverInteractor interactor, IXRHoverInteractable interactable) { return HasInteractionLayerOverlap(interactor, interactable) && ProcessHoverFilters(interactor, interactable) && interactor.CanHover(interactable) && interactable.IsHoverableBy(interactor); } /// /// Whether the given Interactor can select the given Interactable. /// You can extend this method to add global select validations by code. /// /// The Interactor to check. /// The Interactable to check. /// Returns whether the given Interactor can select the given Interactable. /// /// You can also extend the global select validations without needing to create a derived class by adding select /// filters to this object (see and ). /// /// public virtual bool CanSelect(IXRSelectInteractor interactor, IXRSelectInteractable interactable) { return interactor.isSelectActive && IsSelectPossible(interactor, interactable); } /// /// Whether the given Interactor would be able to select the given Interactable if the Interactor were in a state where it could select. /// /// The Interactor to check. /// The Interactable to check. /// Returns whether the given Interactor would be able to select the given Interactable if the Interactor were in a state where it could select. /// public bool IsSelectPossible(IXRSelectInteractor interactor, IXRSelectInteractable interactable) { return HasInteractionLayerOverlap(interactor, interactable) && ProcessSelectFilters(interactor, interactable) && interactor.CanSelect(interactable) && interactable.IsSelectableBy(interactor); } /// /// Whether the given Interactor can gain focus of the given Interactable. /// You can extend this method to add global focus validations by code. /// /// The Interactor to check. /// The Interactable to check. /// Returns whether the given Interactor can gain focus of the given Interactable. /// public virtual bool CanFocus(IXRInteractor interactor, IXRFocusInteractable interactable) { return IsFocusPossible(interactor, interactable); } /// /// Whether the given Interactor would be able gain focus of the given Interactable if the Interactor were in a state where it could focus. /// /// The Interactor to check. /// The Interactable to check. /// Returns whether the given Interactor would be able to gain focus of the given Interactable if the Interactor were in a state where it could focus. /// public bool IsFocusPossible(IXRInteractor interactor, IXRFocusInteractable interactable) { return interactable.canFocus && HasInteractionLayerOverlap(interactor, interactable); } /// /// Registers a new Interaction Group to be processed. /// /// The Interaction Group to be registered. public virtual void RegisterInteractionGroup(IXRInteractionGroup interactionGroup) { IXRInteractionGroup containingGroup = null; if (interactionGroup is IXRGroupMember groupMember) containingGroup = groupMember.containingGroup; if (containingGroup != null && !IsRegistered(containingGroup)) { Debug.LogError($"Cannot register {interactionGroup} with Interaction Manager before its containing " + "Interaction Group is registered.", this); return; } if (m_InteractionGroups.Register(interactionGroup)) { if (containingGroup != null) m_GroupsInGroup.Add(interactionGroup); using (m_InteractionGroupRegisteredEventArgs.Get(out var args)) { args.manager = this; args.interactionGroupObject = interactionGroup; args.containingGroupObject = containingGroup; OnRegistered(args); } } } /// /// Automatically called when an Interaction Group is registered with this Interaction Manager. /// Notifies the Interaction Group, passing the given . /// /// Event data containing the registered Interaction Group. /// /// is only valid during this method call, do not hold a reference to it. /// /// protected virtual void OnRegistered(InteractionGroupRegisteredEventArgs args) { Debug.Assert(args.manager == this, this); args.interactionGroupObject.OnRegistered(args); interactionGroupRegistered?.Invoke(args); } /// /// Unregister an Interaction Group so it is no longer processed. /// /// The Interaction Group to be unregistered. public virtual void UnregisterInteractionGroup(IXRInteractionGroup interactionGroup) { if (!IsRegistered(interactionGroup)) return; interactionGroup.OnBeforeUnregistered(); // Make sure no registered interactors or groups still reference this group if (m_InteractionGroups.flushedCount > 0) { m_InteractionGroups.GetRegisteredItems(m_ScratchInteractionGroups); foreach (var group in m_ScratchInteractionGroups) { if (group is IXRGroupMember groupMember && groupMember.containingGroup == interactionGroup) { Debug.LogError($"Cannot unregister {interactionGroup} with Interaction Manager before its " + "Group Members have been re-registered as not part of the Group.", this); return; } } } if (m_Interactors.flushedCount > 0) { m_Interactors.GetRegisteredItems(m_ScratchInteractors); foreach (var interactor in m_ScratchInteractors) { if (interactor is IXRGroupMember groupMember && groupMember.containingGroup == interactionGroup) { Debug.LogError($"Cannot unregister {interactionGroup} with Interaction Manager before its " + "Group Members have been re-registered as not part of the Group.", this); return; } } } if (m_InteractionGroups.Unregister(interactionGroup)) { m_GroupsInGroup.Remove(interactionGroup); using (m_InteractionGroupUnregisteredEventArgs.Get(out var args)) { args.manager = this; args.interactionGroupObject = interactionGroup; OnUnregistered(args); } } } /// /// Automatically called when an Interaction Group is unregistered from this Interaction Manager. /// Notifies the Interaction Group, passing the given . /// /// Event data containing the unregistered Interaction Group. /// /// is only valid during this method call, do not hold a reference to it. /// /// protected virtual void OnUnregistered(InteractionGroupUnregisteredEventArgs args) { Debug.Assert(args.manager == this, this); args.interactionGroupObject.OnUnregistered(args); interactionGroupUnregistered?.Invoke(args); } /// /// Gets all currently registered Interaction groups /// /// The list that will filled with all of the registered interaction groups public void GetInteractionGroups(List interactionGroups) { m_InteractionGroups.GetRegisteredItems(interactionGroups); } /// /// Gets the registered Interaction Group with the given name. /// /// The name of the interaction group to retrieve. /// Returns the interaction group with matching name, or null if none were found. /// public IXRInteractionGroup GetInteractionGroup(string groupName) { foreach (var interactionGroup in m_InteractionGroups.registeredSnapshot) { if (interactionGroup.groupName == groupName) return interactionGroup; } return null; } /// /// Registers a new Interactor to be processed. /// /// The Interactor to be registered. public virtual void RegisterInteractor(IXRInteractor interactor) { IXRInteractionGroup containingGroup = null; if (interactor is IXRGroupMember groupMember) containingGroup = groupMember.containingGroup; if (containingGroup != null && !IsRegistered(containingGroup)) { Debug.LogError($"Cannot register {interactor} with Interaction Manager before its containing " + "Interaction Group is registered.", this); return; } if (m_Interactors.Register(interactor)) { if (containingGroup != null) m_InteractorsInGroup.Add(interactor); using (m_InteractorRegisteredEventArgs.Get(out var args)) { args.manager = this; args.interactorObject = interactor; args.containingGroupObject = containingGroup; OnRegistered(args); } } } /// /// Automatically called when an Interactor is registered with this Interaction Manager. /// Notifies the Interactor, passing the given . /// /// Event data containing the registered Interactor. /// /// is only valid during this method call, do not hold a reference to it. /// /// protected virtual void OnRegistered(InteractorRegisteredEventArgs args) { Debug.Assert(args.manager == this, this); args.interactorObject.OnRegistered(args); interactorRegistered?.Invoke(args); } /// /// Unregister an Interactor so it is no longer processed. /// /// The Interactor to be unregistered. public virtual void UnregisterInteractor(IXRInteractor interactor) { if (!IsRegistered(interactor)) return; var interactorTransform = interactor.transform; // We suppress canceling focus for inactive interactors vs. destroyed interactors as that is used as a method of mediation if (interactorTransform == null || interactorTransform.gameObject.activeSelf) CancelInteractorFocus(interactor); if (interactor is IXRSelectInteractor selectInteractor) CancelInteractorSelection(selectInteractor); if (interactor is IXRHoverInteractor hoverInteractor) CancelInteractorHover(hoverInteractor); if (m_Interactors.Unregister(interactor)) { m_InteractorsInGroup.Remove(interactor); using (m_InteractorUnregisteredEventArgs.Get(out var args)) { args.manager = this; args.interactorObject = interactor; OnUnregistered(args); } } } /// /// Automatically called when an Interactor is unregistered from this Interaction Manager. /// Notifies the Interactor, passing the given . /// /// Event data containing the unregistered Interactor. /// /// is only valid during this method call, do not hold a reference to it. /// /// protected virtual void OnUnregistered(InteractorUnregisteredEventArgs args) { Debug.Assert(args.manager == this, this); args.interactorObject.OnUnregistered(args); interactorUnregistered?.Invoke(args); } /// /// Registers a new Interactable to be processed. /// /// The Interactable to be registered. public virtual void RegisterInteractable(IXRInteractable interactable) { if (m_Interactables.Register(interactable)) { foreach (var interactableCollider in interactable.colliders) { if (interactableCollider == null) continue; // Add the association for a fast lookup which maps from Collider to Interactable. // Warn if the same Collider is already used by another registered Interactable // since the lookup will only return the earliest registered rather than a list of all. // The warning is suppressed in the case of gesture interactables since it's common // to compose multiple on the same GameObject. if (!m_ColliderToInteractableMap.TryGetValue(interactableCollider, out var associatedInteractable)) { m_ColliderToInteractableMap.Add(interactableCollider, interactable); } #if AR_FOUNDATION_PRESENT #pragma warning disable 618 else if (!(interactable is ARBaseGestureInteractable && associatedInteractable is ARBaseGestureInteractable)) #pragma warning restore 618 #else else #endif { Debug.LogWarning("A collider used by an Interactable object is already registered with another Interactable object." + $" The {interactableCollider} will remain associated with {associatedInteractable}, which was registered before {interactable}." + " The value returned by XRInteractionManager.TryGetInteractableForCollider will be the first association.", interactable as Object); } } using (m_InteractableRegisteredEventArgs.Get(out var args)) { args.manager = this; args.interactableObject = interactable; OnRegistered(args); } } } /// /// Automatically called when an Interactable is registered with this Interaction Manager. /// Notifies the Interactable, passing the given . /// /// Event data containing the registered Interactable. /// /// is only valid during this method call, do not hold a reference to it. /// /// protected virtual void OnRegistered(InteractableRegisteredEventArgs args) { Debug.Assert(args.manager == this, this); args.interactableObject.OnRegistered(args); interactableRegistered?.Invoke(args); } /// /// Unregister an Interactable so it is no longer processed. /// /// The Interactable to be unregistered. public virtual void UnregisterInteractable(IXRInteractable interactable) { if (!IsRegistered(interactable)) return; if (interactable is IXRFocusInteractable focusable) CancelInteractableFocus(focusable); if (interactable is IXRSelectInteractable selectable) CancelInteractableSelection(selectable); if (interactable is IXRHoverInteractable hoverable) CancelInteractableHover(hoverable); if (m_Interactables.Unregister(interactable)) { // This makes the assumption that the list of Colliders has not been changed after // the Interactable is registered. If any were removed afterward, those would remain // in the dictionary. foreach (var interactableCollider in interactable.colliders) { if (interactableCollider == null) continue; if (m_ColliderToInteractableMap.TryGetValue(interactableCollider, out var associatedInteractable) && associatedInteractable == interactable) m_ColliderToInteractableMap.Remove(interactableCollider); } using (m_InteractableUnregisteredEventArgs.Get(out var args)) { args.manager = this; args.interactableObject = interactable; OnUnregistered(args); } } } /// /// Automatically called when an Interactable is unregistered from this Interaction Manager. /// Notifies the Interactable, passing the given . /// /// Event data containing the unregistered Interactable. /// /// is only valid during this method call, do not hold a reference to it. /// /// protected virtual void OnUnregistered(InteractableUnregisteredEventArgs args) { Debug.Assert(args.manager == this, this); args.interactableObject.OnUnregistered(args); interactableUnregistered?.Invoke(args); } /// /// Registers a new snap volume to associate the snap collider and interactable. /// /// The snap volume to be registered. /// public void RegisterSnapVolume(XRInteractableSnapVolume snapVolume) { if (snapVolume == null) return; var snapCollider = snapVolume.snapCollider; if (snapCollider == null) return; if (!m_ColliderToSnapVolumes.TryGetValue(snapCollider, out var associatedSnapVolume)) { m_ColliderToSnapVolumes.Add(snapCollider, snapVolume); } else { Debug.LogWarning("A collider used by a snap volume component is already registered with another snap volume component." + $" The {snapCollider} will remain associated with {associatedSnapVolume}, which was registered before {snapVolume}." + " The value returned by XRInteractionManager.TryGetInteractableForCollider will be the first association.", snapVolume); } } /// /// Unregister the snap volume so it is no longer associated with the snap collider or interactable. /// /// The snap volume to be unregistered. /// public void UnregisterSnapVolume(XRInteractableSnapVolume snapVolume) { if (snapVolume == null) return; // This makes the assumption that the snap collider has not been changed after // the snap volume is registered. var snapCollider = snapVolume.snapCollider; if (snapCollider == null) return; if (m_ColliderToSnapVolumes.TryGetValue(snapCollider, out var associatedSnapVolume) && associatedSnapVolume == snapVolume) m_ColliderToSnapVolumes.Remove(snapCollider); } /// /// Returns all registered Interaction Groups into List . /// /// List to receive registered Interaction Groups. /// /// This method populates the list with the registered Interaction Groups at the time the /// method is called. It is not a live view, meaning Interaction Groups /// registered or unregistered afterward will not be reflected in the /// results of this method. /// Clears before adding to it. /// public void GetRegisteredInteractionGroups(List results) { if (results == null) throw new ArgumentNullException(nameof(results)); m_InteractionGroups.GetRegisteredItems(results); } /// /// Returns all registered Interactors into List . /// /// List to receive registered Interactors. /// /// This method populates the list with the registered Interactors at the time the /// method is called. It is not a live view, meaning Interactors /// registered or unregistered afterward will not be reflected in the /// results of this method. /// Clears before adding to it. /// /// public void GetRegisteredInteractors(List results) { if (results == null) throw new ArgumentNullException(nameof(results)); m_Interactors.GetRegisteredItems(results); } /// /// Returns all registered Interactables into List . /// /// List to receive registered Interactables. /// /// This method populates the list with the registered Interactables at the time the /// method is called. It is not a live view, meaning Interactables /// registered or unregistered afterward will not be reflected in the /// results of this method. /// Clears before adding to it. /// /// public void GetRegisteredInteractables(List results) { if (results == null) throw new ArgumentNullException(nameof(results)); m_Interactables.GetRegisteredItems(results); } /// /// Checks whether the is registered with this Interaction Manager. /// /// The Interaction Group to check. /// Returns if registered. Otherwise, returns . /// public bool IsRegistered(IXRInteractionGroup interactionGroup) { return m_InteractionGroups.IsRegistered(interactionGroup); } /// /// Checks whether the is registered with this Interaction Manager. /// /// The Interactor to check. /// Returns if registered. Otherwise, returns . /// public bool IsRegistered(IXRInteractor interactor) { return m_Interactors.IsRegistered(interactor); } /// /// Checks whether the is registered with this Interaction Manager. /// /// The Interactable to check. /// Returns if registered. Otherwise, returns . /// public bool IsRegistered(IXRInteractable interactable) { return m_Interactables.IsRegistered(interactable); } /// /// Gets the Interactable a specific is attached to. /// /// The collider of the Interactable to retrieve. /// The returned Interactable associated with the collider. /// Returns if an Interactable was associated with the collider. Otherwise, returns . public bool TryGetInteractableForCollider(Collider interactableCollider, out IXRInteractable interactable) { interactable = null; if (interactableCollider == null) return false; // Try direct association, and then fallback to snap volume association var hasDirectAssociation = m_ColliderToInteractableMap.TryGetValue(interactableCollider, out interactable); if (!hasDirectAssociation) { if (m_ColliderToSnapVolumes.TryGetValue(interactableCollider, out var snapVolume) && snapVolume != null) interactable = snapVolume.interactable; } return interactable != null && (!(interactable is Object unityObject) || unityObject != null); } /// /// Gets the Interactable a specific is attached to. /// /// The collider of the Interactable to retrieve. /// The returned Interactable associated with the collider. /// The returned snap volume associated with the collider. /// Returns if an Interactable was associated with the collider. Otherwise, returns . public bool TryGetInteractableForCollider(Collider interactableCollider, out IXRInteractable interactable, out XRInteractableSnapVolume snapVolume) { interactable = null; snapVolume = null; if (interactableCollider == null) return false; // Populate both out params var hasDirectAssociation = m_ColliderToInteractableMap.TryGetValue(interactableCollider, out interactable); if (m_ColliderToSnapVolumes.TryGetValue(interactableCollider, out snapVolume) && snapVolume != null) { if (hasDirectAssociation) { // Detect mismatch, ignore the snap volume if (snapVolume.interactable != interactable) snapVolume = null; } else { interactable = snapVolume.interactable; } } return interactable != null && (!(interactable is Object unityObject) || unityObject != null); } /// /// Checks if a given collider is paired with an interactable registered with the interaction mananger, or if it is paired to a snap volume. /// /// Collider to lookup /// True if collider is paired to either an interactable directly, or indirectly via a snap volume. public bool IsColliderRegisteredToInteractable(in Collider colliderToCheck) { return m_ColliderToInteractableMap.ContainsKey(colliderToCheck) || m_ColliderToSnapVolumes.ContainsKey(colliderToCheck); } /// /// Checks if the given is registered with a snap volume. /// /// Collider to check. /// True if the collider is paired to a snap volume. public bool IsColliderRegisteredSnapVolume(in Collider potentialSnapVolumeCollider) { return m_ColliderToSnapVolumes.ContainsKey(potentialSnapVolumeCollider); } /// /// Gets whether the given Interactable is the highest priority candidate for selection in this frame, useful for /// custom feedback. /// Only s that are configured to monitor Targets will be considered. /// /// The Interactable to check if it's the highest priority candidate for selection. /// (Optional) Returns the list of Interactors where the given Interactable has the highest priority for selection. /// Returns if the given Interactable is the highest priority candidate for selection. Otherwise, returns . /// /// Clears before adding to it. /// public bool IsHighestPriorityTarget(IXRSelectInteractable target, List interactors = null) { if (!m_HighestPriorityTargetMap.TryGetValue(target, out var targetPriorityInteractors)) return false; if (interactors == null) return true; interactors.Clear(); interactors.AddRange(targetPriorityInteractors); return true; } /// /// Checks if any registered interactor with the associated handedness is currently selecting any interactable. /// /// Hand to check, such as Left or Right. /// Returns if any registered interactor has the given handedness and is selecting any interactable. public bool IsHandSelecting(InteractorHandedness hand) { foreach (var interactor in m_Interactors.registeredSnapshot) { if (!m_Interactors.IsStillRegistered(interactor) || interactor.handedness != hand) continue; if (interactor is IXRSelectInteractor { hasSelection: true }) return true; } return false; } /// /// Retrieves the list of Interactables that the given Interactor could possibly interact with this frame. /// This list is sorted by priority (with highest priority first), and will only contain Interactables /// that are registered with this Interaction Manager. /// /// The Interactor to get valid targets for. /// The results list to populate with Interactables that are valid for selection, hover, or focus. /// /// Unity expects the 's implementation of to clear before adding to it. /// /// public void GetValidTargets(IXRInteractor interactor, List targets) { targets.Clear(); interactor.GetValidTargets(targets); // ReSharper disable once PossiblyImpureMethodCallOnReadonlyVariable -- ProfilerMarker.Begin with context object does not have Pure attribute using (s_FilterRegisteredValidTargetsMarker.Auto()) RemoveAllUnregistered(this, targets); } /// /// Removes all the Interactables from the given list that are not being handled by the manager. /// /// The Interaction Manager to check registration against. /// List of elements that will be filtered to exclude those not registered. /// Returns the number of elements removed from the list. /// /// Does not modify the manager at all, just the list. /// internal static int RemoveAllUnregistered(XRInteractionManager manager, List interactables) { var numRemoved = 0; for (var i = interactables.Count - 1; i >= 0; --i) { if (!manager.m_Interactables.IsRegistered(interactables[i])) { interactables.RemoveAt(i); ++numRemoved; } } return numRemoved; } /// /// Automatically called each frame during Update to clear the focus of the interaction group if necessary due to current conditions. /// /// The interaction group to potentially exit its focus state. protected virtual void ClearInteractionGroupFocus(IXRInteractionGroup interactionGroup) { // We want to unfocus whenever we select 'nothing' // If nothing is focused, then we are not in that scenario. // Otherwise, we check for selection activation with lack of selected object. var focusInteractor = interactionGroup.focusInteractor; var focusInteractable = interactionGroup.focusInteractable; if (focusInteractor == null || focusInteractable == null) return; var cleared = false; var selectInteractor = focusInteractor as IXRSelectInteractor; var selectInteractable = focusInteractable as IXRSelectInteractable; if (selectInteractor != null) cleared = (selectInteractor.isSelectActive && !selectInteractor.IsSelecting(selectInteractable)); if (cleared || !CanFocus(focusInteractor, focusInteractable)) { FocusExit(interactionGroup, interactionGroup.focusInteractable); } } void CancelInteractorFocus(IXRInteractor interactor) { var asGroupMember = interactor as IXRGroupMember; var group = asGroupMember?.containingGroup; if (group != null && group.focusInteractable != null) { FocusCancel(group, group.focusInteractable); } } /// /// Automatically called when an Interactable is unregistered to cancel the focus of the Interactable if necessary. /// /// The Interactable to potentially exit its focus state due to cancellation. public virtual void CancelInteractableFocus(IXRFocusInteractable interactable) { for (var i = interactable.interactionGroupsFocusing.Count - 1; i >= 0; --i) { FocusCancel(interactable.interactionGroupsFocusing[i], interactable); } } /// /// Automatically called each frame during Update to clear the selection of the Interactor if necessary due to current conditions. /// /// The Interactor to potentially exit its selection state. /// The list of interactables that this Interactor could possibly interact with this frame. /// protected internal virtual void ClearInteractorSelection(IXRSelectInteractor interactor, List validTargets) { if (interactor.interactablesSelected.Count == 0) return; m_CurrentSelected.Clear(); m_CurrentSelected.AddRange(interactor.interactablesSelected); // Performance optimization of the Contains checks by putting the valid targets into a HashSet. // Some Interactors like ARGestureInteractor can have hundreds of valid Interactables // since they will add most ARBaseGestureInteractable instances. m_UnorderedValidTargets.Clear(); if (validTargets.Count > 0) { foreach (var target in validTargets) { m_UnorderedValidTargets.Add(target); } } for (var i = m_CurrentSelected.Count - 1; i >= 0; --i) { var interactable = m_CurrentSelected[i]; // Selection, unlike hover, can control whether the interactable has to continue being a valid target // to automatically cause it to be deselected. if (!CanSelect(interactor, interactable) || (!interactor.keepSelectedTargetValid && !m_UnorderedValidTargets.Contains(interactable))) SelectExit(interactor, interactable); } } /// /// Automatically called when an Interactor is unregistered to cancel the selection of the Interactor if necessary. /// /// The Interactor to potentially exit its selection state due to cancellation. public virtual void CancelInteractorSelection(IXRSelectInteractor interactor) { for (var i = interactor.interactablesSelected.Count - 1; i >= 0; --i) { SelectCancel(interactor, interactor.interactablesSelected[i]); } } /// /// Automatically called when an Interactable is unregistered to cancel the selection of the Interactable if necessary. /// /// The Interactable to potentially exit its selection state due to cancellation. public virtual void CancelInteractableSelection(IXRSelectInteractable interactable) { for (var i = interactable.interactorsSelecting.Count - 1; i >= 0; --i) { SelectCancel(interactable.interactorsSelecting[i], interactable); } } /// /// Automatically called each frame during Update to clear the hover state of the Interactor if necessary due to current conditions. /// /// The Interactor to potentially exit its hover state. /// The list of interactables that this Interactor could possibly interact with this frame. /// protected internal virtual void ClearInteractorHover(IXRHoverInteractor interactor, List validTargets) { if (interactor.interactablesHovered.Count == 0) return; m_CurrentHovered.Clear(); m_CurrentHovered.AddRange(interactor.interactablesHovered); // Performance optimization of the Contains checks by putting the valid targets into a HashSet. // Some Interactors like ARGestureInteractor can have hundreds of valid Interactables // since they will add most ARBaseGestureInteractable instances. m_UnorderedValidTargets.Clear(); if (validTargets.Count > 0) { foreach (var target in validTargets) { m_UnorderedValidTargets.Add(target); } } for (var i = m_CurrentHovered.Count - 1; i >= 0; --i) { var interactable = m_CurrentHovered[i]; if (!CanHover(interactor, interactable) || !m_UnorderedValidTargets.Contains(interactable)) HoverExit(interactor, interactable); } } /// /// Automatically called when an Interactor is unregistered to cancel the hover state of the Interactor if necessary. /// /// The Interactor to potentially exit its hover state due to cancellation. public virtual void CancelInteractorHover(IXRHoverInteractor interactor) { for (var i = interactor.interactablesHovered.Count - 1; i >= 0; --i) { HoverCancel(interactor, interactor.interactablesHovered[i]); } } /// /// Automatically called when an Interactable is unregistered to cancel the hover state of the Interactable if necessary. /// /// The Interactable to potentially exit its hover state due to cancellation. public virtual void CancelInteractableHover(IXRHoverInteractable interactable) { for (var i = interactable.interactorsHovering.Count - 1; i >= 0; --i) { HoverCancel(interactable.interactorsHovering[i], interactable); } } /// /// Initiates focus of an Interactable by an Interactor. This method may first result in other interaction events /// such as causing the Interactable to first lose focus. /// /// The Interactor that is gaining focus. Must be a member of an Interaction group. /// The Interactable being focused. /// /// This attempt may be ignored depending on the focus policy of the Interactor and/or the Interactable. This attempt will also be ignored if the Interactor is not a member of an Interaction group. /// public virtual void FocusEnter(IXRInteractor interactor, IXRFocusInteractable interactable) { var asGroupMember = interactor as IXRGroupMember; var group = asGroupMember?.containingGroup; if (group == null || !CanFocus(interactor, interactable)) return; if (interactable.isFocused && !ResolveExistingFocus(group, interactable)) return; using (m_FocusEnterEventArgs.Get(out var args)) { args.manager = this; args.interactorObject = interactor; args.interactableObject = interactable; args.interactionGroup = group; FocusEnter(group, interactable, args); } } /// /// Initiates losing focus of an Interactable by an Interactor. /// /// The Interaction group that is losing focus. /// The Interactable that is no longer focused. public virtual void FocusExit(IXRInteractionGroup group, IXRFocusInteractable interactable) { var interactor = group.focusInteractor; using (m_FocusExitEventArgs.Get(out var args)) { args.manager = this; args.interactorObject = interactor; args.interactableObject = interactable; args.interactionGroup = group; args.isCanceled = false; FocusExit(group, interactable, args); } } /// /// Initiates losing focus of an Interactable by an Interaction group due to cancellation, /// such as from either being unregistered due to being disabled or destroyed. /// /// The Interaction group that is losing focus of the interactable. /// The Interactable that is no longer focused. public virtual void FocusCancel(IXRInteractionGroup group, IXRFocusInteractable interactable) { using (m_FocusExitEventArgs.Get(out var args)) { args.manager = this; args.interactorObject = group.focusInteractor; args.interactableObject = interactable; args.interactionGroup = group; args.isCanceled = true; FocusExit(group, interactable, args); } } /// /// Initiates selection of an Interactable by an Interactor. This method may first result in other interaction events /// such as causing the Interactable to first exit being selected. /// /// The Interactor that is selecting. /// The Interactable being selected. /// /// This attempt may be ignored depending on the selection policy of the Interactor and/or the Interactable. /// public virtual void SelectEnter(IXRSelectInteractor interactor, IXRSelectInteractable interactable) { if (interactable.isSelected && !ResolveExistingSelect(interactor, interactable)) return; using (m_SelectEnterEventArgs.Get(out var args)) { args.manager = this; args.interactorObject = interactor; args.interactableObject = interactable; SelectEnter(interactor, interactable, args); } if (interactable is IXRFocusInteractable focusInteractable) { FocusEnter(interactor, focusInteractable); } } /// /// Initiates ending selection of an Interactable by an Interactor. /// /// The Interactor that is no longer selecting. /// The Interactable that is no longer being selected. public virtual void SelectExit(IXRSelectInteractor interactor, IXRSelectInteractable interactable) { using (m_SelectExitEventArgs.Get(out var args)) { args.manager = this; args.interactorObject = interactor; args.interactableObject = interactable; args.isCanceled = false; SelectExit(interactor, interactable, args); } } /// /// Initiates ending selection of an Interactable by an Interactor due to cancellation, /// such as from either being unregistered due to being disabled or destroyed. /// /// The Interactor that is no longer selecting. /// The Interactable that is no longer being selected. public virtual void SelectCancel(IXRSelectInteractor interactor, IXRSelectInteractable interactable) { using (m_SelectExitEventArgs.Get(out var args)) { args.manager = this; args.interactorObject = interactor; args.interactableObject = interactable; args.isCanceled = true; SelectExit(interactor, interactable, args); } } /// /// Initiates hovering of an Interactable by an Interactor. /// /// The Interactor that is hovering. /// The Interactable being hovered over. public virtual void HoverEnter(IXRHoverInteractor interactor, IXRHoverInteractable interactable) { using (m_HoverEnterEventArgs.Get(out var args)) { args.manager = this; args.interactorObject = interactor; args.interactableObject = interactable; HoverEnter(interactor, interactable, args); } } /// /// Initiates ending hovering of an Interactable by an Interactor. /// /// The Interactor that is no longer hovering. /// The Interactable that is no longer being hovered over. public virtual void HoverExit(IXRHoverInteractor interactor, IXRHoverInteractable interactable) { using (m_HoverExitEventArgs.Get(out var args)) { args.manager = this; args.interactorObject = interactor; args.interactableObject = interactable; args.isCanceled = false; HoverExit(interactor, interactable, args); } } /// /// Initiates ending hovering of an Interactable by an Interactor due to cancellation, /// such as from either being unregistered due to being disabled or destroyed. /// /// The Interactor that is no longer hovering. /// The Interactable that is no longer being hovered over. public virtual void HoverCancel(IXRHoverInteractor interactor, IXRHoverInteractable interactable) { using (m_HoverExitEventArgs.Get(out var args)) { args.manager = this; args.interactorObject = interactor; args.interactableObject = interactable; args.isCanceled = true; HoverExit(interactor, interactable, args); } } /// /// Initiates focus of an Interactable by an interaction group, passing the given . /// /// The interaction group that is gaining focus. /// The Interactable being focused. /// Event data containing the interaction group and Interactable involved in the event. /// /// The interaction group and interactable are notified immediately without waiting for a previous call to finish /// in the case when this method is called again in a nested way. This means that if this method is /// called during the handling of the first event, the second will start and finish before the first /// event finishes calling all methods in the sequence to notify of the first event. /// // ReSharper disable PossiblyImpureMethodCallOnReadonlyVariable -- ProfilerMarker.Begin with context object does not have Pure attribute protected virtual void FocusEnter(IXRInteractionGroup group, IXRFocusInteractable interactable, FocusEnterEventArgs args) { Debug.Assert(args.interactableObject == interactable, this); Debug.Assert(args.interactionGroup == group, this); Debug.Assert(args.manager == this || args.manager == null, this); args.manager = this; using (s_FocusEnterMarker.Auto()) { group.OnFocusEntering(args); interactable.OnFocusEntering(args); interactable.OnFocusEntered(args); } lastFocused = interactable; focusGained?.Invoke(args); } /// /// Initiates losing focus of an Interactable by an Interaction Group, passing the given . /// /// The Interaction Group that is no longer selecting. /// The Interactable that is no longer being selected. /// Event data containing the Interactor and Interactable involved in the event. /// /// The interactable is notified immediately without waiting for a previous call to finish /// in the case when this method is called again in a nested way. This means that if this method is /// called during the handling of the first event, the second will start and finish before the first /// event finishes calling all methods in the sequence to notify of the first event. /// protected virtual void FocusExit(IXRInteractionGroup group, IXRFocusInteractable interactable, FocusExitEventArgs args) { Debug.Assert(args.interactorObject == group.focusInteractor, this); Debug.Assert(args.interactableObject == interactable, this); Debug.Assert(args.manager == this || args.manager == null, this); args.manager = this; using (s_FocusExitMarker.Auto()) { group.OnFocusExiting(args); interactable.OnFocusExiting(args); interactable.OnFocusExited(args); } if (interactable == lastFocused) lastFocused = null; focusLost?.Invoke(args); } /// /// Initiates selection of an Interactable by an Interactor, passing the given . /// /// The Interactor that is selecting. /// The Interactable being selected. /// Event data containing the Interactor and Interactable involved in the event. /// /// The interactor and interactable are notified immediately without waiting for a previous call to finish /// in the case when this method is called again in a nested way. This means that if this method is /// called during the handling of the first event, the second will start and finish before the first /// event finishes calling all methods in the sequence to notify of the first event. /// // ReSharper disable PossiblyImpureMethodCallOnReadonlyVariable -- ProfilerMarker.Begin with context object does not have Pure attribute protected virtual void SelectEnter(IXRSelectInteractor interactor, IXRSelectInteractable interactable, SelectEnterEventArgs args) { Debug.Assert(args.interactorObject == interactor, this); Debug.Assert(args.interactableObject == interactable, this); Debug.Assert(args.manager == this || args.manager == null, this); args.manager = this; using (s_SelectEnterMarker.Auto()) { interactor.OnSelectEntering(args); interactable.OnSelectEntering(args); interactor.OnSelectEntered(args); interactable.OnSelectEntered(args); } } /// /// Initiates ending selection of an Interactable by an Interactor, passing the given . /// /// The Interactor that is no longer selecting. /// The Interactable that is no longer being selected. /// Event data containing the Interactor and Interactable involved in the event. /// /// The interactor and interactable are notified immediately without waiting for a previous call to finish /// in the case when this method is called again in a nested way. This means that if this method is /// called during the handling of the first event, the second will start and finish before the first /// event finishes calling all methods in the sequence to notify of the first event. /// protected virtual void SelectExit(IXRSelectInteractor interactor, IXRSelectInteractable interactable, SelectExitEventArgs args) { Debug.Assert(args.interactorObject == interactor, this); Debug.Assert(args.interactableObject == interactable, this); Debug.Assert(args.manager == this || args.manager == null, this); args.manager = this; using (s_SelectExitMarker.Auto()) { interactor.OnSelectExiting(args); interactable.OnSelectExiting(args); interactor.OnSelectExited(args); interactable.OnSelectExited(args); } } /// /// Initiates hovering of an Interactable by an Interactor, passing the given . /// /// The Interactor that is hovering. /// The Interactable being hovered over. /// Event data containing the Interactor and Interactable involved in the event. /// /// The interactor and interactable are notified immediately without waiting for a previous call to finish /// in the case when this method is called again in a nested way. This means that if this method is /// called during the handling of the first event, the second will start and finish before the first /// event finishes calling all methods in the sequence to notify of the first event. /// protected virtual void HoverEnter(IXRHoverInteractor interactor, IXRHoverInteractable interactable, HoverEnterEventArgs args) { Debug.Assert(args.interactorObject == interactor, this); Debug.Assert(args.interactableObject == interactable, this); Debug.Assert(args.manager == this || args.manager == null, this); args.manager = this; using (s_HoverEnterMarker.Auto()) { interactor.OnHoverEntering(args); interactable.OnHoverEntering(args); interactor.OnHoverEntered(args); interactable.OnHoverEntered(args); } } /// /// Initiates ending hovering of an Interactable by an Interactor, passing the given . /// /// The Interactor that is no longer hovering. /// The Interactable that is no longer being hovered over. /// Event data containing the Interactor and Interactable involved in the event. /// /// The interactor and interactable are notified immediately without waiting for a previous call to finish /// in the case when this method is called again in a nested way. This means that if this method is /// called during the handling of the first event, the second will start and finish before the first /// event finishes calling all methods in the sequence to notify of the first event. /// protected virtual void HoverExit(IXRHoverInteractor interactor, IXRHoverInteractable interactable, HoverExitEventArgs args) { Debug.Assert(args.interactorObject == interactor, this); Debug.Assert(args.interactableObject == interactable, this); Debug.Assert(args.manager == this || args.manager == null, this); args.manager = this; using (s_HoverExitMarker.Auto()) { interactor.OnHoverExiting(args); interactable.OnHoverExiting(args); interactor.OnHoverExited(args); interactable.OnHoverExited(args); } } // ReSharper restore PossiblyImpureMethodCallOnReadonlyVariable /// /// Automatically called each frame during Update to enter the selection state of the Interactor if necessary due to current conditions. /// /// The Interactor to potentially enter its selection state. /// The list of interactables that this Interactor could possibly interact with this frame. /// /// If the Interactor implements and is configured to monitor Targets, this method will update its /// Targets For Selection property. /// /// protected internal virtual void InteractorSelectValidTargets(IXRSelectInteractor interactor, List validTargets) { if (validTargets.Count == 0) return; var targetPriorityInteractor = interactor as IXRTargetPriorityInteractor; var targetPriorityMode = TargetPriorityMode.None; if (targetPriorityInteractor != null) targetPriorityMode = targetPriorityInteractor.targetPriorityMode; var foundHighestPriorityTarget = false; foreach (var target in validTargets) { if (!(target is IXRSelectInteractable interactable)) continue; if (targetPriorityMode == TargetPriorityMode.None || targetPriorityMode == TargetPriorityMode.HighestPriorityOnly && foundHighestPriorityTarget) { if (CanSelect(interactor, interactable)) SelectEnter(interactor, interactable); } else if (IsSelectPossible(interactor, interactable)) { if (!foundHighestPriorityTarget) { foundHighestPriorityTarget = true; if (!m_HighestPriorityTargetMap.TryGetValue(interactable, out var interactorList)) { interactorList = s_TargetPriorityInteractorListPool.Get(); m_HighestPriorityTargetMap[interactable] = interactorList; } interactorList.Add(targetPriorityInteractor); } // ReSharper disable once PossibleNullReferenceException -- Guaranteed to not be null in this branch since not TargetPriorityMode.None targetPriorityInteractor.targetsForSelection?.Add(interactable); if (interactor.isSelectActive) SelectEnter(interactor, interactable); } } } /// /// Automatically called each frame during Update to enter the hover state of the Interactor if necessary due to current conditions. /// /// The Interactor to potentially enter its hover state. /// The list of interactables that this Interactor could possibly interact with this frame. /// protected internal virtual void InteractorHoverValidTargets(IXRHoverInteractor interactor, List validTargets) { if (validTargets.Count == 0) return; foreach (var target in validTargets) { if (target is IXRHoverInteractable interactable) { if (CanHover(interactor, interactable) && !interactor.IsHovering(interactable)) { HoverEnter(interactor, interactable); } } } } /// /// Automatically called when gaining focus of an Interactable by an interaction group is initiated /// and the Interactable is already focused. /// /// The interaction group that is gaining focus. /// The Interactable being focused. /// Returns if the existing focus was successfully resolved and focus should continue. /// Otherwise, returns if the focus should be ignored. /// protected virtual bool ResolveExistingFocus(IXRInteractionGroup interactionGroup, IXRFocusInteractable interactable) { Debug.Assert(interactable.isFocused, this); if (interactionGroup.focusInteractable == interactable) return false; switch (interactable.focusMode) { case InteractableFocusMode.Single: ExitInteractableFocus(interactable); break; case InteractableFocusMode.Multiple: break; default: Debug.Assert(false, $"Unhandled {nameof(InteractableFocusMode)}={interactable.focusMode}", this); return false; } return true; } /// /// Automatically called when selection of an Interactable by an Interactor is initiated /// and the Interactable is already selected. /// /// The Interactor that is selecting. /// The Interactable being selected. /// Returns if the existing selection was successfully resolved and selection should continue. /// Otherwise, returns if the select should be ignored. /// protected virtual bool ResolveExistingSelect(IXRSelectInteractor interactor, IXRSelectInteractable interactable) { Debug.Assert(interactable.isSelected, this); if (interactor.IsSelecting(interactable)) return false; switch (interactable.selectMode) { case InteractableSelectMode.Single: ExitInteractableSelection(interactable); break; case InteractableSelectMode.Multiple: break; default: Debug.Assert(false, $"Unhandled {nameof(InteractableSelectMode)}={interactable.selectMode}", this); return false; } return true; } /// /// Determines whether the Interactor and Interactable share at least one interaction layer /// between their Interaction Layer Masks. /// /// The Interactor to check. /// The Interactable to check. /// Returns if the Interactor and Interactable share at least one interaction layer. Otherwise, returns . /// /// protected static bool HasInteractionLayerOverlap(IXRInteractor interactor, IXRInteractable interactable) { return (interactor.interactionLayers & interactable.interactionLayers) != 0; } /// /// Returns the processing value of the filters in for the given Interactor and /// Interactable. /// /// The Interactor to be validated by the hover filters. /// The Interactable 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, IXRHoverInteractable interactable) { return XRFilterUtility.Process(m_HoverFilters, interactor, interactable); } /// /// Returns the processing value of the filters in for the given Interactor and /// Interactable. /// /// The Interactor to be validated by the select filters. /// The Interactable 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, IXRSelectInteractable interactable) { return XRFilterUtility.Process(m_SelectFilters, interactor, interactable); } void ExitInteractableSelection(IXRSelectInteractable interactable) { for (var i = interactable.interactorsSelecting.Count - 1; i >= 0; --i) { SelectExit(interactable.interactorsSelecting[i], interactable); } } void ExitInteractableFocus(IXRFocusInteractable interactable) { for (var i = interactable.interactionGroupsFocusing.Count - 1; i >= 0; --i) { FocusExit(interactable.interactionGroupsFocusing[i], interactable); } } void ClearPriorityForSelectionMap() { if (m_HighestPriorityTargetMap.Count == 0) return; foreach (var interactorList in m_HighestPriorityTargetMap.Values) { foreach (var interactor in interactorList) interactor?.targetsForSelection?.Clear(); s_TargetPriorityInteractorListPool.Release(interactorList); } m_HighestPriorityTargetMap.Clear(); } void FlushRegistration() { m_InteractionGroups.Flush(); m_Interactors.Flush(); m_Interactables.Flush(); } #if UNITY_EDITOR static string GetHierarchyPath(GameObject gameObject, bool includeScene = true) { #if UNITY_2021_3_OR_NEWER return SearchUtils.GetHierarchyPath(gameObject, includeScene); #else var sb = new StringBuilder(200); if (includeScene) { var sceneName = gameObject.scene.name; if (sceneName == string.Empty) { var prefabStage = PrefabStageUtility.GetPrefabStage(gameObject); if (prefabStage != null) sceneName = "Prefab Stage"; else sceneName = "Unsaved Scene"; } sb.Append("" + sceneName + ""); } sb.Append(GetTransformPath(gameObject.transform)); var path = sb.ToString(); return path; static string GetTransformPath(Transform tform) { if (tform.parent == null) return "/" + tform.name; return GetTransformPath(tform.parent) + "/" + tform.name; } #endif } #endif } }