using System; using System.Collections; using UnityEngine.XR.ARSubsystems; using UnityEngine.XR.Management; namespace UnityEngine.XR.ARFoundation { /// /// /// Controls the lifecycle and configuration options for an AR session. There /// is only one active session. If you have multiple components, /// they all communicate to the same session and will conflict with each other. /// /// Enabling or disabling the starts or stops the session, /// respectively. /// /// /// /// Related information: AR Session component /// [DisallowMultipleComponent] [DefaultExecutionOrder(ARUpdateOrder.k_Session)] [AddComponentMenu("XR/AR Foundation/AR Session")] [HelpURL("features/session")] public sealed class ARSession : SubsystemLifecycleManager { // Internal for tests internal static ARSessionState s_State; /// /// The reason AR tracking was lost. /// public static NotTrackingReason notTrackingReason => s_NotTrackingReason; static NotTrackingReason s_NotTrackingReason; static SessionAvailability s_Availability; int m_VSyncCount; int m_TargetFrameRate; bool m_WasFrameRateSet; [SerializeField] bool m_AttemptUpdate = true; /// /// If the device supports XR but does not have the necessary software, some platforms /// allow prompting the user to install or update the software. If /// is true, a software update will be attempted. If the appropriate software is not installed /// or out of date, and is false, then AR will not be available. /// public bool attemptUpdate { get => m_AttemptUpdate; set => m_AttemptUpdate = value; } [SerializeField] bool m_MatchFrameRate = true; /// /// If true, the underlying subsystem will attempt to synchronize the AR frame rate with Unity's. /// /// public bool matchFrameRateEnabled => descriptor?.supportsMatchFrameRate == true && subsystem.matchFrameRateEnabled; /// /// If true, the session will block execution until a new AR frame is available and set /// [Application.targetFrameRate](xref:UnityEngine.Application.targetFrameRate) /// to match the native update frequency of the AR session. /// Otherwise, the AR session is updated independently of the Unity frame. /// /// /// If enabled with a simple scene, the `ARSession.Update` might appear to take a long time. /// This is because your application is waiting for the next AR frame, similar to the way Unity will `WaitForTargetFPS` at the /// end of a frame. If the rest of the Unity frame takes non-trivial time, then the next `ARSession.Update` /// will take a proportionally smaller amount of time. /// /// This option does three things: /// - Enables a [setting on the XRSessionSubsystem](xref:UnityEngine.XR.ARSubsystems.XRSessionSubsystem.matchFrameRateRequested) which causes the update to block until the next AR frame is ready. /// - Sets [Application.targetFrameRate](xref:UnityEngine.Application.targetFrameRate) to the session's preferred update rate. /// - Sets [QualitySettings.vSyncCount](xref:UnityEngine.QualitySettings.vSyncCount) to zero. /// /// These settings are not reverted when the AR Session is disabled. /// public bool matchFrameRateRequested { get => descriptor?.supportsMatchFrameRate == true ? subsystem.matchFrameRateRequested : m_MatchFrameRate; set => SetMatchFrameRateRequested(value); } [SerializeField] TrackingMode m_TrackingMode = TrackingMode.PositionAndRotation; /// /// Get or set the TrackingMode for the session. /// public TrackingMode requestedTrackingMode { get => subsystem?.requestedTrackingMode.ToTrackingMode() ?? m_TrackingMode; set { m_TrackingMode = value; if (enabled && subsystem != null) subsystem.requestedTrackingMode = value.ToFeature(); } } /// /// Get the current TrackingMode in use by the session. /// public TrackingMode currentTrackingMode => subsystem?.currentTrackingMode.ToTrackingMode() ?? TrackingMode.DontCare; /// /// Get the number of AR frames produced per second, or null if the frame rate cannot be determined. /// public int? frameRate => descriptor?.supportsMatchFrameRate ?? false ? new int?(subsystem.frameRate) : null; /// /// This event is invoked whenever the changes. /// public static event Action stateChanged; /// /// The state of the entire system. Use this to determine the status of AR availability and installation. /// public static ARSessionState state { get => s_State; private set { if (s_State == value) return; s_State = value; UpdateNotTrackingReason(); if (stateChanged != null) stateChanged(new ARSessionStateChangedEventArgs(state)); } } /// /// Resets the AR Session. /// /// /// Resetting the session destroys all trackables and resets device tracking (for example, the position of the session /// is reset to the origin). /// public void Reset() { subsystem?.Reset(); if (state > ARSessionState.Ready) state = ARSessionState.SessionInitializing; } void SetMatchFrameRateRequested(bool value) { m_MatchFrameRate = value; if (descriptor?.supportsMatchFrameRate == true) subsystem.matchFrameRateRequested = value; } /// /// Prints a warning to the console if more than one /// component is active. There is only a single, global AR Session; this /// component controls that session. If two or more s are /// simultaneously active, then they both issue commands to the same session. /// Although this can cause unintended behavior, it is not expressly forbidden. /// /// This method is resource-intensive and should not be called frequently. /// static void WarnIfMultipleARSessions() { var sessions = FindObjectsByType(FindObjectsSortMode.None); if (sessions.Length > 1) { // Compile a list of session names string sessionNames = ""; foreach (var session in sessions) { sessionNames += $"\t{session.name}\n"; } Debug.LogWarningFormat( "Multiple active AR Sessions found. These will conflict with each other, so you should only have " + "one active ARSession at a time. Found these active sessions:\n{0}", sessionNames); } } static XRSessionSubsystem GetSubsystem() { if (XRGeneralSettings.Instance != null && XRGeneralSettings.Instance.Manager != null) { var loader = XRGeneralSettings.Instance.Manager.activeLoader; if (loader != null) return loader.GetLoadedSubsystem(); } return null; } /// /// Start checking the availability of XR on the current device. /// /// /// The availability check can be asynchronous, so this is implemented as a coroutine. /// It is safe to call this multiple times; if called a second time while an availability /// check is being made, it returns a new coroutine which waits on the first. /// /// An IEnumerator used for a coroutine. public static IEnumerator CheckAvailability() { // Wait if availability is currently being checked. while (state == ARSessionState.CheckingAvailability) { yield return null; } // Availability has already been determined if we make it here and the state is not None. if (state != ARSessionState.None) yield break; // Normally, the subsystem is created in OnEnable, but users may // want to check availability before enabling the session. var subsystem = GetSubsystem(); if (subsystem == null) { // No subsystem means there is no support on this platform. state = ARSessionState.Unsupported; } else if (state == ARSessionState.None) { state = ARSessionState.CheckingAvailability; var availabilityPromise = subsystem.GetAvailabilityAsync(); yield return availabilityPromise; s_Availability = availabilityPromise.result; if (s_Availability.IsSupported() && s_Availability.IsInstalled()) { state = ARSessionState.Ready; } else if (s_Availability.IsSupported() && !s_Availability.IsInstalled()) { bool supportsInstall = subsystem.subsystemDescriptor.supportsInstall; state = supportsInstall ? ARSessionState.NeedsInstall : ARSessionState.Unsupported; } else { state = ARSessionState.Unsupported; } } } /// /// Begin installing AR software on the current device (if supported). /// /// /// /// Installation can be asynchronous, so this is implemented as a coroutine. /// It is safe to call this multiple times, but you must first call . /// /// You must call before trying to start the installation, /// and the must not be /// or this method will throw . /// /// /// An IEnumerator used for a coroutine. /// Thrown if is /// or . public static IEnumerator Install() { while (state is ARSessionState.Installing or ARSessionState.CheckingAvailability) { yield return null; } switch (state) { case ARSessionState.Installing: case ARSessionState.NeedsInstall: break; case ARSessionState.None: throw new InvalidOperationException("Cannot install until availability has been determined. Have you called CheckAvailability()?"); case ARSessionState.Ready: case ARSessionState.SessionInitializing: case ARSessionState.SessionTracking: yield break; case ARSessionState.Unsupported: throw new InvalidOperationException("Cannot install because XR is not supported on this platform."); } // We can't get this far without having had a valid subsystem at one point. var subsystem = GetSubsystem(); if (subsystem == null) throw new InvalidOperationException("The subsystem was destroyed while attempting to install AR software."); state = ARSessionState.Installing; var installPromise = subsystem.InstallAsync(); yield return installPromise; var installStatus = installPromise.result; switch (installStatus) { case SessionInstallationStatus.Success: state = ARSessionState.Ready; s_Availability |= SessionAvailability.Installed; break; case SessionInstallationStatus.ErrorUserDeclined: state = ARSessionState.NeedsInstall; break; default: state = ARSessionState.Unsupported; break; } } /// /// Creates and initializes the session subsystem. Begins checking for availability. /// protected override void OnEnable() { #if DEVELOPMENT_BUILD || UNITY_EDITOR WarnIfMultipleARSessions(); #endif EnsureSubsystemInstanceSet(); // Cache these values and restore them in OnDisable m_VSyncCount = QualitySettings.vSyncCount; m_TargetFrameRate = Application.targetFrameRate; m_WasFrameRateSet = false; if (subsystem != null) { StartCoroutine(Initialize()); } #if DEVELOPMENT_BUILD else { Debug.LogWarning("No ARSession available for the current platform. " + "Please ensure you have installed the relevant XR Plug-in package " + "for this platform via XR Plug-in Management (Project Settings > XR Plug-in Management)." ); } #endif } IEnumerator Initialize() { // Make sure we've checked for availability if (state <= ARSessionState.CheckingAvailability) yield return CheckAvailability(); // Make sure we didn't get disabled while checking for availability if (!enabled) yield break; // Complete install if necessary if ((state == ARSessionState.NeedsInstall && attemptUpdate) || state == ARSessionState.Installing) { yield return Install(); } // If we're still enabled and everything is ready, then start. if (state == ARSessionState.Ready && enabled) StartSubsystem(); else enabled = false; } void StartSubsystem() { SetMatchFrameRateRequested(m_MatchFrameRate); subsystem.requestedTrackingMode = m_TrackingMode.ToFeature(); subsystem.Start(); } void Awake() { s_NotTrackingReason = NotTrackingReason.None; } void Update() { if (subsystem == null || !subsystem.running) return; m_TrackingMode = subsystem.requestedTrackingMode.ToTrackingMode(); if (subsystem.matchFrameRateEnabled && m_MatchFrameRate) { m_WasFrameRateSet = true; Application.targetFrameRate = subsystem.frameRate; QualitySettings.vSyncCount = 0; } subsystem.Update(new XRSessionUpdateParams { screenOrientation = Screen.orientation, screenDimensions = new Vector2Int(Screen.width, Screen.height) }); switch (subsystem.trackingState) { case TrackingState.None: case TrackingState.Limited: state = ARSessionState.SessionInitializing; break; case TrackingState.Tracking: state = ARSessionState.SessionTracking; break; } } void OnApplicationPause(bool paused) { if (subsystem == null) return; if (paused) subsystem.OnApplicationPause(); else subsystem.OnApplicationResume(); } /// /// Invoked when this `MonoBehaviour` is disabled. Affects the . /// protected override void OnDisable() { base.OnDisable(); // Only set back to ready if we were previously running if (state > ARSessionState.Ready) { state = ARSessionState.Ready; } // Restore values cached in OnEnable if (m_WasFrameRateSet) { QualitySettings.vSyncCount = m_VSyncCount; Application.targetFrameRate = m_TargetFrameRate; } } /// /// Invoked when this `MonoBehaviour` is destroyed. Affects the . /// protected override void OnDestroy() { base.OnDestroy(); // Only set back to ready if we were previously running if (state > ARSessionState.Ready) state = ARSessionState.Ready; } static void UpdateNotTrackingReason() { switch (state) { case ARSessionState.None: case ARSessionState.SessionInitializing: s_NotTrackingReason = GetSubsystem()?.notTrackingReason ?? NotTrackingReason.Unsupported; break; case ARSessionState.Unsupported: s_NotTrackingReason = NotTrackingReason.Unsupported; break; case ARSessionState.CheckingAvailability: case ARSessionState.NeedsInstall: case ARSessionState.Installing: case ARSessionState.Ready: case ARSessionState.SessionTracking: s_NotTrackingReason = NotTrackingReason.None; break; } } } }