using System.Collections; using NUnit.Framework; using UnityEngine.InputSystem; using UnityEngine.InputSystem.Processors; using UnityEngine.TestTools; using UnityEngine.TestTools.Utils; using UnityEngine.XR.Interaction.Toolkit.Locomotion; using UnityEngine.XR.Interaction.Toolkit.Locomotion.Movement; using UnityEngine.XR.Interaction.Toolkit.Locomotion.Turning; using UnityEngine.XR.Interaction.Toolkit.Utilities; namespace UnityEngine.XR.Interaction.Toolkit.Tests { [TestFixture] class LocomotionInputTests : InputTestFixture { enum ForwardSource { Default, Camera, Controller, } [TearDown] public override void TearDown() { TestUtilities.DestroyAllSceneObjects(); base.TearDown(); } [UnityTest] public IEnumerator MoveInDefaultDirection() { return MoveInDirection(ForwardSource.Default); } [UnityTest] public IEnumerator MoveInCameraDirection() { return MoveInDirection(ForwardSource.Camera); } [UnityTest] public IEnumerator MoveInControllerDirection() { return MoveInDirection(ForwardSource.Controller); } IEnumerator MoveInDirection(ForwardSource forwardSource) { // Create a stick control to serve as the input action source for the move provider var gamepad = InputSystem.InputSystem.AddDevice(); var asset = ScriptableObject.CreateInstance(); var actionMap = asset.AddActionMap("Locomotion"); var action = actionMap.AddAction("Move", InputActionType.Value, "/leftStick"); var inputActionReference = ScriptableObject.CreateInstance(); inputActionReference.Set(action); action.Enable(); var xrOrigin = TestUtilities.CreateXROrigin(); var rigTransform = xrOrigin.Origin.transform; var cameraTransform = xrOrigin.Camera.transform; // Rotate the camera to face a different direction than rig forward to test // that the move provider will move with respect to a selected forward object. cameraTransform.Rotate(0f, 45f, 0f); var cameraForward = cameraTransform.forward; Assert.That(rigTransform.forward, Is.Not.EqualTo(cameraForward).Using(Vector3ComparerWithEqualsOperator.Instance)); // Create a controller object to serve as another forward source var controllerGO = new GameObject("Controller"); controllerGO.transform.SetParent(xrOrigin.CameraFloorOffsetObject.transform, false); controllerGO.transform.Rotate(0f, -45f, 0f); var controllerForward = controllerGO.transform.forward; Assert.That(rigTransform.forward, Is.Not.EqualTo(controllerForward).Using(Vector3ComparerWithEqualsOperator.Instance)); // Config continuous move on XR Origin var mediator = xrOrigin.gameObject.AddComponent(); mediator.GetComponent().xrOrigin = xrOrigin; var moveProvider = xrOrigin.gameObject.AddComponent(); moveProvider.mediator = mediator; moveProvider.leftHandMoveInput.inputActionReference = inputActionReference; moveProvider.moveSpeed = 1f; switch (forwardSource) { case ForwardSource.Default: break; case ForwardSource.Camera: moveProvider.forwardSource = xrOrigin.Camera.transform; break; case ForwardSource.Controller: moveProvider.forwardSource = controllerGO.transform; break; default: Assert.Fail($"Unhandled {nameof(ForwardSource)}={forwardSource}"); break; } // See Script Execution Order diagram https://docs.unity3d.com/Manual/ExecutionOrder.html // This test will begin after Update() during the yield null/yield WaitForSeconds/yield StartCoroutine stage. // The move provider will process input during Update() of the next frame, and scale the move based on Time.deltaTime. // After yielding for 1 second with the stick pushed forward, the stick will be released back to center. // The move provider will process the release during Update() of the next frame, and should not apply any more movement. // Partially push stick directly forward. // This tests that the move speed will be scaled by the input magnitude. var input = new Vector2(0f, 0.5f); var processedInput = new StickDeadzoneProcessor().Process(input); Set(gamepad.leftStick, input); var startTime = Time.time; for (var i = 0; i < 60; ++i) // wait for 60 frames. yield return Application.isBatchMode ? null : new WaitForEndOfFrame(); var actualPosition = rigTransform.position; var actualDistance = Vector3.Distance(Vector3.zero, actualPosition); var expectedDistance = processedInput.magnitude * moveProvider.moveSpeed * (Time.time - startTime); Assert.That(actualDistance, Is.EqualTo(expectedDistance).Within(1e-5f)); switch (forwardSource) { case ForwardSource.Default: case ForwardSource.Camera: Assert.That(actualPosition, Is.EqualTo(cameraForward * expectedDistance).Using(Vector3ComparerWithEqualsOperator.Instance)); break; case ForwardSource.Controller: Assert.That(actualPosition, Is.EqualTo(controllerForward * expectedDistance).Using(Vector3ComparerWithEqualsOperator.Instance)); break; default: Assert.Fail($"Unhandled {nameof(ForwardSource)}={forwardSource}"); break; } // Stop moving Set(gamepad.leftStick, Vector2.zero); yield return Application.isBatchMode ? null : new WaitForEndOfFrame(); // ReSharper disable Unity.InefficientPropertyAccess -- Property value accessed after yield Assert.That(Vector3.Distance(actualPosition, rigTransform.position), Is.EqualTo(0f)); // ReSharper restore Unity.InefficientPropertyAccess } [UnityTest] public IEnumerator SmoothTurn() { // Create a stick control to serve as the input action source for the turn provider var gamepad = InputSystem.InputSystem.AddDevice(); var asset = ScriptableObject.CreateInstance(); var actionMap = asset.AddActionMap("Locomotion"); var action = actionMap.AddAction("Turn", InputActionType.Value, "/rightStick"); var inputActionReference = ScriptableObject.CreateInstance(); inputActionReference.Set(action); action.Enable(); var xrOrigin = TestUtilities.CreateXROrigin(); var rigTransform = xrOrigin.Origin.transform; // Config continuous turn on XR Origin var mediator = xrOrigin.gameObject.AddComponent(); mediator.GetComponent().xrOrigin = xrOrigin; var turnProvider = xrOrigin.gameObject.AddComponent(); turnProvider.mediator = mediator; turnProvider.leftHandTurnInput.inputActionReference = inputActionReference; turnProvider.turnSpeed = 60f; // Partially push stick directly right. // This tests that the turn speed will be scaled by the input magnitude. var input = new Vector2(0.5f, 0f); var processedInput = new StickDeadzoneProcessor().Process(input); Set(gamepad.rightStick, input); var startTime = Time.time; for (var i = 0; i < 60; ++i) // wait for 60 frames. yield return Application.isBatchMode ? null : new WaitForEndOfFrame(); var turnAmount = processedInput.magnitude * turnProvider.turnSpeed * (Time.time - startTime); var actualRotation = rigTransform.rotation; Assert.That(actualRotation, Is.EqualTo(Quaternion.Euler(0f, turnAmount, 0f)).Using(QuaternionEqualityComparer.Instance)); // Stop turning Set(gamepad.rightStick, Vector2.zero); yield return Application.isBatchMode ? null : new WaitForEndOfFrame(); // ReSharper disable Unity.InefficientPropertyAccess -- Property value accessed after yield Assert.That(actualRotation, Is.EqualTo(rigTransform.rotation).Using(QuaternionEqualityComparer.Instance)); // ReSharper restore Unity.InefficientPropertyAccess } [UnityTest] public IEnumerator TeleportationMonitorDetectsSnapTurns() { // Create a stick control to serve as the input action source for the turn provider var gamepad = InputSystem.InputSystem.AddDevice(); var asset = ScriptableObject.CreateInstance(); var actionMap = asset.AddActionMap("Locomotion"); var action = actionMap.AddAction("Turn", InputActionType.Value, "/rightStick"); var inputActionReference = ScriptableObject.CreateInstance(); inputActionReference.Set(action); action.Enable(); var xrOrigin = TestUtilities.CreateXROrigin(); var rigTransform = xrOrigin.Origin.transform; var mediator = xrOrigin.gameObject.AddComponent(); mediator.GetComponent().xrOrigin = xrOrigin; // Config snap turn on XR Origin var snapTurnProvider = xrOrigin.gameObject.AddComponent(); snapTurnProvider.mediator = mediator; snapTurnProvider.rightHandTurnInput.inputActionReference = inputActionReference; snapTurnProvider.enableTurnAround = true; snapTurnProvider.debounceTime = 0f; var snapTurnStepped = false; snapTurnProvider.afterStepLocomotion += _ => snapTurnStepped = true; // Config continuous turn on XR Origin var continuousTurnProvider = xrOrigin.gameObject.AddComponent(); continuousTurnProvider.mediator = mediator; continuousTurnProvider.rightHandTurnInput.inputActionReference = inputActionReference; continuousTurnProvider.enableTurnAround = true; var continuousTurnStepped = false; continuousTurnProvider.afterStepLocomotion += _ => continuousTurnStepped = true; // Create interactor under the XR Origin var interactor = TestUtilities.CreateMockInteractor(); interactor.transform.SetParent(rigTransform); var teleported = false; var monitor = new TeleportationMonitor(); monitor.teleported += (_, _, _) => teleported = true; monitor.AddInteractor(interactor); // First test with SnapTurnProvider snapTurnProvider.enabled = true; continuousTurnProvider.enabled = false; // Push stick down to trigger turn around. Set(gamepad.rightStick, Vector2.down); yield return null; Assert.That(teleported, Is.True); Assert.That(snapTurnStepped, Is.True); Assert.That(continuousTurnStepped, Is.False); teleported = false; snapTurnStepped = false; // Stop turning Set(gamepad.rightStick, Vector2.zero); yield return null; // Turn right to trigger snap right Set(gamepad.rightStick, Vector2.right); yield return null; Assert.That(teleported, Is.True); Assert.That(snapTurnStepped, Is.True); Assert.That(continuousTurnStepped, Is.False); teleported = false; snapTurnStepped = false; // Stop turning Set(gamepad.rightStick, Vector2.zero); yield return null; // Now test with ContinuousTurnProvider snapTurnProvider.enabled = false; continuousTurnProvider.enabled = true; // Push stick down to trigger turn around. Set(gamepad.rightStick, Vector2.down); yield return null; Assert.That(teleported, Is.True); Assert.That(snapTurnStepped, Is.False); Assert.That(continuousTurnStepped, Is.True); teleported = false; continuousTurnStepped = false; // Stop turning Set(gamepad.rightStick, Vector2.zero); yield return null; // Turn right to trigger partial turn right, which should not trigger a teleport // since it only turned a few degrees Set(gamepad.rightStick, Vector2.right); yield return null; Assert.That(teleported, Is.False); Assert.That(snapTurnStepped, Is.False); Assert.That(continuousTurnStepped, Is.True); continuousTurnStepped = false; } } }