Enable Unity XR in Runtime

TL;DR

  1. How to enable Unity’s XR in runtime
  2. Fix “Failed to set DeveloperMode on Start.” error

It’s been a while since I wrote a technical blog. And it’s been a while since I developed for VR. Encountering a few issues while getting up to date with the latter, I decided to write about my troubles and pass my knowledge to the future generation of young and naive VR developers. Let’s get to business, shall we?

The business

If you need to the VR the second your application starts, all you need to do is to set the “Initialize XR on Startup” setting to true.

Initialize XR on Startup

Unfortunately, in my case, keeping VR enabled during the lifetime of the app was causing some issues. So, as stated in the heading, I needed to enable-disable it in runtime. After a quick search online, I got this script:

using System.Collections;
using UnityEngine.XR.Management;

namespace EnablingXR
{
	public static class XRController
	{
		/// <summary>
		/// Initializes XR
		/// </summary>
		public static IEnumerator EnableXRCoroutine()
		{
			// Make sure we don't have an active loader already
			if (!XRGeneralSettings.Instance.Manager.activeLoader)
			{
				yield return XRGeneralSettings.Instance.Manager.InitializeLoader();
			}

			// Make sure we have an active loader, and the manager is initialized
			if (XRGeneralSettings.Instance.Manager.activeLoader &&
			    XRGeneralSettings.Instance.Manager.isInitializationComplete)
			{
				XRGeneralSettings.Instance.Manager.StartSubsystems();
			}
		}

		/// <summary>
		/// Disables XR
		/// </summary>
		public static void DisableXR()
		{
			// Make sure there is something to de-initialize
			if (XRGeneralSettings.Instance.Manager.isInitializationComplete)
			{
				XRGeneralSettings.Instance.Manager.StopSubsystems();
				XRGeneralSettings.Instance.Manager.DeinitializeLoader();
			}
		}
	}
}

Added this script to the project, started EnableXRCoroutine from a relevant place, and everything worked! Or did it?

The error

The first time I ran the script, everything went great. The trouble started on the consecutive runs. Oculus wasn’t starting, and I was seeing the error:

Failed to set DeveloperMode on Start.
UnityEngine.Debug:LogError (object)
Unity.XR.Oculus.Development:OverrideDeveloperModeStart () (at ./Library/PackageCache/com.unity.xr.oculus@4.0.0/Runtime/OculusDevelopment.cs:39)
Unity.XR.Oculus.OculusLoader:Start () (at ./Library/PackageCache/com.unity.xr.oculus@4.0.0/Runtime/OculusLoader.cs:211)
UnityEngine.XR.Management.XRManagerSettings:StartSubsystems ()
EnablingXR.XRController/<EnableXRCoroutine>d__0:MoveNext () (at Assets/Scripts/XRController.cs:23)
UnityEngine.MonoBehaviour:StartCoroutine (System.Collections.IEnumerator)
EnablingXR.Initializer:Start () (at Assets/Scripts/Initializer.cs:9)

After about half a day of tinkering, and blaming everything from a spotty Wi-Fi connection, the Oculus package to Unity itself, etc., it finally hit me! The first run was working perfectly fine. I was getting this error on consecutive runs! Which meant there was a problem with de-initialization. Surely enough, checking the XRGeneralSettings.Instance.Manager.activeLoader I saw that it already was there, even though I didn’t call the InitializeLoader() method yet. The activeLoader was a leftover from the previous run.

The solution

Once I discovered the problem, the solution was clear: de-initialize the XR plugin before initializing it. Adding a check to the EnableXRCoroutine() did solve the issue. Here is the final working script:

using System.Collections;
using UnityEngine.XR.Management;

