using System.Collections; using NUnit.Framework; using UnityEngine.TestTools; using UnityEngine.XR.Interaction.Toolkit.Filtering; using UnityEngine.XR.Interaction.Toolkit.Interactables; using UnityEngine.XR.Interaction.Toolkit.Interactors; namespace UnityEngine.XR.Interaction.Toolkit.Tests { [TestFixture] class InteractableTests { public enum InteractorPositionOption { InteractorInsideCollider, InteractorOutsideCollider, } public enum InteractionOption { HoverOnly, SelectOnly, HoverAndSelect, } static readonly InteractionOption[] s_InteractionOptions = { InteractionOption.HoverOnly, InteractionOption.SelectOnly, InteractionOption.HoverAndSelect, }; static readonly InteractableSelectMode[] s_SelectModes = { InteractableSelectMode.Single, InteractableSelectMode.Multiple, }; static readonly InteractableFocusMode[] s_FocusModes = { InteractableFocusMode.Single, InteractableFocusMode.Multiple, }; static readonly XRBaseInteractable.DistanceCalculationMode[] s_DistanceCalculationMode = { XRBaseInteractable.DistanceCalculationMode.TransformPosition, XRBaseInteractable.DistanceCalculationMode.ColliderPosition, XRBaseInteractable.DistanceCalculationMode.ColliderVolume, }; static readonly InteractorPositionOption[] s_InteractorPositionOption = { InteractorPositionOption.InteractorInsideCollider, InteractorPositionOption.InteractorOutsideCollider, }; static readonly InteractorHandedness[] s_HandednessValues = { InteractorHandedness.None, InteractorHandedness.Left, InteractorHandedness.Right, }; [TearDown] public void TearDown() { TestUtilities.DestroyAllSceneObjects(); } [UnityTest] public IEnumerator InteractableIsHoveredOrSelectedBy([ValueSource(nameof(s_HandednessValues))] InteractorHandedness handedness) { TestUtilities.CreateInteractionManager(); var interactor = TestUtilities.CreateMockInteractor(); interactor.handedness = handedness; var interactable = TestUtilities.CreateGrabInteractable(); interactable.selectMode = InteractableSelectMode.Multiple; Assert.That(interactable.isHovered, Is.False); Assert.That(interactable.IsHoveredByLeft(), Is.False); Assert.That(interactable.IsHoveredByRight(), Is.False); Assert.That(interactable.isSelected, Is.False); Assert.That(interactable.IsSelectedByLeft(), Is.False); Assert.That(interactable.IsSelectedByRight(), Is.False); interactor.validTargets.Add(interactable); yield return null; Assert.That(interactable.isHovered, Is.True); Assert.That(interactable.IsHoveredByLeft(), Is.EqualTo(handedness == InteractorHandedness.Left)); Assert.That(interactable.IsHoveredByRight(), Is.EqualTo(handedness == InteractorHandedness.Right)); Assert.That(interactable.isSelected, Is.True); Assert.That(interactable.IsSelectedByLeft(), Is.EqualTo(handedness == InteractorHandedness.Left)); Assert.That(interactable.IsSelectedByRight(), Is.EqualTo(handedness == InteractorHandedness.Right)); // Add another interactor with opposite handedness. If None, make another None type. var interactorOpposite = TestUtilities.CreateMockInteractor(); switch (handedness) { case InteractorHandedness.Left: interactorOpposite.handedness = InteractorHandedness.Right; break; case InteractorHandedness.Right: interactorOpposite.handedness = InteractorHandedness.Left; break; default: interactorOpposite.handedness = InteractorHandedness.None; break; } interactorOpposite.validTargets.Add(interactable); yield return null; Assert.That(interactable.isHovered, Is.True); Assert.That(interactable.IsHoveredByLeft(), Is.EqualTo(handedness != InteractorHandedness.None)); Assert.That(interactable.IsHoveredByRight(), Is.EqualTo(handedness != InteractorHandedness.None)); Assert.That(interactable.isSelected, Is.True); Assert.That(interactable.IsSelectedByLeft(), Is.EqualTo(handedness != InteractorHandedness.None)); Assert.That(interactable.IsSelectedByRight(), Is.EqualTo(handedness != InteractorHandedness.None)); } [UnityTest] public IEnumerator InteractableIsHoveredWhileAnyInteractorHovering() { TestUtilities.CreateInteractionManager(); var interactor1 = TestUtilities.CreateMockInteractor(); var interactor2 = TestUtilities.CreateMockInteractor(); var interactable = TestUtilities.CreateGrabInteractable(); Assert.That(interactable.isHovered, Is.False); Assert.That(interactable.interactorsHovering, Is.Empty); interactor1.validTargets.Add(interactable); interactor2.validTargets.Add(interactable); yield return null; Assert.That(interactable.isHovered, Is.True); Assert.That(interactable.interactorsHovering, Is.EquivalentTo(new[] { interactor1, interactor2 })); interactor2.validTargets.Clear(); yield return null; Assert.That(interactable.isHovered, Is.True); Assert.That(interactable.interactorsHovering, Is.EquivalentTo(new[] { interactor1 })); interactor1.validTargets.Clear(); yield return null; Assert.That(interactable.isHovered, Is.False); Assert.That(interactable.interactorsHovering, Is.Empty); } [UnityTest] public IEnumerator InteractableSelectModeSelect([ValueSource(nameof(s_SelectModes))] InteractableSelectMode selectMode) { TestUtilities.CreateInteractionManager(); var interactor1 = TestUtilities.CreateMockInteractor(); var interactor2 = TestUtilities.CreateMockInteractor(); var interactable = TestUtilities.CreateGrabInteractable(); interactable.selectMode = selectMode; interactor1.validTargets.Add(interactable); yield return null; Assert.That(interactable.isSelected, Is.True); Assert.That(interactable.interactorsSelecting, Is.EqualTo(new[] { interactor1 })); interactor2.validTargets.Add(interactable); yield return null; Assert.That(interactable.isSelected, Is.True); switch (selectMode) { case InteractableSelectMode.Single: Assert.That(interactable.interactorsSelecting, Is.EqualTo(new[] { interactor2 })); break; case InteractableSelectMode.Multiple: Assert.That(interactable.interactorsSelecting, Is.EqualTo(new[] { interactor1, interactor2 })); break; default: Assert.Fail($"Unhandled {nameof(InteractableSelectMode)}={selectMode}"); break; } } [UnityTest] public IEnumerator InteractableFocusModeSelect([ValueSource(nameof(s_FocusModes))] InteractableFocusMode focusMode) { TestUtilities.CreateInteractionManager(); var group1 = TestUtilities.CreateInteractionGroup(); var group2 = TestUtilities.CreateInteractionGroup(); var interactor1 = TestUtilities.CreateMockInteractor(); var interactor2 = TestUtilities.CreateMockInteractor(); group1.AddGroupMember(interactor1); group2.AddGroupMember(interactor2); var interactable = TestUtilities.CreateGrabInteractable(); interactable.focusMode = focusMode; interactor1.validTargets.Add(interactable); yield return null; Assert.That(interactable.isFocused, Is.True); Assert.That(interactable.interactionGroupsFocusing, Is.EqualTo(new[] { group1 })); interactor2.validTargets.Add(interactable); yield return null; Assert.That(interactable.isFocused, Is.True); switch (focusMode) { case InteractableFocusMode.Single: Assert.That(interactable.interactionGroupsFocusing, Is.EqualTo(new[] { group2 })); break; case InteractableFocusMode.Multiple: Assert.That(interactable.interactionGroupsFocusing, Is.EqualTo(new[] { group1, group2 })); break; default: Assert.Fail($"Unhandled {nameof(InteractableFocusMode)}={focusMode}"); break; } } [Test] public void InteractableDistanceCalculationModeWithInteractorInsideColliders([ValueSource(nameof(s_DistanceCalculationMode))] XRBaseInteractable.DistanceCalculationMode distanceCalculationMode) { TestUtilities.CreateInteractionManager(); IXRInteractor interactor = TestUtilities.CreateMockInteractor(); var interactable = TestUtilities.CreateSimpleInteractableWithColliders(); interactor.transform.position = new Vector3(0.5f, 0f, 0f); interactable.distanceCalculationMode = distanceCalculationMode; Assert.That(interactable.distanceCalculationMode, Is.EqualTo(distanceCalculationMode)); Assert.That(interactable.colliders.Count, Is.GreaterThan(1)); var distanceSqr = interactable.GetDistanceSqrToInteractor(interactor); switch (distanceCalculationMode) { case XRBaseInteractable.DistanceCalculationMode.TransformPosition: Assert.That(distanceSqr, Is.EqualTo(0.5f * 0.5f)); break; case XRBaseInteractable.DistanceCalculationMode.ColliderPosition: Assert.That(distanceSqr, Is.EqualTo(0.5f * 0.5f)); break; case XRBaseInteractable.DistanceCalculationMode.ColliderVolume: Assert.That(distanceSqr, Is.EqualTo(0f)); break; default: Assert.Fail($"Unhandled {nameof(InteractableSelectMode)}={distanceCalculationMode}"); break; } } [Test] public void InteractableDistanceCalculationMode( [ValueSource(nameof(s_DistanceCalculationMode))] XRBaseInteractable.DistanceCalculationMode distanceCalculationMode, [ValueSource(nameof(s_InteractorPositionOption))] InteractorPositionOption interactorPositionOption) { TestUtilities.CreateInteractionManager(); IXRInteractor interactor = TestUtilities.CreateMockInteractor(); var interactable = TestUtilities.CreateSimpleInteractableWithColliders(); // The sphere or box colliders have size of 1f and they are translated (below) to a random position inside a sphere of radius 0.25f var interactorPosition = interactorPositionOption == InteractorPositionOption.InteractorInsideCollider ? Random.insideUnitSphere * 0.2f : Random.onUnitSphere * 5f; interactor.transform.position = interactorPosition; interactable.distanceCalculationMode = distanceCalculationMode; Assert.That(interactable.distanceCalculationMode, Is.EqualTo(distanceCalculationMode)); Assert.That(interactable.colliders.Count, Is.GreaterThan(1)); // Move the colliders to random positions not far from the interactable foreach (var col in interactable.colliders) col.transform.position = Random.insideUnitSphere * 0.25f; var distanceSqr = interactable.GetDistanceSqrToInteractor(interactor); switch (distanceCalculationMode) { case XRBaseInteractable.DistanceCalculationMode.TransformPosition: var offset = interactable.transform.position - interactorPosition; Assert.That(distanceSqr, Is.EqualTo(offset.sqrMagnitude)); break; case XRBaseInteractable.DistanceCalculationMode.ColliderPosition: var minColDistanceSqr = float.MaxValue; foreach (var col in interactable.colliders) { offset = col.transform.position - interactorPosition; minColDistanceSqr = Mathf.Min(minColDistanceSqr, offset.sqrMagnitude); } Assert.That(distanceSqr, Is.EqualTo(minColDistanceSqr)); break; case XRBaseInteractable.DistanceCalculationMode.ColliderVolume: minColDistanceSqr = float.MaxValue; foreach (var col in interactable.colliders) { offset = col.ClosestPoint(interactorPosition) - interactorPosition; minColDistanceSqr = Mathf.Min(minColDistanceSqr, offset.sqrMagnitude); } Assert.That(distanceSqr, Is.EqualTo(minColDistanceSqr)); break; default: Assert.Fail($"Unhandled {nameof(InteractableSelectMode)}={distanceCalculationMode}"); break; } } [UnityTest] public IEnumerator InteractableCanProcessHoverFilters() { TestUtilities.CreateInteractionManager(); var interactor = TestUtilities.CreateMockInteractor(); var interactable = TestUtilities.CreateSimpleInteractable(); interactor.validTargets.Add(interactable); var filter1WasProcessed = false; var filter1 = new XRHoverFilterDelegate((x, y) => { filter1WasProcessed = true; return true; }); interactable.hoverFilters.Add(filter1); var filter2WasProcessed = false; var filter2 = new XRHoverFilterDelegate((x, y) => { filter2WasProcessed = true; return true; }); interactable.hoverFilters.Add(filter2); yield return null; Assert.That(filter1WasProcessed, Is.True); Assert.That(filter2WasProcessed, Is.True); Assert.That(interactable.interactorsHovering, Is.EquivalentTo(new[] { interactor })); // Add filter that returns false var filter3WasProcessed = false; var filter3 = new XRHoverFilterDelegate((x, y) => { filter3WasProcessed = true; return false; }); interactable.hoverFilters.Add(filter3); yield return null; Assert.That(filter3WasProcessed, Is.True); Assert.That(interactable.interactorsHovering, Is.Empty); } [UnityTest] public IEnumerator InteractableCanProcessSelectFilters() { TestUtilities.CreateInteractionManager(); var interactor = TestUtilities.CreateMockInteractor(); var interactable = TestUtilities.CreateSimpleInteractable(); interactor.validTargets.Add(interactable); var filter1WasProcessed = false; var filter1 = new XRSelectFilterDelegate((x, y) => { filter1WasProcessed = true; return true; }); interactable.selectFilters.Add(filter1); var filter2WasProcessed = false; var filter2 = new XRSelectFilterDelegate((x, y) => { filter2WasProcessed = true; return true; }); interactable.selectFilters.Add(filter2); yield return null; Assert.That(filter1WasProcessed, Is.True); Assert.That(filter2WasProcessed, Is.True); Assert.That(interactable.interactorsSelecting, Is.EquivalentTo(new[] { interactor })); // Add filter that returns false var filter3WasProcessed = false; var filter3 = new XRSelectFilterDelegate((x, y) => { filter3WasProcessed = true; return false; }); interactable.selectFilters.Add(filter3); yield return null; Assert.That(filter3WasProcessed, Is.True); Assert.That(interactable.interactorsSelecting, Is.Empty); } [UnityTest] public IEnumerator InteractableCanProcessInteractionStrengthFilters([ValueSource(nameof(s_InteractionOptions))] InteractionOption interactionOption) { TestUtilities.CreateInteractionManager(); var interactor = TestUtilities.CreateMockInteractor(); var interactable = TestUtilities.CreateSimpleInteractable(); interactor.validTargets.Add(interactable); switch (interactionOption) { case InteractionOption.HoverOnly: interactor.allowSelect = false; break; case InteractionOption.SelectOnly: interactor.allowHover = false; break; case InteractionOption.HoverAndSelect: break; default: Assert.Fail($"Unhandled {nameof(InteractionOption)}={interactionOption}"); break; } var filter1ProcessedCount = 0; var filter1InputStrength = -1f; const float filter1Strength = 0.5f; var filter1 = new XRInteractionStrengthFilterDelegate((_, __, strength) => { filter1ProcessedCount++; filter1InputStrength = strength; return filter1Strength; }); interactable.interactionStrengthFilters.Add(filter1); var filter2ProcessedCount = 0; var filter2InputStrength = -1f; const float filter2Strength = 0.75f; var filter2 = new XRInteractionStrengthFilterDelegate((_, __, strength) => { filter2ProcessedCount++; filter2InputStrength = strength; return filter2Strength; }); interactable.interactionStrengthFilters.Add(filter2); yield return null; var expectedInitialInputStrength = 0f; switch (interactionOption) { case InteractionOption.HoverOnly: Assert.That(interactable.interactorsHovering, Is.EquivalentTo(new[] { interactor })); Assert.That(interactable.interactorsSelecting, Is.Empty); expectedInitialInputStrength = 0f; break; case InteractionOption.SelectOnly: Assert.That(interactable.interactorsSelecting, Is.EquivalentTo(new[] { interactor })); Assert.That(interactable.interactorsHovering, Is.Empty); expectedInitialInputStrength = 1f; break; case InteractionOption.HoverAndSelect: Assert.That(interactable.interactorsHovering, Is.EquivalentTo(new[] { interactor })); Assert.That(interactable.interactorsSelecting, Is.EquivalentTo(new[] { interactor })); expectedInitialInputStrength = 1f; break; default: Assert.Fail($"Unhandled {nameof(InteractionOption)}={interactionOption}"); break; } Assert.That(filter1ProcessedCount, Is.EqualTo(1)); Assert.That(filter2ProcessedCount, Is.EqualTo(1)); Assert.That(filter1InputStrength, Is.EqualTo(expectedInitialInputStrength)); Assert.That(filter2InputStrength, Is.EqualTo(filter1Strength)); Assert.That(interactable.largestInteractionStrength.Value, Is.EqualTo(filter2Strength)); Assert.That(interactor.largestInteractionStrength.Value, Is.EqualTo(filter2Strength)); Assert.That(interactable.GetInteractionStrength(interactor), Is.EqualTo(filter2Strength)); Assert.That(interactor.GetInteractionStrength(interactable), Is.EqualTo(filter2Strength)); } [UnityTest] public IEnumerator InteractableLosesFocusOnSelectOfOtherInteractable() { TestUtilities.CreateInteractionManager(); var group = TestUtilities.CreateInteractionGroup(); var directInteractor = TestUtilities.CreateDirectInteractor(); group.AddGroupMember(directInteractor); var interactable = TestUtilities.CreateGrabInteractable(); var interactable2 = TestUtilities.CreateGrabInteractable(); interactable2.transform.position = new Vector3(10f, 0, 0); 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)); Assert.That(interactable.isFocused, Is.EqualTo(true)); Assert.That(interactable2.isFocused, Is.EqualTo(false)); 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 2nd object in focus. Assert.That(group.focusInteractable, Is.EqualTo(interactable2)); Assert.That(interactable.isFocused, Is.EqualTo(false)); Assert.That(interactable2.isFocused, Is.EqualTo(true)); } } }