Configuration
The 2023.04 release includes support for working with Unity’s Legacy XR implementation. To use this feature, simply enable XR Hooks through the GDIOAgent configuration:
Or using the API:
api.EnableHooks(HookingObject.XRLEGACY);
Note: In mixed input environments (i.e. LegacyXR and Input System), you may need to use the "Force Old Input System" option in the agent. To trigger this via the client API, simply use a SetObjectFieldValue command after the initial connection, such as:
api.SetObjectFieldValue("//*[@name='GDIOAgent']/fn:component('gdio.unity_agent.GDIOAgent')", "m_ForceOldInputSystem", true);
Where "GDIOAgent" is the name of the object holding the GDIOAgent component.
Input Usage
Input calls use the same paths as with the Input System, i.e. using ButtonPress and InputEvent actions, but using paths set by the agent:
- GDIOHMD for the Headset
- GDIOLeftHand for the Left Controller
- GDIORightHand for the Right Controller
Example for setting the location of the HMD:
api.Vector3InputEvent("GDIOHMD/centerEyePosition", new Vector3(0.0f, 1.8f, 0.0f), 0);
Some additional device paths can be used for setting the position and rotation of the simulated devices:
GDIOHMD/centerEyePosition GDIOHMD/centerEyeRotation GDIOLeftHand/devicePosition GDIOLeftHand/deviceRotation GDIORightHand/devicePosition GDIORightHand/deviceRotation
Legacy XR is also supported by the GameDriver Recorder and can be used to determine the inputs to be used for testing. If the project contains both old and new input systems, checking the “Force Old Input System“ may be required.
An example test demonstrating the use of LegacyXR inputs is attached below and includes a recording from the Escape Room tutorial project from Unity. You may need to adjust the initial positioning of the XR
Oculus VR (OVR) Support
As of the 2023.10 release, GameDriver's LegacyXR support is extended to include the Oculus VR (OVR) package. This support utilizes the same input methods and general usage as outlined above, with the addition of support for hand gestures.
Hand gesture support is provided via multiple rotations representing each finger, or around 14 rotation calls per gesture. To help with this, we provide a helper class named HandPosePrinter.cs that can print out to a file all the rotations that form the hand gesture.
To use it, run the project without the GDIOAgent enabled, and with the headset on, make the desired
gesture with your hand and press the [P] key on the keyboard. Two files named OVRPoseRightHand and OVRPoseLeftHand will be created on the desktop folder containing the test code necessary. The test code used to support these gestures will include the use of the SkeletonInputEvent method, such as:
api.SkeletonInputEvent("OculusLeftHand/skeleton", new float[]{0.0f,0.0f,0.0f,0.0f,0.0f}, 2); // Set hand skeleton to fully open hand.
You can organize these into methods in your test code to execute the specific captured pose. An example of this approach is shown below.
public void PinchPose() { //Hand_Thumb0 api.SkeletonInputEvent("GDIORightHand/skeleton", 2, api.EulerToQuat(39.00648f, -297.9758f, -323.5771f), 0); api.SkeletonInputEvent("GDIORightHand/skeleton", 3, api.EulerToQuat(32.08998f, -341.2277f, -336.2333f), 0); api.SkeletonInputEvent("GDIORightHand/skeleton", 4, api.EulerToQuat(351.5882f, -7.960947f, -342.8726f), 0); api.SkeletonInputEvent("GDIORightHand/skeleton", 5, api.EulerToQuat(7.265752f, -353.5541f, -332.6898f), 0); //Hand_Index1 api.SkeletonInputEvent("GDIORightHand/skeleton", 6, api.EulerToQuat(4.145337f, -349.4534f, -307.5352f), 0); api.SkeletonInputEvent("GDIORightHand/skeleton", 7, api.EulerToQuat(357.1325f, -0.7786186f, -306.1896f), 0); api.SkeletonInputEvent("GDIORightHand/skeleton", 8, api.EulerToQuat(358.6137f, -2.876725f, -335.6526f), 0); //Hand_Middle1 api.SkeletonInputEvent("GDIORightHand/skeleton", 9, api.EulerToQuat(359.0811f, -1.158072f, -349.7017f), 0); api.SkeletonInputEvent("GDIORightHand/skeleton", 10, api.EulerToQuat(358.6111f, -0.5548517f, -302.3149f), 0); api.SkeletonInputEvent("GDIORightHand/skeleton", 11, api.EulerToQuat(352.5197f, -0.6495829f, -333.5562f), 0); //Hand_Ring1 api.SkeletonInputEvent("GDIORightHand/skeleton", 12, api.EulerToQuat(354.2881f, -10.8357f, -10.93568f), 0); api.SkeletonInputEvent("GDIORightHand/skeleton", 13, api.EulerToQuat(355.0336f, -1.024279f, -297.8333f), 0); api.SkeletonInputEvent("GDIORightHand/skeleton", 14, api.EulerToQuat(357.1022f, -357.3359f, -327.2591f), 0); //Hand_Pinky0 api.SkeletonInputEvent("GDIORightHand/skeleton", 15, api.EulerToQuat(336.6904f, -17.70523f, -354.1525f), 0); api.SkeletonInputEvent("GDIORightHand/skeleton", 16, api.EulerToQuat(11.74857f, -13.97097f, -29.30103f), 0); api.SkeletonInputEvent("GDIORightHand/skeleton", 17, api.EulerToQuat(353.5829f, -5.809196f, -309.7157f), 0); api.SkeletonInputEvent("GDIORightHand/skeleton", 18, api.EulerToQuat(358.9135f, -354.639f, -333.5012f), 0); } public void PinchPoseLeft() { //Hand_Thumb0 api.SkeletonInputEvent("GDIOLeftHand/skeleton", 2, api.EulerToQuat(38.82063f, -297.5854f, -327.9914f), 0); api.SkeletonInputEvent("GDIOLeftHand/skeleton", 3, api.EulerToQuat(29.59883f, -338.8843f, -335.5561f), 0); api.SkeletonInputEvent("GDIOLeftHand/skeleton", 4, api.EulerToQuat(351.7505f, -7.906457f, -340.3772f), 0); api.SkeletonInputEvent("GDIOLeftHand/skeleton", 5, api.EulerToQuat(6.546093f, -354.0137f, -322.5562f), 0); //Hand_Index1 api.SkeletonInputEvent("GDIOLeftHand/skeleton", 6, api.EulerToQuat(4.216051f, -351.8521f, -311.5842f), 0); api.SkeletonInputEvent("GDIOLeftHand/skeleton", 7, api.EulerToQuat(357.14f, -0.7670153f, -299.3587f), 0); api.SkeletonInputEvent("GDIOLeftHand/skeleton", 8, api.EulerToQuat(358.7017f, -2.832536f, -330.9977f), 0); //Hand_Middle1 api.SkeletonInputEvent("GDIOLeftHand/skeleton", 9, api.EulerToQuat(359.0885f, -2.093114f, -355.8686f), 0); api.SkeletonInputEvent("GDIOLeftHand/skeleton", 10, api.EulerToQuat(358.6343f, -0.5280398f, -319.4061f), 0); api.SkeletonInputEvent("GDIOLeftHand/skeleton", 11, api.EulerToQuat(353.742f, -0.207874f, -346.9665f), 0); //Hand_Ring1 api.SkeletonInputEvent("GDIOLeftHand/skeleton", 12, api.EulerToQuat(354.183f, -9.713001f, -15.66434f), 0); api.SkeletonInputEvent("GDIOLeftHand/skeleton", 13, api.EulerToQuat(355.2855f, -0.6915488f, -316.6288f), 0); api.SkeletonInputEvent("GDIOLeftHand/skeleton", 14, api.EulerToQuat(358.1482f, -356.8446f, -342.4605f), 0); //Hand_Pinky0 api.SkeletonInputEvent("GDIOLeftHand/skeleton", 15, api.EulerToQuat(336.6904f, -17.70522f, -354.1525f), 0); api.SkeletonInputEvent("GDIOLeftHand/skeleton", 16, api.EulerToQuat(12.01481f, -9.105035f, -32.43835f), 0); api.SkeletonInputEvent("GDIOLeftHand/skeleton", 17, api.EulerToQuat(354.1174f, -5.321207f, -325.8288f), 0); api.SkeletonInputEvent("GDIOLeftHand/skeleton", 18, api.EulerToQuat(359.5704f, -354.4142f, -348.7165f), 0); } public void OpenHandPose() { api.SkeletonInputEvent("GDIORightHand/skeleton", 2, api.EulerToQuat(38.69f, -297.28f, -335.78f), 0); api.SkeletonInputEvent("GDIORightHand/skeleton", 3, api.EulerToQuat(24.92f, -333.71f, -334.27f), 0); api.SkeletonInputEvent("GDIORightHand/skeleton", 4, api.EulerToQuat(351.11f, -8.08f, -350.09f), 0); api.SkeletonInputEvent("GDIORightHand/skeleton", 5, api.EulerToQuat(8.26f, -353.17f, -345.33f), 0); api.SkeletonInputEvent("GDIORightHand/skeleton", 6, api.EulerToQuat(3.04f, -350.32f, -350.40f), 0); api.SkeletonInputEvent("GDIORightHand/skeleton", 7, api.EulerToQuat(357.09f, -0.81f, -331.50f), 0); api.SkeletonInputEvent("GDIORightHand/skeleton", 8, api.EulerToQuat(358.26f, -2.98f, -352.86f), 0); api.SkeletonInputEvent("GDIORightHand/skeleton", 9, api.EulerToQuat(359.10f, -2.30f, -355.11f), 0); api.SkeletonInputEvent("GDIORightHand/skeleton", 10, api.EulerToQuat(358.65f, -0.52f, -329.55f), 0); api.SkeletonInputEvent("GDIORightHand/skeleton", 11, api.EulerToQuat(354.60f, -0.08f, -355.84f), 0); api.SkeletonInputEvent("GDIORightHand/skeleton", 12, api.EulerToQuat(354.39f, -10.51f, -357.40f), 0); api.SkeletonInputEvent("GDIORightHand/skeleton", 13, api.EulerToQuat(355.55f, -0.50f, -331.40f), 0); api.SkeletonInputEvent("GDIORightHand/skeleton", 14, api.EulerToQuat(358.53f, -356.74f, -347.70f), 0); api.SkeletonInputEvent("GDIORightHand/skeleton", 15, api.EulerToQuat(336.69f, -17.71f, -354.15f), 0); api.SkeletonInputEvent("GDIORightHand/skeleton", 16, api.EulerToQuat(10.75f, -4.33f, -2.96f), 0); api.SkeletonInputEvent("GDIORightHand/skeleton", 17, api.EulerToQuat(354.32f, -5.19f, -331.19f), 0); api.SkeletonInputEvent("GDIORightHand/skeleton", 18, api.EulerToQuat(359.56f, -354.42f, -348.54f), 0); } public void PointPose() { //Hand_Thumb0 api.SkeletonInputEvent("GDIORightHand/skeleton", 2, api.EulerToQuat(38.89811f, -297.7541f, -325.8609f), 0); api.SkeletonInputEvent("GDIORightHand/skeleton", 3, api.EulerToQuat(35.59741f, -7.550374f, -351.1602f), 0); api.SkeletonInputEvent("GDIORightHand/skeleton", 4, api.EulerToQuat(352.6481f, -7.44523f, -325.6596f), 0); api.SkeletonInputEvent("GDIORightHand/skeleton", 5, api.EulerToQuat(5.831383f, -354.6906f, -310.8864f), 0); api.SkeletonInputEvent("GDIORightHand/skeleton", 6, api.EulerToQuat(3.839882f, -355.4543f, 0f), 0); api.SkeletonInputEvent("GDIORightHand/skeleton", 7, api.EulerToQuat(357.0401f, -0.8260307f, 0f), 0); api.SkeletonInputEvent("GDIORightHand/skeleton", 8, api.EulerToQuat(357.9413f, -2.972085f, 0f), 0); api.SkeletonInputEvent("GDIORightHand/skeleton", 9, api.EulerToQuat(359.4679f, -4.349733f, -289.7907f), 0); api.SkeletonInputEvent("GDIORightHand/skeleton", 10, api.EulerToQuat(358.5927f, -0.6143429f, -272.0609f), 0); api.SkeletonInputEvent("GDIORightHand/skeleton", 11, api.EulerToQuat(350.0163f, -3.198704f, -296.062f), 0); api.SkeletonInputEvent("GDIORightHand/skeleton", 12, api.EulerToQuat(354.7221f, -8.257318f, -289.0827f), 0); api.SkeletonInputEvent("GDIORightHand/skeleton", 13, api.EulerToQuat(354.8939f, -1.762349f, -263.6889f), 0); api.SkeletonInputEvent("GDIORightHand/skeleton", 14, api.EulerToQuat(355.621f, -358.9403f, -298.2752f), 0); api.SkeletonInputEvent("GDIORightHand/skeleton", 15, api.EulerToQuat(336.6904f, -17.70524f, -354.1525f), 0); api.SkeletonInputEvent("GDIORightHand/skeleton", 16, api.EulerToQuat(8.31627f, -353.5389f, -308.8625f), 0); api.SkeletonInputEvent("GDIORightHand/skeleton", 17, api.EulerToQuat(353.0914f, -8.18574f, -253.8036f), 0); api.SkeletonInputEvent("GDIORightHand/skeleton", 18, api.EulerToQuat(357.8244f, -355.6555f, -300.4726f), 0); }
By organizing these poses into separate methods, we can call upon them in a test case much more readily. For example, the following test will first position the simulated devices into specific locations in the scene, then call the required methods captured above.
[Test] public void OVRHandsTest() { // LegacyXR Hooking must be enabled for OVR Inputs api.DisableHooks(HookingObject.MOUSE); api.DisableHooks(HookingObject.KEYBOARD); api.EnableHooks(HookingObject.XRLEGACY); // In this example, we're forcing Legacy Inputs in the agent due to a mixed input method project api.SetObjectFieldValue("//*[@name='GDIOAgent']/fn:component('gdio.unity_agent.GDIOAgent')", "m_ForceOldInputSystem", true); // We're now loading a device description from a JSON file. Refer to Input System usage for more details. //string desc = api.LoadDeviceDescription("GDIORightHand.json"); //api.CreateInputDeviceFromDescription(desc, "GDIORightHand", new string[] { "GDIOInputProvider" }); api.Wait(2000); api.ButtonPress("GDIOHMD/UserPresence", 0, 1f); api.ButtonPress("GDIOHMD/IsTracked", 0, 1f); api.IntegerInputEvent("GDIOHMD/TrackingState", 63, 0); api.IntegerInputEvent("GDIORightHand/TrackingState", 63, 0); api.IntegerInputEvent("GDIOLeftHand/TrackingState", 63, 0); //api.Vector3InputEvent("GDIOHMD/CenterEyePosition", new Vector3(0.0f, 0.0f, 0.0f), 0); api.QuaternionInputEvent("GDIOHMD/CenterEyeRotation", api.EulerToQuat(35f, 0f, 0f), 0); api.Vector3InputEvent("GDIOLeftHand/DevicePosition", new Vector3(-0.3f, -1.0f, 0.6f), 0); api.Vector3InputEvent("GDIORightHand/DevicePosition", new Vector3(0.3f, -1.0f, 0.6f), 0); api.Wait(2000); // Position hand on buttons PinchPose(); PinchPoseLeft(); // These are test-specific methods used to position the simulated XR devices at the right place in the scene Vector3 position = api.GetObjectPosition("//*[@name='StartStopButtonHousing']"); Vector3 up = api.GetObjectFieldValue<Vector3>("//*[@name='StartStopButtonHousing']/fn:component('UnityEngine.Transform')", "up"); position.x += 0.02f; position.y += up.y * 0.22f; position.z += up.z * 0.22f; api.Vector3InputEvent("GDIORightHand/DevicePosition", position, 0); api.Wait(1000); Quaternion rotation = RotateTo("RightHandAnchor", "StartStopButtonHousing", new Vector3(0.02f, 0.0f, 0.0f)); api.QuaternionInputEvent("GDIORightHand/DeviceRotation", rotation, 0); api.Wait(1500); // Point pose and press PointPose(); position.y -= up.y * 0.04f; position.z -= up.z * 0.04f; api.Vector3InputEvent("GDIORightHand/DevicePosition", position, 0); api.Wait(1000); // Reposition position.y += up.y * 0.04f; position.z += up.z * 0.04f; api.Vector3InputEvent("GDIORightHand/DevicePosition", position, 0); api.Wait(1000); position.x = 0f; position.y += 0.1f; position.z += 0.04f; api.Vector3InputEvent("GDIORightHand/DevicePosition", position, 0); api.QuaternionInputEvent("GDIORightHand/DeviceRotation", api.EulerToQuat(-20.0f, 0.0f, 180.0f), 0); api.Wait(2000); position.x += 1.8f; position.z -= 2.0f; api.Wait(1000); OpenHandPose(); api.Wait(1000); api.Vector3InputEvent("GDIORightHand/DevicePosition", new Vector3(position.x, -1.0f, 0.43f), 0); api.QuaternionInputEvent("GDIORightHand/DeviceRotation", api.EulerToQuat(0.0f, 0.0f, 0.0f), 0); api.Wait(1000); PinchPose(); api.Wait(3000); OpenHandPose(); api.Wait(1000); api.QuaternionInputEvent("GDIORightHand/DeviceRotation", api.EulerToQuat(-90.0f, 0.0f, 180.0f), 0); api.Wait(1000); PinchPose(); api.Wait(2000); OpenHandPose(); api.Wait(3000); }
This example code also makes use of some useful helper functions, such as RotateTo, which can be found in this article.