using System; using System.Collections.Generic; using UnityEngine.XR.Interaction.Toolkit.Interactables; using UnityEngine.XR.Interaction.Toolkit.Interactors; using UnityEngine.XR.Interaction.Toolkit.Utilities.Pooling; namespace UnityEngine.XR.Interaction.Toolkit.Utilities { /// /// Use this class to maintain a registration of items (like Interactors or Interactables). This maintains /// a synchronized list that stays constant until buffered registration status changes are /// explicitly committed. /// /// The type of object to register, such as or . /// /// Items like Interactors and Interactables may be registered or unregistered (such as from an Interaction Manager) /// at any time, including when processing those items. This class can be used to manage those registration changes. /// For consistency with the functionality of Unity components which do not have /// Update called the same frame in which they are enabled, disabled, or destroyed, /// this class will maintain multiple lists to achieve that desired result with processing /// the items, these lists are pooled and reused between instances. /// abstract class BaseRegistrationList { /// /// Reusable list of buffered items (used to avoid unnecessary allocations when items to be added or removed /// need to be monitored). /// static readonly LinkedPool> s_BufferedListPool = new LinkedPool> (() => new List(), actionOnRelease: list => list.Clear(), collectionCheck: false); /// /// A snapshot of registered items that should potentially be processed this update phase of the current frame. /// The count of items shall only change upon a call to . /// /// /// Items being in this collection does not imply that the item is currently registered. ///
/// Logically this should be an but is kept as a /// to avoid allocations when iterating. Use and /// instead of directly changing this list. ///
public List registeredSnapshot { get; } = new List(); /// /// Gets the number of registered items including the effective result of buffered registration changes. /// public int flushedCount => registeredSnapshot.Count - bufferedRemoveCount + bufferedAddCount; /// /// List with buffered items to be added when calling . /// The count of items shall only change upon a call to , /// or . /// This list can be , use to check if there are elements /// before directly accessing it. A new list is pooled from when needed. /// /// /// Logically this should be an but is kept as a to avoid /// allocations when iterating. /// protected List m_BufferedAdd; /// /// List with buffered items to be removed when calling . /// The count of items shall only change upon a call to , /// or . /// This list can be , use to check if there are elements /// before directly accessing it. A new list is pooled from when needed. /// /// /// Logically this should be an but is kept as a to avoid /// allocations when iterating. /// protected List m_BufferedRemove; /// /// The number of buffered items to be added when calling . /// protected int bufferedAddCount => m_BufferedAdd?.Count ?? 0; /// /// The number of buffered items to be removed when calling . /// protected int bufferedRemoveCount => m_BufferedRemove?.Count ?? 0; /// /// Adds the given item to the list. /// /// The item to be added. /// /// Gets a new list from the if needed. /// protected void AddToBufferedAdd(T item) { if (m_BufferedAdd == null) m_BufferedAdd = s_BufferedListPool.Get(); m_BufferedAdd.Add(item); } /// /// Removes the given item from the list. /// /// The item to be removed. /// Returns if the item was successfully removed. Otherwise, returns . protected bool RemoveFromBufferedAdd(T item) => m_BufferedAdd != null && m_BufferedAdd.Remove(item); /// /// Removes all items from the and returns this list to the pool (). /// protected void ClearBufferedAdd() { if (m_BufferedAdd == null) return; s_BufferedListPool.Release(m_BufferedAdd); m_BufferedAdd = null; } /// /// Adds the given item to the list. /// /// The item to be added. /// /// Gets a new list from the if needed. /// protected void AddToBufferedRemove(T item) { if (m_BufferedRemove == null) m_BufferedRemove = s_BufferedListPool.Get(); m_BufferedRemove.Add(item); } /// /// Removes the given item from the list. /// /// The item to be removed. /// Returns if the item was successfully removed. Otherwise, returns . protected bool RemoveFromBufferedRemove(T item) => m_BufferedRemove != null && m_BufferedRemove.Remove(item); /// /// Removes all items from the and returns this list to the pool (). /// protected void ClearBufferedRemove() { if (m_BufferedRemove == null) return; s_BufferedListPool.Release(m_BufferedRemove); m_BufferedRemove = null; } /// /// Checks the registration status of . /// /// The item to query. /// Returns if registered. Otherwise, returns . /// /// This includes pending changes that have not yet been pushed to . /// /// public abstract bool IsRegistered(T item); /// /// Faster variant of that assumes that the is in the snapshot. /// It short circuits the check when there are no pending changes to unregister, which is usually the case. /// /// The item to query. /// Returns if registered. /// /// This includes pending changes that have not yet been pushed to . /// Use this method instead of when iterating over /// for improved performance. /// /// public abstract bool IsStillRegistered(T item); /// /// Register . /// /// The item to register. /// Returns if a change in registration status occurred. Otherwise, returns . public abstract bool Register(T item); /// /// Unregister . /// /// The item to unregister. /// Returns if a change in registration status occurred. Otherwise, returns . public abstract bool Unregister(T item); /// /// Flush pending registration changes into . /// public abstract void Flush(); /// /// Return all registered items into List in the order they were registered. /// /// List to receive registered items. /// /// Clears before adding to it. /// public abstract void GetRegisteredItems(List results); /// /// Returns the registered item at based on the order they were registered. /// /// Index of the item to return. Must be smaller than and not negative. /// Returns the item at the given index. public abstract T GetRegisteredItemAt(int index); /// /// Moves the given item in the registration list. Takes effect immediately without calling . /// If the item is not in the registration list, this can be used to insert the item at the specified index. /// /// The item to move or register. /// New index of the item. /// Returns if the item was registered as a result of this method, otherwise returns . /// Throws when there are pending registration changes that have not been flushed. public bool MoveItemImmediately(T item, int newIndex) { if (bufferedRemoveCount != 0 || bufferedAddCount != 0) throw new InvalidOperationException("Cannot move item when there are pending registration changes that have not been flushed."); var currentIndex = registeredSnapshot.IndexOf(item); if (currentIndex == newIndex) return false; if (currentIndex >= 0) registeredSnapshot.RemoveAt(currentIndex); registeredSnapshot.Insert(newIndex, item); OnItemMovedImmediately(item, newIndex); return currentIndex < 0; } /// /// Called after the given item has been inserted at or moved to the specified index. /// /// The item that was moved or registered. /// New index of the item. protected virtual void OnItemMovedImmediately(T item, int newIndex) { } /// /// Unregister all currently registered items. Starts from the last registered item and proceeds backward /// until the first registered item is unregistered. /// public void UnregisterAll() { using (s_BufferedListPool.Get(out var registeredItems)) { GetRegisteredItems(registeredItems); for (var i = registeredItems.Count - 1; i >= 0; --i) Unregister(registeredItems[i]); } } protected static void EnsureCapacity(List list, int capacity) { if (list.Capacity < capacity) list.Capacity = capacity; } } /// sealed class RegistrationList : BaseRegistrationList { readonly HashSet m_UnorderedBufferedAdd = new HashSet(); readonly HashSet m_UnorderedBufferedRemove = new HashSet(); readonly HashSet m_UnorderedRegisteredSnapshot = new HashSet(); readonly HashSet m_UnorderedRegisteredItems = new HashSet(); /// /// Equivalent to m_UnorderedBufferedRemove.Count == 0. /// /// /// This profiled slightly faster than checking the count directly, and is used by /// which is called during each loop iteration for every registered item. /// bool m_BufferedRemoveEmpty = true; /// public override bool IsRegistered(T item) => m_UnorderedRegisteredItems.Contains(item); /// public override bool IsStillRegistered(T item) => m_BufferedRemoveEmpty || !m_UnorderedBufferedRemove.Contains(item); /// public override bool Register(T item) { if (m_UnorderedBufferedAdd.Count > 0 && m_UnorderedBufferedAdd.Contains(item)) return false; var snapshotContainsItem = m_UnorderedRegisteredSnapshot.Contains(item); if ((!m_BufferedRemoveEmpty && m_UnorderedBufferedRemove.Remove(item)) || !snapshotContainsItem) { RemoveFromBufferedRemove(item); m_BufferedRemoveEmpty = m_UnorderedBufferedRemove.Count == 0; m_UnorderedRegisteredItems.Add(item); if (!snapshotContainsItem) { AddToBufferedAdd(item); m_UnorderedBufferedAdd.Add(item); } return true; } return false; } /// public override bool Unregister(T item) { if (!m_BufferedRemoveEmpty && m_UnorderedBufferedRemove.Contains(item)) return false; if (m_UnorderedBufferedAdd.Count > 0 && m_UnorderedBufferedAdd.Remove(item)) { RemoveFromBufferedAdd(item); m_UnorderedRegisteredItems.Remove(item); return true; } if (m_UnorderedRegisteredSnapshot.Contains(item)) { AddToBufferedRemove(item); m_UnorderedBufferedRemove.Add(item); m_BufferedRemoveEmpty = false; m_UnorderedRegisteredItems.Remove(item); return true; } return false; } /// public override void Flush() { // This method is called multiple times each frame, // so additional explicit Count checks are done for // performance. if (!m_BufferedRemoveEmpty) { foreach (var item in m_BufferedRemove) { registeredSnapshot.Remove(item); m_UnorderedRegisteredSnapshot.Remove(item); } ClearBufferedRemove(); m_UnorderedBufferedRemove.Clear(); m_BufferedRemoveEmpty = true; } if (bufferedAddCount > 0) { foreach (var item in m_BufferedAdd) { if (!m_UnorderedRegisteredSnapshot.Contains(item)) { registeredSnapshot.Add(item); m_UnorderedRegisteredSnapshot.Add(item); } } ClearBufferedAdd(); m_UnorderedBufferedAdd.Clear(); } } /// public override void GetRegisteredItems(List results) { if (results == null) throw new ArgumentNullException(nameof(results)); results.Clear(); EnsureCapacity(results, flushedCount); foreach (var item in registeredSnapshot) { if (!m_BufferedRemoveEmpty && m_UnorderedBufferedRemove.Contains(item)) continue; results.Add(item); } if (bufferedAddCount > 0) results.AddRange(m_BufferedAdd); } /// public override T GetRegisteredItemAt(int index) { if (index < 0 || index >= flushedCount) throw new ArgumentOutOfRangeException(nameof(index), "Index was out of range. Must be non-negative and less than the size of the registration collection."); if (bufferedRemoveCount == 0 && bufferedAddCount == 0) return registeredSnapshot[index]; if (index >= registeredSnapshot.Count - bufferedRemoveCount) return m_BufferedAdd[index - (registeredSnapshot.Count - bufferedRemoveCount)]; var effectiveIndex = 0; foreach (var item in registeredSnapshot) { if (m_UnorderedBufferedRemove.Contains(item)) continue; if (effectiveIndex == index) return registeredSnapshot[index]; ++effectiveIndex; } // Unreachable code throw new ArgumentOutOfRangeException(nameof(index), "Index was out of range. Must be non-negative and less than the size of the registration collection."); } /// protected override void OnItemMovedImmediately(T item, int newIndex) { base.OnItemMovedImmediately(item, newIndex); m_UnorderedRegisteredItems.Add(item); m_UnorderedRegisteredSnapshot.Add(item); } } }