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
}
}