using System; using System.Collections.Generic; using Unity.Collections; using UnityEngine.XR.ARSubsystems; using UnityEngine.SubsystemsImplementation; namespace UnityEngine.XR.ARFoundation { /// /// A generic manager for components generated by features detected in the physical environment. /// /// /// When the manager is informed that a trackable has been added, a new GameObject /// is created with an ARTrackable component on it. If /// /// is not null, then that Prefab will be instantiated. /// /// The Subsystem which provides this manager data. /// The SubsystemDescriptor required to create the Subsystem. /// The [provider](xref:UnityEngine.SubsystemsImplementation.SubsystemProvider) associated with the . /// A concrete struct used to hold data provided by the Subsystem. /// The type of component that this component will manage (that is, create, update, and destroy). [RequireComponent(typeof(ARSessionOrigin))] [DisallowMultipleComponent] public abstract class ARTrackableManager : SubsystemLifecycleManager where TSubsystem : TrackingSubsystem, new() where TSubsystemDescriptor : SubsystemDescriptorWithProvider where TProvider : SubsystemProvider where TSessionRelativeData : struct, ITrackable where TTrackable : ARTrackable { internal static ARTrackableManager instance { get; private set; } /// /// A collection of all trackables managed by this component. /// public TrackableCollection trackables => new TrackableCollection(m_Trackables); /// /// Iterates over every instantiated and /// activates or deactivates its GameObject based on the value of /// . /// This calls /// GameObject.SetActive /// on each trackable's GameObject. /// /// If true each trackable's GameObject is activated. /// Otherwise, it is deactivated. public void SetTrackablesActive(bool active) { foreach (var trackable in trackables) { trackable.gameObject.SetActive(active); } } /// /// The ARSessionOrigin which will be used to instantiate detected trackables. /// protected ARSessionOrigin sessionOrigin { get; private set; } /// /// The name prefix that should be used when instantiating new GameObjects. /// protected abstract string gameObjectName { get; } /// /// The Prefab that should be instantiated when adding a trackable. Can be `null`. /// /// The prefab should be instantiated when adding a trackable. protected virtual GameObject GetPrefab() => null; /// /// A dictionary of all trackables, keyed by TrackableId. /// protected Dictionary m_Trackables = new Dictionary(); /// /// A dictionary of trackables added via but not yet reported as added. /// protected Dictionary m_PendingAdds = new Dictionary(); /// /// Invoked by Unity once when this component wakes up. /// protected virtual void Awake() { sessionOrigin = GetComponent(); } /// protected override void OnEnable() { base.OnEnable(); instance = this; sessionOrigin.trackablesParentTransformChanged += OnTrackablesParentTransformChanged; } /// protected override void OnDisable() { base.OnDisable(); sessionOrigin.trackablesParentTransformChanged -= OnTrackablesParentTransformChanged; } /// /// Determines whether an existing can be added /// to the underlying subsystem. /// /// /// If has not been added yet (that is, it is not tracked by this manager) and the /// manager is either disabled or does not have a valid subsystem, then the 's /// state is set to `true`. /// /// An existing to add to /// the underlying subsystem. /// Returns `true` if this manager is enabled, has a valid subsystem, and /// is not already being tracked by this manager. Returns `false` otherwise. /// Thrown if is `null`. protected bool CanBeAddedToSubsystem(TTrackable trackable) { if (trackable == null) throw new ArgumentNullException(nameof(trackable)); // If it already has a valid trackableId, then don't re-add it if (!trackable.trackableId.Equals(TrackableId.invalidId)) return false; // If we already know about it, then early out if (m_Trackables.ContainsKey(trackable.trackableId)) return false; if (!enabled || subsystem == null) { // If the manager is disabled or there is no subsystem, and we don't already know about // this trackable, then it must be pending. trackable.pending = true; return false; } // Finally, we can only add it if we have a session origin. return sessionOrigin && sessionOrigin.trackablesParent; } void OnTrackablesParentTransformChanged(ARTrackablesParentTransformChangedEventArgs eventArgs) { foreach (var trackable in trackables) { var transform = trackable.transform; if (transform.parent != eventArgs.trackablesParent) { var desiredPose = eventArgs.trackablesParent.TransformPose(trackable.sessionRelativePose); transform.SetPositionAndRotation(desiredPose.position, desiredPose.rotation); } } } /// /// Update is called once per frame. This component's internal state /// is first updated, and then an event notifying whether any trackables have been added, removed, or updated /// is invoked by the derived manager. /// protected virtual void Update() { if (subsystem == null || !subsystem.running) return; using (new ScopedProfiler("GetChanges")) using (var changes = subsystem.GetChanges(Allocator.Temp)) { using (new ScopedProfiler("ProcessAdded")) { ClearAndSetCapacity(s_Added, changes.added.Length); foreach (var added in changes.added) { s_Added.Add(CreateOrUpdateTrackable(added)); } } using (new ScopedProfiler("ProcessUpdated")) { ClearAndSetCapacity(s_Updated, changes.updated.Length); foreach (var updated in changes.updated) { s_Updated.Add(CreateOrUpdateTrackable(updated)); } } using (new ScopedProfiler("ProcessRemoved")) { ClearAndSetCapacity(s_Removed, changes.removed.Length); foreach (var trackableId in changes.removed) { if (m_Trackables.TryGetValue(trackableId, out var trackable)) { m_Trackables.Remove(trackableId); if (trackable) { s_Removed.Add(trackable); } } } } } try { // User events if ((s_Added.Count) > 0 || (s_Updated.Count) > 0 || (s_Removed.Count) > 0) { OnTrackablesChanged(s_Added, s_Updated, s_Removed); } } finally { // Make sure destroy happens even if a user callback throws an exception foreach (var removed in s_Removed) DestroyTrackable(removed); } } /// /// Invoked when trackables have change (that is, they were added, updated, or removed). /// Use this to perform additional logic, or to invoke public events /// related to your trackables. /// /// A list of trackables added this frame. /// A list of trackables updated this frame. /// A list of trackables removed this frame. /// The trackable components are not destroyed until after this method returns. protected virtual void OnTrackablesChanged( List added, List updated, List removed) { } /// /// Invoked after creating the trackable. The trackable's sessionRelativeData property will already be set. /// /// The newly created trackable. protected virtual void OnCreateTrackable(TTrackable trackable) { } /// /// Invoked just after session-relative data has been set on a trackable. /// /// The trackable that has just been updated. /// The session relative data used to update the trackable. protected virtual void OnAfterSetSessionRelativeData( TTrackable trackable, TSessionRelativeData sessionRelativeData) { } /// /// Creates a immediately and leaves it in a "pending" state. /// /// /// /// Trackables are usually created, updated, or destroyed during . /// This method creates a trackable immediately and marks it as "pending" /// until it is reported as added by the subsystem. This is useful for subsystems that deal /// with trackables that can be both detected and manually created. /// /// This method does not invoke , /// so no "added" notifications will occur until the next call to . /// /// The trackable will appear in the collection immediately. /// /// /// The data associated with the trackable. All spatial data should /// be relative to the . /// A new TTrackable protected TTrackable CreateTrackableImmediate(TSessionRelativeData sessionRelativeData) { var trackable = CreateOrUpdateTrackable(sessionRelativeData); trackable.pending = true; m_PendingAdds.Add(trackable.trackableId, trackable); return trackable; } /// /// If in a "pending" state and is /// `true`, this method destroys the trackable's GameObject. Otherwise, this method has no effect. /// /// /// /// Trackables are usually removed only when the subsystem reports they /// have been removed during . /// /// This method will immediately remove a trackable only if it was created by /// /// and has not yet been reported as added by the /// . /// /// This can happen if the trackable is created and removed within the same frame, as the subsystem might never /// have a chance to report its existence. Derived classes should use this /// if they support the concept of manual addition and removal of trackables, as there might not /// be a removal event if the trackable is added and removed quickly. /// /// If the trackable is not in a pending state (that is, it has already been reported as "added"), /// then this method does nothing. /// /// /// This method does not invoke , /// so no "removed" notifications will occur until the next call to . "Removed" notifications will only /// occur if it was previously reported as "added". /// /// /// The id of the trackable to destroy. /// True if the trackable is "pending" (that is, not yet reported as "added"). protected bool DestroyPendingTrackable(TrackableId trackableId) { if (m_PendingAdds.TryGetValue(trackableId, out var trackable)) { m_PendingAdds.Remove(trackableId); m_Trackables.Remove(trackableId); DestroyTrackable(trackable); return true; } return false; } static void ClearAndSetCapacity(List list, int capacity) { list.Clear(); if (list.Capacity < capacity) list.Capacity = capacity; } string GetTrackableName(TrackableId trackableId) { return gameObjectName + " " + trackableId.ToString(); } (GameObject gameObject, bool shouldBeActive) CreateGameObjectDeactivated() { var prefab = GetPrefab(); if (prefab == null) { var gameObject = new GameObject(); gameObject.SetActive(false); gameObject.transform.parent = sessionOrigin.trackablesParent; return (gameObject, true); } else { var active = prefab.activeSelf; prefab.SetActive(false); var gameObject = Instantiate(prefab, sessionOrigin.trackablesParent); prefab.SetActive(active); return (gameObject, active); } } (GameObject gameObject, bool shouldBeActive) CreateGameObjectDeactivated(string name) { var tuple = CreateGameObjectDeactivated(); tuple.gameObject.name = name; return tuple; } (GameObject gameObject, bool shouldBeActive) CreateGameObjectDeactivated(TrackableId trackableId) { using (new ScopedProfiler("CreateGameObject")) { return CreateGameObjectDeactivated(GetTrackableName(trackableId)); } } TTrackable CreateTrackable(TSessionRelativeData sessionRelativeData) { var (gameObject, shouldBeActive) = CreateGameObjectDeactivated(sessionRelativeData.trackableId); var trackable = gameObject.GetComponent(); if (trackable == null) { trackable = gameObject.AddComponent(); } m_Trackables.Add(sessionRelativeData.trackableId, trackable); SetSessionRelativeData(trackable, sessionRelativeData); trackable.gameObject.SetActive(shouldBeActive); return trackable; } void SetSessionRelativeData(TTrackable trackable, TSessionRelativeData data) { trackable.SetSessionRelativeData(data); var worldSpacePose = sessionOrigin.trackablesParent.TransformPose(data.pose); trackable.transform.SetPositionAndRotation(worldSpacePose.position, worldSpacePose.rotation); } /// /// Creates the native counterpart for an existing /// added with a call to [AddComponent](xref:UnityEngine.GameObject.AddComponent), for example. /// /// The existing trackable component. /// The AR data associated with the trackable. This usually comes from the /// trackable's associated [subsystem](xref:UnityEngine.Subsystem) protected void CreateTrackableFromExisting(TTrackable existingTrackable, TSessionRelativeData sessionRelativeData) { // Same as CreateOrUpdateTrackable var trackableId = sessionRelativeData.trackableId; m_Trackables.Add(trackableId, existingTrackable); SetSessionRelativeData(existingTrackable, sessionRelativeData); OnCreateTrackable(existingTrackable); OnAfterSetSessionRelativeData(existingTrackable, sessionRelativeData); existingTrackable.OnAfterSetSessionRelativeData(); // Remaining logic from CreateTrackableImmediate m_PendingAdds.Add(trackableId, existingTrackable); existingTrackable.pending = true; } TTrackable CreateOrUpdateTrackable(TSessionRelativeData sessionRelativeData) { var trackableId = sessionRelativeData.trackableId; if (m_Trackables.TryGetValue(trackableId, out var trackable)) { m_PendingAdds.Remove(trackableId); trackable.pending = false; SetSessionRelativeData(trackable, sessionRelativeData); } else { trackable = CreateTrackable(sessionRelativeData); OnCreateTrackable(trackable); } OnAfterSetSessionRelativeData(trackable, sessionRelativeData); trackable.OnAfterSetSessionRelativeData(); return trackable; } void DestroyTrackable(TTrackable trackable) { if (trackable.destroyOnRemoval) { Destroy(trackable.gameObject); } } static List s_Added = new List(); static List s_Updated = new List(); static List s_Removed = new List(); } }