C# · Programming

A Script for Controllers (Vive/Index/Oculus) to Use with OpenXR in Unity 2020.2.6

OpenXR allows us to use the same code for multiple controllers without modification. That’s great, but figuring out how to implement them is tricky. You first need to subscribe to specific types of device connections and disconnections. When a device connects, you need to check it to make sure that it has the characteristics of hardware you want to monitor. Then you need to add the device to a list that you can iterate through every frame to watch for changes.

There’s no usable example code currently provided to provide controller input like this. There are some fragmented examples in the documentation, but I wanted to post this online so that you don’t have to go through the whole process of ‘poking at it until it starts working’.

If you’ve read any of my books, you’ll already know that I like to reuse code. Yeah, I know, a lot of people say reusing code never happens and it’s a waste of time but I have an entire framework I’ve used for three games in a row now and it saved a ton of time in setup. I stand by my ‘write reusable code’ mantra, always have and always will. For this project, I wanted to make controller code that could easily have multiple other scripts use without spaghetti code tying them together, as well as being easy to re-use on future projects.

It works like a standard input Component might, in that it sits on a GameObject watching the controllers and storing numbers/bools to represent input. Any other script that wants to know what’s going on with input can either subscribe to UnityEvents fired by it (in the case of buttons) or look directly at its public variables for things like input axis or trigger values. Building it this way means that I can have multiple script Components attached to the same GameObject which all need input, but I don’t have to repeat any code since all of them can tap into the one input Component. It’s clean, reusable and keeps the input code all in one place should we ever need to update or change the way it works in the future.

Here’s the XRInput script in full, which I’ll break down and explain fully another day:

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.XR;
using UnityEngine.XR.Interaction.Toolkit;

namespace WordGame
{
	[System.Serializable]
	public class OnMenuButtonEvent : UnityEvent<bool> { }

	[System.Serializable]
	public class OnTriggerChangeEvent : UnityEvent<bool> { }

	[System.Serializable]
	public class OnGripChangeEvent : UnityEvent<bool> { }

	public class XRInput : MonoBehaviour
	{
		public OnMenuButtonEvent OnMenuButton;
		public OnTriggerChangeEvent OnTriggerChange;
		public OnGripChangeEvent OnGripChange;

		public InputDeviceCharacteristics characteristics;

		public bool buttonState;
		public bool old_buttonState;
		public float triggerVal;
		public float gripVal;

		public bool needsReset;
		public bool triggerReset;
		public bool gripReset;

		private List<InputDevice> devicesToMonitor;

		public bool didInit;

		void OnEnable()
		{
			if (!didInit)
				return;

			RegisterDevices();
		}

		void OnDisable()
		{
			DeregisterDevices();
		}

		void Update()
		{
			if (!didInit)
			{
				SetUpEvents();
				return;
			}

			// here, we go through any devices we're monitoring to get input information. This means that if more than one set of controllers are attached (which meet the characteristics set in the characteristics variable) then all of the buttons will be registered from all of the controllers.
			InputDevice device;

			for (int i = 0; i < devicesToMonitor.Count; i++)
			{
				device = devicesToMonitor[i];

				// menu button ---------------------------------------------------------------------------
				device.TryGetFeatureValue(CommonUsages.primary2DAxisClick, out buttonState);
				if (old_buttonState != buttonState)
				{
					OnMenuButton.Invoke(buttonState);
				}

				old_buttonState = buttonState;
				// ---------------------------------------------------------------------------------------

				// trigger -------------------------------------------------------------------------------
				device.TryGetFeatureValue(CommonUsages.trigger, out triggerVal);
				if (triggerVal > 0)
				{
					triggerReset = true;
				}
				else
				{
					triggerReset = false;
				}
				// ---------------------------------------------------------------------------------------

				// grip ----------------------------------------------------------------------------------
				device.TryGetFeatureValue(CommonUsages.grip, out gripVal);
				if (gripVal > 0)
				{
					gripReset = true;
				}
				else
				{
					gripReset = false;
				}
				// --------------------------------------------------------------------------------------

				if (gripReset || triggerReset)
				{
					needsReset = true;
				}
				else
				{
					needsReset = false;
				}
			}
		}

		void SetUpEvents()
		{
			// this initializes the events if they're not set in the Inspector, so other scripts can subscribe to them too
			if (OnMenuButton == null)
				OnMenuButton = new OnMenuButtonEvent();

			if (OnTriggerChange == null)
				OnTriggerChange = new OnTriggerChangeEvent();

			if (OnGripChange == null)
				OnGripChange = new OnGripChangeEvent();

			RegisterDevices();

			didInit = true;
		}

		void RegisterDevices()
		{
			Debug.Log("Trying to register controller devices..");

			devicesToMonitor = new List<InputDevice>();

			// grab all existing connected devices and call connected on them to add them to monitor
			List<InputDevice> allDevices = new List<InputDevice>();

			InputDevices.GetDevicesWithCharacteristics(characteristics, allDevices);
			foreach (InputDevice device in allDevices)
				InputDevices_deviceConnected(device);

			// now register to connected/disconnected events for future device connections/disconnections
			InputDevices.deviceConnected += InputDevices_deviceConnected;
			InputDevices.deviceDisconnected += InputDevices_deviceDisconnected;
		}

		void DeregisterDevices()
		{
			// de-register device connection/disconnection events
			InputDevices.deviceConnected -= InputDevices_deviceConnected;
			InputDevices.deviceDisconnected -= InputDevices_deviceDisconnected;

			// clear out the list of devices to monitor
			devicesToMonitor.Clear();
		}

		private void InputDevices_deviceConnected(InputDevice device)
		{
			// first, we find all devices with the right characteristics (chosen via the Inspector in editor)
			List<InputDevice> allDevices = new List<InputDevice>();
			InputDevices.GetDevicesWithCharacteristics(characteristics, allDevices);

			// now we have a list of matching devices, we go through them and compare each one to the device that just connected. If it's in the list of devices with the right characteristics, we add the one that just connected to the list of devices to monitor
			foreach (InputDevice deviceFromAll in allDevices)
			{
				if (!devicesToMonitor.Contains(deviceFromAll) && deviceFromAll == device)
				{
					Debug.Log(string.Format("Found device name '{0}' which has characteristics '{1}'", deviceFromAll.name, deviceFromAll.characteristics.ToString()));

					devicesToMonitor.Add(deviceFromAll);
				}
			}
		}

		private void InputDevices_deviceDisconnected(InputDevice device)
		{
			// if we're monitoring it, it'll be in the list, so here we remove it from the monitor list..
			if (devicesToMonitor.Contains(device))
				devicesToMonitor.Remove(device);
		}
	}
}

Note that the script is within a namespace named XRInput, so if you’re using it you will need to either change the namespace, remove it, or just add this to the top of your code:

using XRInput;

I do intend to come back and write this up at some stage, but I just wanted to get something up here quickly for people to use.. so, it is what it is!

If you want to add more functionality to this script, to monitor more buttons and so on, you’ll need to take a look at this page on Unity’s docs to figure out what buttons are called by OpenXR and how they relate to each different type of controller. Here’s a link to that page, which also contains most of the documentation for the XR controller system: https://docs.unity.cn/Manual/xr_input.html#XRInputMappings

It’s likely that you might also ask “What about the menu button, Jeff?” well, yeah. Menu buttons aren’t working at the moment. I guess Unity just isn’t supporting them just yet. Hopefully this gets resolved soon because I really like using the menu button. For now, I’m using the trackpad click to display a menu and that’s not ideal.

Good luck!