namespace EnablingXR
{
	public static class XRController
	{
		/// <summary>
		/// Initializes XR
		/// </summary>
		public static IEnumerator EnableXRCoroutine()
		{
			// Make sure the XR is disabled and properly disposed. It can happen that there is an activeLoader left
			// from the previous run.
			if (XRGeneralSettings.Instance.Manager.activeLoader ||
			    XRGeneralSettings.Instance.Manager.isInitializationComplete)
			{
				DisableXR();
              	// Wait for the next frame, just in case
				yield return null;
			}

			// Make sure we don't have an active loader already
			if (!XRGeneralSettings.Instance.Manager.activeLoader)
			{
				yield return XRGeneralSettings.Instance.Manager.InitializeLoader();
			}

			// Make sure we have an active loader, and the manager is initialized
			if (XRGeneralSettings.Instance.Manager.activeLoader &&
			    XRGeneralSettings.Instance.Manager.isInitializationComplete)
			{
				XRGeneralSettings.Instance.Manager.StartSubsystems();
			}
		}

		/// <summary>
		/// Disables XR
		/// </summary>
		public static void DisableXR()
		{
			// Make sure there is something to de-initialize
			if (XRGeneralSettings.Instance.Manager.isInitializationComplete)
			{
				XRGeneralSettings.Instance.Manager.StopSubsystems();
				XRGeneralSettings.Instance.Manager.DeinitializeLoader();
			}
		}
	}
}

Additional considerations

Admittedly, this solution feels a bit hacky. We should dispose of any loaders and settings at the end of each run. In fact, it’s Unity’s job! I hit stop, I expect it to clean after itself! But it doesn’t, so here we are.

Anyway… My first solution was to call DisableXR from OnDestroy or OnDisable. It didn’t work for some reason, so I moved that check code where it is now. The idea is to make sure everything is de-initialized before calling initialization methods. As long as you do that, everything should be fine. Also, this is not an issue in the actual build.

Additionally, I was using Oculus’ body and face tracking (OVRBody, and OVRFaceExpressions). At first, neither of those worked. Upon further investigation, I found out that those scripts are automatically disabled if the XR plugin is not enabled, which makes sense. You don’t want them running if the Oculus is not working. But they are not automatically enabled! So don’t forget to enable relevant scripts after the initialization. I passed the OVRBody, and OVRFaceExpressions components to the EnableXRCroutine(). Here is what I ended up with:

using System.Collections;
using UnityEngine.XR.Management;

namespace EnablingXR
{
	public static class XRController
	{
		/// <summary>
		/// Initializes XR
		/// </summary>
		public static IEnumerator EnableXRCoroutine(OVRBody bodyToTrack, OVRFaceExpressions faceExpressionsToTrack)
		{
			// Make sure the XR is disabled and properly disposed. It can happen that there is an activeLoader left
			// from the previous run.
			if (XRGeneralSettings.Instance.Manager.activeLoader ||
			    XRGeneralSettings.Instance.Manager.isInitializationComplete)
			{
				DisableXR();
				yield return null;
			}

			// Make sure we don't have an active loader already
			if (!XRGeneralSettings.Instance.Manager.activeLoader)
			{
				yield return XRGeneralSettings.Instance.Manager.InitializeLoader();
			}

			// Make sure we have an active loader, and the manager is initialized
			if (XRGeneralSettings.Instance.Manager.activeLoader &&
			    XRGeneralSettings.Instance.Manager.isInitializationComplete)
			{
				XRGeneralSettings.Instance.Manager.StartSubsystems();
				
				// This is the Oculus part
				yield return null;

				if (OVRPlugin.StartBodyTracking())
				{
					if (bodyToTrack)
					{
						bodyToTrack.enabled = true;
					}
				}

				if (OVRPlugin.StartFaceTracking())
				{
					if (faceExpressionsToTrack)
					{
						faceExpressionsToTrack.enabled = true;
					}
				}
			}
		}

		/// <summary>
		/// Disables XR
		/// </summary>
		public static void DisableXR()
		{
			// Make sure there is something to de-initialize
			if (XRGeneralSettings.Instance.Manager.isInitializationComplete)
			{
				OVRPlugin.StopBodyTracking();
				OVRPlugin.StopFaceTracking();

				XRGeneralSettings.Instance.Manager.StopSubsystems();
				XRGeneralSettings.Instance.Manager.DeinitializeLoader();
			}
		}
	}
}

I hope this helps someone save a few hours of trial and error. See you in the next one!