using System.Collections; using System.Collections.Generic; using NUnit.Framework; using UnityEngine.TestTools; using UnityEngine.XR.Interaction.Toolkit.Interactables; using UnityEngine.XR.Interaction.Toolkit.Interactors; namespace UnityEngine.XR.Interaction.Toolkit.Tests { [TestFixture] class DirectInteractorTests { [TearDown] public void TearDown() { TestUtilities.DestroyAllSceneObjects(); } [UnityTest] public IEnumerator DirectInteractorCanHoverInteractable() { var manager = TestUtilities.CreateInteractionManager(); var interactable = TestUtilities.CreateGrabInteractable(); var directInteractor = TestUtilities.CreateDirectInteractor(); yield return new WaitForFixedUpdate(); yield return null; var validTargets = new List(); manager.GetValidTargets(directInteractor, validTargets); Assert.That(validTargets, Is.EqualTo(new[] { interactable })); Assert.That(directInteractor.interactablesHovered, Is.EqualTo(new[] { interactable })); Assert.That(directInteractor.hasHover, Is.True); } [UnityTest] public IEnumerator DirectInteractorHandlesUnregisteredInteractable() { var manager = TestUtilities.CreateInteractionManager(); var interactable = TestUtilities.CreateGrabInteractable(); var directInteractor = TestUtilities.CreateDirectInteractor(); yield return new WaitForFixedUpdate(); yield return null; var validTargets = new List(); manager.GetValidTargets(directInteractor, validTargets); Assert.That(validTargets, Is.EqualTo(new[] { interactable })); directInteractor.GetValidTargets(validTargets); Assert.That(validTargets, Is.EqualTo(new[] { interactable })); Assert.That(directInteractor.interactablesHovered, Is.EqualTo(new[] { interactable })); Object.Destroy(interactable); yield return null; Assert.That(interactable == null, Is.True); manager.GetValidTargets(directInteractor, validTargets); Assert.That(validTargets, Is.Empty); directInteractor.GetValidTargets(validTargets); Assert.That(validTargets, Is.Empty); Assert.That(directInteractor.interactablesHovered, Is.Empty); } [UnityTest] public IEnumerator DirectInteractorCanSelectInteractable() { TestUtilities.CreateInteractionManager(); var interactable = TestUtilities.CreateGrabInteractable(); var directInteractor = TestUtilities.CreateDirectInteractor(); var controllerRecorder = TestUtilities.CreateControllerRecorder(directInteractor, (recording) => { recording.AddRecordingFrameNonAlloc(new XRControllerState(0.0f, Vector3.zero, Quaternion.identity, InputTrackingState.All, true, false, false, false)); recording.AddRecordingFrameNonAlloc(new XRControllerState(0.1f, Vector3.zero, Quaternion.identity, InputTrackingState.All, true, true, false, false)); recording.AddRecordingFrameNonAlloc(new XRControllerState(float.MaxValue, Vector3.zero, Quaternion.identity, InputTrackingState.All, true, true, false, false)); }); controllerRecorder.isPlaying = true; controllerRecorder.visitEachFrame = true; yield return new WaitForFixedUpdate(); yield return null; Assert.That(directInteractor.interactablesSelected, Is.EqualTo(new[] { interactable })); } [UnityTest] public IEnumerator DirectInteractorHandlesCanceledInteractable() { var manager = TestUtilities.CreateInteractionManager(); var interactable = TestUtilities.CreateGrabInteractable(); var directInteractor = TestUtilities.CreateDirectInteractor(); var controllerRecorder = TestUtilities.CreateControllerRecorder(directInteractor, (recording) => { recording.AddRecordingFrameNonAlloc(new XRControllerState(0.0f, Vector3.zero, Quaternion.identity, InputTrackingState.All, true, false, false, false)); recording.AddRecordingFrameNonAlloc(new XRControllerState(0.1f, Vector3.zero, Quaternion.identity, InputTrackingState.All, true, true, false, false)); recording.AddRecordingFrameNonAlloc(new XRControllerState(float.MaxValue, Vector3.zero, Quaternion.identity, InputTrackingState.All, true, true, false, false)); }); controllerRecorder.isPlaying = true; yield return new WaitForFixedUpdate(); yield return null; Assert.That(directInteractor.interactablesSelected, Is.EqualTo(new[] { interactable })); IXRSelectInteractor canceledInteractor = null; IXRSelectInteractable canceledInteractable = null; interactable.selectExited.AddListener(args => canceledInteractor = args.isCanceled ? args.interactorObject : null); directInteractor.selectExited.AddListener(args => canceledInteractable = args.isCanceled ? args.interactableObject : null); Object.Destroy(interactable); yield return null; // ReSharper disable once ConditionIsAlwaysTrueOrFalse -- overloaded Object operator== Assert.That(interactable == null, Is.True); Assert.That(directInteractor.interactablesSelected, Is.Empty); Assert.That(canceledInteractor, Is.SameAs(directInteractor)); Assert.That(canceledInteractable, Is.SameAs(interactable)); var validTargets = new List(); manager.GetValidTargets(directInteractor, validTargets); Assert.That(validTargets, Is.Empty); Assert.That(directInteractor.interactablesHovered, Is.Empty); } [UnityTest] public IEnumerator DirectInteractorCanGainAndLoseFocusOfInteractable() { TestUtilities.CreateInteractionManager(); var interactable = TestUtilities.CreateGrabInteractable(); var group = TestUtilities.CreateInteractionGroup(); var directInteractor = TestUtilities.CreateDirectInteractor(); group.AddGroupMember(directInteractor); var controllerRecorder = TestUtilities.CreateControllerRecorder(directInteractor, (recording) => { recording.AddRecordingFrameNonAlloc(new XRControllerState(0.0f, Vector3.zero, Quaternion.identity, InputTrackingState.All, true, false, false, false)); recording.AddRecordingFrameNonAlloc(new XRControllerState(0.1f, Vector3.zero, Quaternion.identity, InputTrackingState.All, true, true, false, false)); recording.AddRecordingFrameNonAlloc(new XRControllerState(0.2f, Vector3.zero, Quaternion.identity, InputTrackingState.All, true, false, false, false)); recording.AddRecordingFrameNonAlloc(new XRControllerState(0.3f, Vector3.zero, Quaternion.identity, InputTrackingState.All, true, false, false, false)); }); controllerRecorder.isPlaying = true; controllerRecorder.visitEachFrame = true; while (controllerRecorder.isPlaying) { yield return new WaitForSeconds(0.1f); } // Selection has come and gone - resulting in a focus of the grab interactable Assert.That(group.focusInteractable, Is.EqualTo(interactable)); controllerRecorder.isPlaying = false; Object.Destroy(controllerRecorder); yield return null; var offset = new Vector3(10.0f, 0.0f, 0.0f); controllerRecorder = TestUtilities.CreateControllerRecorder(directInteractor, (recording) => { recording.AddRecordingFrameNonAlloc(new XRControllerState(0.0f, offset, Quaternion.identity, InputTrackingState.All, true, false, false, false)); recording.AddRecordingFrameNonAlloc(new XRControllerState(0.1f, offset, Quaternion.identity, InputTrackingState.All, true, false, false, false)); recording.AddRecordingFrameNonAlloc(new XRControllerState(0.2f, offset, Quaternion.identity, InputTrackingState.All, true, true, false, false)); recording.AddRecordingFrameNonAlloc(new XRControllerState(0.3f, offset, Quaternion.identity, InputTrackingState.All, true, true, false, false)); }); controllerRecorder.isPlaying = true; controllerRecorder.visitEachFrame = true; while (controllerRecorder.isPlaying) { yield return new WaitForSeconds(0.1f); } // Selection attempted again with no object present, resulting in loss of focus Assert.That(group.focusInteractable, Is.EqualTo(null)); } [UnityTest] public IEnumerator DirectInteractorCanPassToAnother() { TestUtilities.CreateInteractionManager(); var interactable = TestUtilities.CreateGrabInteractable(); var directInteractor1 = TestUtilities.CreateDirectInteractor(); directInteractor1.name = "directInteractor1"; var controllerRecorder1 = TestUtilities.CreateControllerRecorder(directInteractor1, (recording) => { recording.AddRecordingFrameNonAlloc(new XRControllerState(0.0f, Vector3.zero, Quaternion.identity, InputTrackingState.All, true, false, false, false)); recording.AddRecordingFrameNonAlloc(new XRControllerState(0.1f, Vector3.zero, Quaternion.identity, InputTrackingState.All, true, true, false, false)); recording.AddRecordingFrameNonAlloc(new XRControllerState(float.MaxValue, Vector3.zero, Quaternion.identity, InputTrackingState.All, true, true, false, false)); }); var directInteractor2 = TestUtilities.CreateDirectInteractor(); directInteractor2.name = "directInteractor2"; var controllerRecorder2 = TestUtilities.CreateControllerRecorder(directInteractor2, (recording) => { recording.AddRecordingFrameNonAlloc(new XRControllerState(0.0f, Vector3.zero, Quaternion.identity, InputTrackingState.All, true, false, false, false)); recording.AddRecordingFrameNonAlloc(new XRControllerState(0.1f, Vector3.zero, Quaternion.identity, InputTrackingState.All, true, true, false, false)); recording.AddRecordingFrameNonAlloc(new XRControllerState(float.MaxValue, Vector3.zero, Quaternion.identity, InputTrackingState.All, true, true, false, false)); }); controllerRecorder1.isPlaying = true; controllerRecorder1.visitEachFrame = true; // Wait for Physics update for trigger collision yield return new WaitForFixedUpdate(); yield return new WaitForSeconds(0.1f); // directInteractor1 grabs the interactable Assert.That(interactable.interactorsSelecting, Is.EqualTo(new[] { directInteractor1 }), "In first frame, controller 1 should grab the interactable."); controllerRecorder2.isPlaying = true; controllerRecorder2.visitEachFrame = true; // Wait for the proper interaction that signifies the handoff yield return new WaitForSeconds(0.1f); // directInteractor2 grabs the interactable from directInteractor1 Assert.That(interactable.interactorsSelecting, Is.EqualTo(new[] { directInteractor2 }), "In second frame, controller 2 should grab the interactable."); } [UnityTest] public IEnumerator DirectInteractorReportsValidTargetWhenInteractableRegisteredAfterContact() { TestUtilities.CreateInteractionManager(); var interactor = TestUtilities.CreateDirectInteractor(); var interactable = TestUtilities.CreateGrabInteractable(); interactor.selectActionTrigger = XRBaseInputInteractor.InputTriggerType.State; var controllerRecorder = TestUtilities.CreateControllerRecorder(interactor, (recording) => { recording.AddRecordingFrameNonAlloc(new XRControllerState(0.0f, Vector3.zero, Quaternion.identity, InputTrackingState.All, true, true, false, false)); recording.AddRecordingFrameNonAlloc(new XRControllerState(float.MaxValue, Vector3.zero, Quaternion.identity, InputTrackingState.All, true, true, false, false)); }); controllerRecorder.isPlaying = true; yield return new WaitForFixedUpdate(); yield return null; Assert.That(interactor.interactablesSelected, Is.EqualTo(new[] { interactable })); Assert.That(interactable.interactorsSelecting, Is.EqualTo(new[] { interactor })); var validTargets = new List(); interactor.GetValidTargets(validTargets); Assert.That(validTargets, Is.EqualTo(new[] { interactable })); // Disable the Interactable so it will be removed as a valid target. interactable.enabled = false; yield return null; Assert.That(interactor.interactablesSelected, Is.Empty); Assert.That(interactable.interactorsSelecting, Is.Empty); interactor.GetValidTargets(validTargets); Assert.That(validTargets, Is.Empty); // Re-enable the Interactable. It should not be required to leave and enter the collider to be selected again. interactable.enabled = true; yield return null; Assert.That(interactor.interactablesSelected, Is.EqualTo(new[] { interactable })); Assert.That(interactable.interactorsSelecting, Is.EqualTo(new[] { interactor })); interactor.GetValidTargets(validTargets); Assert.That(validTargets, Is.EqualTo(new[] { interactable })); } [UnityTest] public IEnumerator DirectInteractorUpdatesStayedCollidersOnDisablingInteractableCollider() { TestUtilities.CreateInteractionManager(); var interactor = TestUtilities.CreateDirectInteractor(); var interactable = TestUtilities.CreateGrabInteractable(); yield return new WaitForFixedUpdate(); yield return null; Assert.That(interactor, Is.Not.Null); // Check that the interactor is hovering the interactable. var validTargets = new List(); interactor.GetValidTargets(validTargets); Assert.That(validTargets, Is.EqualTo(new[] { interactable })); Assert.That(interactor.interactablesHovered, Is.EqualTo(new[] { interactable })); Assert.That(interactor.hasHover, Is.True); // Disable the collider component. interactable.GetComponent().enabled = false; yield return new WaitForFixedUpdate(); yield return null; // Since interactable's collider is disabled, it should not show up in the list of valid targets. interactor.GetValidTargets(validTargets); Assert.That(validTargets, Is.Empty); Assert.That(interactor.interactablesHovered, Is.Empty); yield return new WaitForFixedUpdate(); // Additional test: re-enable the collider component, the interactor should re-hover it. interactable.GetComponent().enabled = true; yield return new WaitForFixedUpdate(); yield return null; interactor.GetValidTargets(validTargets); Assert.That(validTargets, Is.EqualTo(new[] { interactable })); Assert.That(interactor.interactablesHovered, Is.EqualTo(new[] { interactable })); Assert.That(interactor.hasHover, Is.True); } [UnityTest] public IEnumerator DirectInteractorUpdatesStayedCollidersOnDeactivatingInteractableObject() { TestUtilities.CreateInteractionManager(); var interactor = TestUtilities.CreateDirectInteractor(); var interactable = TestUtilities.CreateGrabInteractable(); yield return new WaitForFixedUpdate(); yield return null; // Check that the interactable is a valid target of and can be hovered by the interactor. var validTargets = new List(); interactor.GetValidTargets(validTargets); Assert.That(validTargets, Is.EqualTo(new[] { interactable })); Assert.That(interactor.interactablesHovered, Is.EqualTo(new[] { interactable })); Assert.That(interactor.hasHover, Is.True); // De-activate the interactable's gameObject. interactable.gameObject.SetActive(false); // Test to make sure that the interactable gameObject would no longer be a valid target // after its gameObject is set to disable. yield return new WaitForFixedUpdate(); yield return null; interactor.GetValidTargets(validTargets); Assert.That(validTargets, Is.Empty); Assert.That(interactor.hasHover, Is.False); yield return new WaitForFixedUpdate(); // Re-enabling the interactable gameObject, the interactor should re-hover it. interactable.gameObject.SetActive(true); yield return new WaitForFixedUpdate(); yield return null; interactor.GetValidTargets(validTargets); Assert.That(validTargets, Is.EqualTo(new[] { interactable })); Assert.That(interactor.interactablesHovered, Is.EqualTo(new[] { interactable })); Assert.That(interactor.hasHover, Is.True); } [UnityTest] public IEnumerator DirectInteractorUpdatesStayedCollidersOnDestroyingInteractableObject() { TestUtilities.CreateInteractionManager(); var interactor = TestUtilities.CreateDirectInteractor(); var interactable = TestUtilities.CreateGrabInteractable(); yield return new WaitForFixedUpdate(); yield return null; // Check that the interactable is a valid target of and can be hovered by the interactor. var validTargets = new List(); interactor.GetValidTargets(validTargets); Assert.That(validTargets, Is.EqualTo(new[] { interactable })); Assert.That(interactor.interactablesHovered, Is.EqualTo(new[] { interactable })); Assert.That(interactor.hasHover, Is.True); // Destroy the interactable's gameObject. Object.Destroy(interactable.gameObject); // Test to make sure that the interactable gameObject would no longer be a valid target. yield return new WaitForFixedUpdate(); yield return null; interactor.GetValidTargets(validTargets); Assert.That(validTargets, Is.Empty); Assert.That(interactor.hasHover, Is.False); yield return new WaitForFixedUpdate(); // Additional test: re-spawning the interactable gameObject, the interactor should re-hover it. interactable = TestUtilities.CreateGrabInteractable(); yield return new WaitForFixedUpdate(); yield return null; interactor.GetValidTargets(validTargets); Assert.That(validTargets, Is.EqualTo(new[] { interactable })); Assert.That(interactor.interactablesHovered, Is.EqualTo(new[] { interactable })); Assert.That(interactor.hasHover, Is.True); } [UnityTest] public IEnumerator DirectInteractorCanUseTargetFilter() { TestUtilities.CreateInteractionManager(); var interactor = TestUtilities.CreateDirectInteractor(); var interactable = TestUtilities.CreateGrabInteractable(); yield return new WaitForFixedUpdate(); yield return null; var filter = new MockTargetFilter(); Assert.That(filter.callbackExecution, Is.EqualTo(new List())); // Link the filter interactor.targetFilter = filter; Assert.That(interactor.targetFilter, Is.EqualTo(filter)); Assert.That(filter.callbackExecution, Is.EqualTo(new List { TargetFilterCallback.Link })); // Process the filter var validTargets = new List(); interactor.GetValidTargets(validTargets); Assert.That(interactor.targetFilter, Is.EqualTo(filter)); Assert.That(validTargets, Is.EqualTo(new[] { interactable })); Assert.That(filter.callbackExecution, Is.EqualTo(new List { TargetFilterCallback.Link, TargetFilterCallback.Process })); // Disable the filter and check if it will no longer be processed filter.canProcess = false; interactor.GetValidTargets(validTargets); Assert.That(filter.callbackExecution, Is.EqualTo(new List { TargetFilterCallback.Link, TargetFilterCallback.Process })); // Unlink the filter interactor.targetFilter = null; Assert.That(interactor.targetFilter, Is.EqualTo(null)); Assert.That(filter.callbackExecution, Is.EqualTo(new List { TargetFilterCallback.Link, TargetFilterCallback.Process, TargetFilterCallback.Unlink })); } } }