using System; using System.Collections; using System.Collections.Generic; using System.Linq; using NUnit.Framework; using Unity.XR.CoreUtils; using UnityEngine.TestTools; using UnityEngine.TestTools.Utils; using UnityEngine.XR.Interaction.Toolkit.Interactables; using UnityEngine.XR.Interaction.Toolkit.Interactors; using UnityEngine.XR.Interaction.Toolkit.Transformers; namespace UnityEngine.XR.Interaction.Toolkit.Tests { [TestFixture] class GrabInteractableTests { static readonly XRBaseInteractable.MovementType[] s_MovementTypes = { XRBaseInteractable.MovementType.VelocityTracking, XRBaseInteractable.MovementType.Kinematic, XRBaseInteractable.MovementType.Instantaneous, }; static readonly Type[] s_GrabTransformers = { typeof(XRSingleGrabFreeTransformer), typeof(XRDualGrabFreeTransformer), typeof(XRGeneralGrabTransformer), }; static readonly Type[] s_MockTransformerTypes = { typeof(MockGrabTransformer), typeof(MockDropTransformer), }; static readonly bool[] s_BooleanValues = { false, true }; [TearDown] public void TearDown() { TestUtilities.DestroyAllSceneObjects(); } static IEnumerator WaitForSteadyState(XRBaseInteractable.MovementType movementType) { yield return null; if (movementType == XRBaseInteractable.MovementType.VelocityTracking) yield return new WaitForFixedUpdate(); yield return new WaitForFixedUpdate(); } [UnityTest] public IEnumerator CenteredObjectWithAttachTransformMovesToExpectedPosition([ValueSource(nameof(s_MovementTypes))] XRBaseInteractable.MovementType movementType, [ValueSource(nameof(s_GrabTransformers))] Type grabTransformerType) { // Create Grab Interactable at some arbitrary point var grabInteractableGO = GameObject.CreatePrimitive(PrimitiveType.Cube); grabInteractableGO.name = "Grab Interactable"; grabInteractableGO.transform.localPosition = new Vector3(1f, 2f, 3f); grabInteractableGO.transform.localRotation = Quaternion.identity; var boxCollider = grabInteractableGO.GetComponent(); var rigidbody = grabInteractableGO.AddComponent(); rigidbody.useGravity = false; rigidbody.isKinematic = true; var grabInteractable = grabInteractableGO.AddComponent(); grabInteractable.movementType = movementType; grabInteractable.addDefaultGrabTransformers = false; grabInteractableGO.AddComponent(grabTransformerType); TestUtilities.DisableDelayProperties(grabInteractable); // Set the Attach Transform to the back upper-right corner of the cube // to test an attach transform different from the transform position (which is also its center). var grabInteractableAttach = new GameObject("Grab Interactable Attach").transform; var attachOffset = new Vector3(0.5f, 0.5f, -0.5f); grabInteractableAttach.SetParent(grabInteractable.transform); grabInteractableAttach.localPosition = attachOffset; grabInteractableAttach.localRotation = Quaternion.identity; grabInteractable.attachTransform = grabInteractableAttach; // The built-in Cube resource has its center at the center of the cube. var centerOffset = Vector3.zero; // Wait for physics update to ensure the Rigidbody is stable and center of mass has been calculated yield return new WaitForFixedUpdate(); yield return null; Assert.That(grabInteractable.isSelected, Is.False); Assert.That(grabInteractable.attachTransform.position, Is.EqualTo(new Vector3(1f, 2f, 3f) + attachOffset).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(grabInteractable.attachTransform.rotation, Is.EqualTo(Quaternion.identity).Using(QuaternionEqualityComparer.Instance)); Assert.That(grabInteractable.transform.position, Is.EqualTo(new Vector3(1f, 2f, 3f)).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(grabInteractable.transform.rotation, Is.EqualTo(Quaternion.identity).Using(QuaternionEqualityComparer.Instance)); Assert.That(boxCollider, Is.Not.Null); Assert.That(boxCollider.center, Is.EqualTo(centerOffset).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(rigidbody.position, Is.EqualTo(new Vector3(1f, 2f, 3f)).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(rigidbody.centerOfMass, Is.EqualTo(centerOffset).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(rigidbody.worldCenterOfMass, Is.EqualTo(new Vector3(1f, 2f, 3f) + centerOffset).Using(Vector3ComparerWithEqualsOperator.Instance)); // Create Interactor at some arbitrary point away from the Interactable var interactor = TestUtilities.CreateMockInteractor(); var targetPosition = Vector3.zero; yield return null; Assert.That(grabInteractable.isSelected, Is.False); Assert.That(interactor.hasSelection, Is.False); Assert.That(interactor.interactablesSelected, Is.Empty); Assert.That(interactor.attachTransform, Is.Not.Null); Assert.That(interactor.attachTransform.position, Is.EqualTo(targetPosition).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(interactor.attachTransform.rotation, Is.EqualTo(Quaternion.identity).Using(QuaternionEqualityComparer.Instance)); // Set valid so it will be selected next frame by the Interaction Manager interactor.validTargets.Add(grabInteractable); yield return WaitForSteadyState(movementType); Assert.That(grabInteractable.isSelected, Is.True); Assert.That(grabInteractable.attachTransform.position, Is.EqualTo(targetPosition).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(grabInteractable.attachTransform.rotation, Is.EqualTo(Quaternion.identity).Using(QuaternionEqualityComparer.Instance)); Assert.That(grabInteractable.transform.position, Is.EqualTo(targetPosition - attachOffset).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(grabInteractable.transform.rotation, Is.EqualTo(Quaternion.identity).Using(QuaternionEqualityComparer.Instance)); Assert.That(interactor.attachTransform.position, Is.EqualTo(targetPosition).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(interactor.attachTransform.rotation, Is.EqualTo(Quaternion.identity).Using(QuaternionEqualityComparer.Instance)); Assert.That(rigidbody.position, Is.EqualTo(targetPosition - attachOffset).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(rigidbody.rotation, Is.EqualTo(Quaternion.identity).Using(QuaternionEqualityComparer.Instance)); // Move the attach transform on the Interactor after being grabbed targetPosition = new Vector3(5f, 5f, 5f); interactor.attachTransform.position = targetPosition; yield return WaitForSteadyState(movementType); Assert.That(grabInteractable.isSelected, Is.True); Assert.That(grabInteractable.attachTransform.position, Is.EqualTo(targetPosition).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(grabInteractable.attachTransform.rotation, Is.EqualTo(Quaternion.identity).Using(QuaternionEqualityComparer.Instance)); Assert.That(grabInteractable.transform.position, Is.EqualTo(targetPosition - attachOffset).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(grabInteractable.transform.rotation, Is.EqualTo(Quaternion.identity).Using(QuaternionEqualityComparer.Instance)); Assert.That(interactor.attachTransform.position, Is.EqualTo(targetPosition).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(interactor.attachTransform.rotation, Is.EqualTo(Quaternion.identity).Using(QuaternionEqualityComparer.Instance)); Assert.That(rigidbody.position, Is.EqualTo(targetPosition - attachOffset).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(rigidbody.rotation, Is.EqualTo(Quaternion.identity).Using(QuaternionEqualityComparer.Instance)); // The XR General Grab Transformer does not support the attach transform of the interactable being modified after // it is already grabbed (it only supports that with the interactor's attach transform changing). if (grabTransformerType != typeof(XRGeneralGrabTransformer)) { // Move the attach transform on the Interactable to the back lower-right corner of the cube attachOffset = new Vector3(0.5f, -0.5f, -0.5f); grabInteractable.attachTransform.localPosition = attachOffset; yield return WaitForSteadyState(movementType); Assert.That(grabInteractable.isSelected, Is.True); Assert.That(grabInteractable.attachTransform.position, Is.EqualTo(targetPosition).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(grabInteractable.attachTransform.rotation, Is.EqualTo(Quaternion.identity).Using(QuaternionEqualityComparer.Instance)); Assert.That(grabInteractable.transform.position, Is.EqualTo(targetPosition - attachOffset).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(grabInteractable.transform.rotation, Is.EqualTo(Quaternion.identity).Using(QuaternionEqualityComparer.Instance)); Assert.That(interactor.attachTransform.position, Is.EqualTo(targetPosition).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(interactor.attachTransform.rotation, Is.EqualTo(Quaternion.identity).Using(QuaternionEqualityComparer.Instance)); Assert.That(rigidbody.position, Is.EqualTo(targetPosition - attachOffset).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(rigidbody.rotation, Is.EqualTo(Quaternion.identity).Using(QuaternionEqualityComparer.Instance)); } } [UnityTest] public IEnumerator CenteredObjectWithoutAttachTransformMovesToExpectedPosition([ValueSource(nameof(s_MovementTypes))] XRBaseInteractable.MovementType movementType) { // Create Grab Interactable at some arbitrary point var grabInteractableGO = GameObject.CreatePrimitive(PrimitiveType.Cube); grabInteractableGO.name = "Grab Interactable"; grabInteractableGO.transform.localPosition = new Vector3(1f, 2f, 3f); grabInteractableGO.transform.localRotation = Quaternion.identity; var boxCollider = grabInteractableGO.GetComponent(); var rigidbody = grabInteractableGO.AddComponent(); rigidbody.useGravity = false; rigidbody.isKinematic = true; var grabInteractable = grabInteractableGO.AddComponent(); grabInteractable.movementType = movementType; TestUtilities.DisableDelayProperties(grabInteractable); // Keep the Attach Transform null to use the transform itself (which is also its center). var attachOffset = Vector3.zero; grabInteractable.attachTransform = null; // The built-in Cube resource has its center at the center of the cube. var centerOffset = Vector3.zero; // Wait for physics update to ensure the Rigidbody is stable and center of mass has been calculated yield return new WaitForFixedUpdate(); yield return null; Assert.That(grabInteractable.isSelected, Is.False); Assert.That(grabInteractable.attachTransform, Is.Null); Assert.That(grabInteractable.transform.position, Is.EqualTo(new Vector3(1f, 2f, 3f)).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(grabInteractable.transform.rotation, Is.EqualTo(Quaternion.identity).Using(QuaternionEqualityComparer.Instance)); Assert.That(boxCollider, Is.Not.Null); Assert.That(boxCollider.center, Is.EqualTo(centerOffset).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(rigidbody.position, Is.EqualTo(new Vector3(1f, 2f, 3f)).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(rigidbody.centerOfMass, Is.EqualTo(centerOffset).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(rigidbody.worldCenterOfMass, Is.EqualTo(new Vector3(1f, 2f, 3f) + centerOffset).Using(Vector3ComparerWithEqualsOperator.Instance)); // Create Interactor at some arbitrary point away from the Interactable var interactor = TestUtilities.CreateMockInteractor(); var targetPosition = Vector3.zero; yield return null; Assert.That(grabInteractable.isSelected, Is.False); Assert.That(interactor.hasSelection, Is.False); Assert.That(interactor.interactablesSelected, Is.Empty); Assert.That(interactor.attachTransform, Is.Not.Null); Assert.That(interactor.attachTransform.position, Is.EqualTo(targetPosition).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(interactor.attachTransform.rotation, Is.EqualTo(Quaternion.identity).Using(QuaternionEqualityComparer.Instance)); // Set valid so it will be selected next frame by the Interaction Manager interactor.validTargets.Add(grabInteractable); yield return WaitForSteadyState(movementType); Assert.That(grabInteractable.isSelected, Is.True); Assert.That(grabInteractable.attachTransform, Is.Null); Assert.That(grabInteractable.transform.position, Is.EqualTo(targetPosition - attachOffset).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(grabInteractable.transform.rotation, Is.EqualTo(Quaternion.identity).Using(QuaternionEqualityComparer.Instance)); Assert.That(interactor.attachTransform.position, Is.EqualTo(targetPosition).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(interactor.attachTransform.rotation, Is.EqualTo(Quaternion.identity).Using(QuaternionEqualityComparer.Instance)); Assert.That(rigidbody.position, Is.EqualTo(targetPosition - attachOffset).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(rigidbody.rotation, Is.EqualTo(Quaternion.identity).Using(QuaternionEqualityComparer.Instance)); // Move the attach transform on the Interactor after being grabbed targetPosition = new Vector3(5f, 5f, 5f); interactor.attachTransform.position = targetPosition; yield return WaitForSteadyState(movementType); Assert.That(grabInteractable.isSelected, Is.True); Assert.That(grabInteractable.attachTransform, Is.Null); Assert.That(grabInteractable.transform.position, Is.EqualTo(targetPosition - attachOffset).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(grabInteractable.transform.rotation, Is.EqualTo(Quaternion.identity).Using(QuaternionEqualityComparer.Instance)); Assert.That(interactor.attachTransform.position, Is.EqualTo(targetPosition).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(interactor.attachTransform.rotation, Is.EqualTo(Quaternion.identity).Using(QuaternionEqualityComparer.Instance)); Assert.That(rigidbody.position, Is.EqualTo(targetPosition - attachOffset).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(rigidbody.rotation, Is.EqualTo(Quaternion.identity).Using(QuaternionEqualityComparer.Instance)); } [UnityTest] public IEnumerator NonCenteredObjectMovesToExpectedPosition([ValueSource(nameof(s_MovementTypes))] XRBaseInteractable.MovementType movementType, [ValueSource(nameof(s_GrabTransformers))] Type grabTransformerType) { // Create a cube mesh with the pivot position at the bottom center // rather than the built-in Cube resource which has its center at the // center of the cube. var cubePrimitiveGO = GameObject.CreatePrimitive(PrimitiveType.Cube); var cubePrimitiveMesh = cubePrimitiveGO.GetComponent().sharedMesh; var centerOffset = new Vector3(0f, 0.5f, 0f); var offCenterMesh = new Mesh { vertices = cubePrimitiveMesh.vertices.Select(vertex => vertex + centerOffset).ToArray(), triangles = cubePrimitiveMesh.triangles.ToArray(), normals = cubePrimitiveMesh.normals.ToArray(), }; cubePrimitiveGO.SetActive(false); Object.Destroy(cubePrimitiveGO); // Create Grab Interactable at some arbitrary point var grabInteractableGO = new GameObject("Grab Interactable"); grabInteractableGO.transform.localPosition = new Vector3(1f, 2f, 3f); grabInteractableGO.transform.localRotation = Quaternion.identity; var meshFilter = grabInteractableGO.AddComponent(); meshFilter.sharedMesh = offCenterMesh; grabInteractableGO.AddComponent(); var boxCollider = grabInteractableGO.AddComponent(); var rigidbody = grabInteractableGO.AddComponent(); rigidbody.useGravity = false; rigidbody.isKinematic = true; var grabInteractable = grabInteractableGO.AddComponent(); grabInteractable.movementType = movementType; grabInteractable.addDefaultGrabTransformers = false; grabInteractableGO.AddComponent(grabTransformerType); TestUtilities.DisableDelayProperties(grabInteractable); // Set the Attach Transform to the back upper-right corner of the cube // to test an attach transform different from both the transform position and center. var grabInteractableAttach = new GameObject("Grab Interactable Attach").transform; var attachOffset = new Vector3(0.5f, 1f, -0.5f); grabInteractableAttach.SetParent(grabInteractable.transform); grabInteractableAttach.localPosition = attachOffset; grabInteractableAttach.localRotation = Quaternion.identity; grabInteractable.attachTransform = grabInteractableAttach; // Wait for physics update to ensure the Rigidbody is stable and center of mass has been calculated yield return new WaitForFixedUpdate(); yield return null; Assert.That(grabInteractable.isSelected, Is.False); Assert.That(grabInteractable.attachTransform.position, Is.EqualTo(new Vector3(1f, 2f, 3f) + attachOffset).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(grabInteractable.attachTransform.rotation, Is.EqualTo(Quaternion.identity).Using(QuaternionEqualityComparer.Instance)); Assert.That(grabInteractable.transform.position, Is.EqualTo(new Vector3(1f, 2f, 3f)).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(grabInteractable.transform.rotation, Is.EqualTo(Quaternion.identity).Using(QuaternionEqualityComparer.Instance)); Assert.That(boxCollider.center, Is.EqualTo(centerOffset).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(rigidbody.position, Is.EqualTo(new Vector3(1f, 2f, 3f)).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(rigidbody.centerOfMass, Is.EqualTo(centerOffset).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(rigidbody.worldCenterOfMass, Is.EqualTo(new Vector3(1f, 2f, 3f) + centerOffset).Using(Vector3ComparerWithEqualsOperator.Instance)); // Create Interactor at some arbitrary point away from the Interactable var interactor = TestUtilities.CreateMockInteractor(); var targetPosition = Vector3.zero; yield return null; Assert.That(grabInteractable.isSelected, Is.False); Assert.That(interactor.hasSelection, Is.False); Assert.That(interactor.interactablesSelected, Is.Empty); Assert.That(interactor.attachTransform, Is.Not.Null); Assert.That(interactor.attachTransform.position, Is.EqualTo(targetPosition).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(interactor.attachTransform.rotation, Is.EqualTo(Quaternion.identity).Using(QuaternionEqualityComparer.Instance)); // Set valid so it will be selected next frame by the Interaction Manager interactor.validTargets.Add(grabInteractable); yield return WaitForSteadyState(movementType); Assert.That(grabInteractable.isSelected, Is.True); Assert.That(grabInteractable.attachTransform.position, Is.EqualTo(targetPosition).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(grabInteractable.attachTransform.rotation, Is.EqualTo(Quaternion.identity).Using(QuaternionEqualityComparer.Instance)); Assert.That(grabInteractable.transform.position, Is.EqualTo(targetPosition - attachOffset).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(grabInteractable.transform.rotation, Is.EqualTo(Quaternion.identity).Using(QuaternionEqualityComparer.Instance)); Assert.That(interactor.attachTransform.position, Is.EqualTo(targetPosition).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(interactor.attachTransform.rotation, Is.EqualTo(Quaternion.identity).Using(QuaternionEqualityComparer.Instance)); Assert.That(rigidbody.position, Is.EqualTo(targetPosition - attachOffset).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(rigidbody.rotation, Is.EqualTo(Quaternion.identity).Using(QuaternionEqualityComparer.Instance)); // Move the attach transform on the Interactor after being grabbed targetPosition = new Vector3(5f, 5f, 5f); interactor.attachTransform.position = targetPosition; yield return WaitForSteadyState(movementType); Assert.That(grabInteractable.isSelected, Is.True); Assert.That(grabInteractable.attachTransform.position, Is.EqualTo(targetPosition).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(grabInteractable.attachTransform.rotation, Is.EqualTo(Quaternion.identity).Using(QuaternionEqualityComparer.Instance)); Assert.That(grabInteractable.transform.position, Is.EqualTo(targetPosition - attachOffset).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(grabInteractable.transform.rotation, Is.EqualTo(Quaternion.identity).Using(QuaternionEqualityComparer.Instance)); Assert.That(interactor.attachTransform.position, Is.EqualTo(targetPosition).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(interactor.attachTransform.rotation, Is.EqualTo(Quaternion.identity).Using(QuaternionEqualityComparer.Instance)); Assert.That(rigidbody.position, Is.EqualTo(targetPosition - attachOffset).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(rigidbody.rotation, Is.EqualTo(Quaternion.identity).Using(QuaternionEqualityComparer.Instance)); // The XR General Grab Transformer does not support the attach transform of the interactable being modified after // it is already grabbed (it only supports that with the interactor's attach transform changing). if (grabTransformerType != typeof(XRGeneralGrabTransformer)) { // Move the attach transform on the Interactable to the back lower-right corner of the cube attachOffset = new Vector3(0.5f, 0f, -0.5f); grabInteractable.attachTransform.localPosition = attachOffset; yield return WaitForSteadyState(movementType); Assert.That(grabInteractable.isSelected, Is.True); Assert.That(grabInteractable.attachTransform.position, Is.EqualTo(targetPosition).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(grabInteractable.attachTransform.rotation, Is.EqualTo(Quaternion.identity).Using(QuaternionEqualityComparer.Instance)); Assert.That(grabInteractable.transform.position, Is.EqualTo(targetPosition - attachOffset).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(grabInteractable.transform.rotation, Is.EqualTo(Quaternion.identity).Using(QuaternionEqualityComparer.Instance)); Assert.That(interactor.attachTransform.position, Is.EqualTo(targetPosition).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(interactor.attachTransform.rotation, Is.EqualTo(Quaternion.identity).Using(QuaternionEqualityComparer.Instance)); Assert.That(rigidbody.position, Is.EqualTo(targetPosition - attachOffset).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(rigidbody.rotation, Is.EqualTo(Quaternion.identity).Using(QuaternionEqualityComparer.Instance)); } } [UnityTest] public IEnumerator NonCenteredObjectRotatesToExpectedOrientation([ValueSource(nameof(s_MovementTypes))] XRBaseInteractable.MovementType movementType) { // Create a cube mesh with the pivot position at the bottom center // rather than the built-in Cube resource which has its center at the // center of the cube. var cubePrimitiveGO = GameObject.CreatePrimitive(PrimitiveType.Cube); var cubePrimitiveMesh = cubePrimitiveGO.GetComponent().sharedMesh; var centerOffset = new Vector3(0f, 0.5f, 0f); var offCenterMesh = new Mesh { vertices = cubePrimitiveMesh.vertices.Select(vertex => vertex + centerOffset).ToArray(), triangles = cubePrimitiveMesh.triangles.ToArray(), normals = cubePrimitiveMesh.normals.ToArray(), }; cubePrimitiveGO.SetActive(false); Object.Destroy(cubePrimitiveGO); // Create Grab Interactable at some arbitrary point var grabInteractableGO = new GameObject("Grab Interactable"); grabInteractableGO.transform.localPosition = new Vector3(1f, 2f, 3f); grabInteractableGO.transform.localRotation = Quaternion.LookRotation(Vector3.back, Vector3.up); var meshFilter = grabInteractableGO.AddComponent(); meshFilter.sharedMesh = offCenterMesh; grabInteractableGO.AddComponent(); var boxCollider = grabInteractableGO.AddComponent(); var rigidbody = grabInteractableGO.AddComponent(); rigidbody.useGravity = false; rigidbody.isKinematic = true; var grabInteractable = grabInteractableGO.AddComponent(); grabInteractable.movementType = movementType; TestUtilities.DisableDelayProperties(grabInteractable); // Set the Attach Transform to the back upper-right corner of the cube // to test an attach transform different from both the transform position and center. var grabInteractableAttach = new GameObject("Grab Interactable Attach").transform; var attachOffset = new Vector3(0.5f, 1f, -0.5f); grabInteractableAttach.SetParent(grabInteractable.transform); grabInteractableAttach.localPosition = attachOffset; var attachRotation = Quaternion.LookRotation(Vector3.left, Vector3.forward); grabInteractableAttach.rotation = attachRotation; grabInteractable.attachTransform = grabInteractableAttach; // Wait for physics update to ensure the Rigidbody is stable and center of mass has been calculated yield return new WaitForFixedUpdate(); yield return null; Assert.That(grabInteractable.isSelected, Is.False); // The Grab Interactable is rotated 180 degrees around the y-axis, // so the Attach Transform becomes the front upper-left corner of the cube from the perspective of the world axes, // so the position will end up at (0.5, 3, 3.5). var worldAttachOffset = new Vector3(-0.5f, 1f, 0.5f); Assert.That(grabInteractable.attachTransform.position, Is.EqualTo(new Vector3(1f, 2f, 3f) + worldAttachOffset).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(grabInteractable.transform.position, Is.EqualTo(new Vector3(1f, 2f, 3f)).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(boxCollider.center, Is.EqualTo(centerOffset).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(rigidbody.position, Is.EqualTo(new Vector3(1f, 2f, 3f)).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(rigidbody.centerOfMass, Is.EqualTo(centerOffset).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(rigidbody.worldCenterOfMass, Is.EqualTo(new Vector3(1f, 2f, 3f) + centerOffset).Using(Vector3ComparerWithEqualsOperator.Instance)); // Create Interactor at some arbitrary point away from the Interactable var interactor = TestUtilities.CreateMockInteractor(); var targetPosition = Vector3.zero; var targetRotation = Quaternion.identity; yield return null; Assert.That(grabInteractable.isSelected, Is.False); Assert.That(interactor.hasSelection, Is.False); Assert.That(interactor.interactablesSelected, Is.Empty); Assert.That(interactor.attachTransform, Is.Not.Null); Assert.That(interactor.attachTransform.position, Is.EqualTo(targetPosition).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(interactor.attachTransform.rotation, Is.EqualTo(targetRotation).Using(QuaternionEqualityComparer.Instance)); // Set valid so it will be selected next frame by the Interaction Manager interactor.validTargets.Add(grabInteractable); yield return WaitForSteadyState(movementType); Assert.That(grabInteractable.isSelected, Is.True); // When the Grab Interactable moves to align with the Interactor's Attach Transform at the origin, // the cube should end up with the transform pivot on the right face from the perspective of the world axes // to have the Attach Transform there pointing forward. var expectedRotation = Quaternion.LookRotation(Vector3.down, Vector3.left); Assert.That(grabInteractable.attachTransform.position, Is.EqualTo(targetPosition).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(grabInteractable.attachTransform.rotation, Is.EqualTo(targetRotation).Using(QuaternionEqualityComparer.Instance)); Assert.That(grabInteractable.transform.position, Is.EqualTo(new Vector3(1f, -0.5f, -0.5f)).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(grabInteractable.transform.rotation, Is.EqualTo(expectedRotation).Using(QuaternionEqualityComparer.Instance)); Assert.That(interactor.attachTransform.position, Is.EqualTo(targetPosition).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(interactor.attachTransform.rotation, Is.EqualTo(targetRotation).Using(QuaternionEqualityComparer.Instance)); Assert.That(rigidbody.position, Is.EqualTo(new Vector3(1f, -0.5f, -0.5f)).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(rigidbody.rotation, Is.EqualTo(expectedRotation).Using(QuaternionEqualityComparer.Instance)); } [UnityTest] public IEnumerator TrackRotationDisabledObjectMovesAndRotatesToExpectedPositionAndOrientation([ValueSource(nameof(s_MovementTypes))] XRBaseInteractable.MovementType movementType, [ValueSource(nameof(s_GrabTransformers))] Type grabTransformerType) { // Create a cube mesh with the pivot position at the bottom center // rather than the built-in Cube resource which has its center at the // center of the cube. var cubePrimitiveGO = GameObject.CreatePrimitive(PrimitiveType.Cube); var cubePrimitiveMesh = cubePrimitiveGO.GetComponent().sharedMesh; var centerOffset = new Vector3(0f, 0.5f, 0f); var offCenterMesh = new Mesh { vertices = cubePrimitiveMesh.vertices.Select(vertex => vertex + centerOffset).ToArray(), triangles = cubePrimitiveMesh.triangles.ToArray(), normals = cubePrimitiveMesh.normals.ToArray(), }; cubePrimitiveGO.SetActive(false); Object.Destroy(cubePrimitiveGO); // Create Grab Interactable at some arbitrary point var grabInteractableGO = new GameObject("Grab Interactable"); grabInteractableGO.transform.localPosition = new Vector3(1f, 2f, 3f); grabInteractableGO.transform.localRotation = Quaternion.identity; var meshFilter = grabInteractableGO.AddComponent(); meshFilter.sharedMesh = offCenterMesh; grabInteractableGO.AddComponent(); var boxCollider = grabInteractableGO.AddComponent(); var rigidbody = grabInteractableGO.AddComponent(); rigidbody.useGravity = false; rigidbody.isKinematic = true; var grabInteractable = grabInteractableGO.AddComponent(); grabInteractable.movementType = movementType; grabInteractable.addDefaultGrabTransformers = false; grabInteractableGO.AddComponent(grabTransformerType); TestUtilities.DisableDelayProperties(grabInteractable); // Set the Attach Transform to the back upper-right corner of the cube // to test an attach transform different from both the transform position and center. var grabInteractableAttach = new GameObject("Grab Interactable Attach").transform; var attachOffset = new Vector3(0.5f, 1f, -0.5f); var attachRotation = Quaternion.Euler(0f, 45f, 0f); grabInteractableAttach.SetParent(grabInteractable.transform); grabInteractableAttach.localPosition = attachOffset; grabInteractableAttach.localRotation = attachRotation; grabInteractable.attachTransform = grabInteractableAttach; // Disable track rotation grabInteractable.trackRotation = false; // Wait for physics update to ensure the Rigidbody is stable and center of mass has been calculated yield return new WaitForFixedUpdate(); yield return null; Assert.That(grabInteractable.isSelected, Is.False); Assert.That(grabInteractable.attachTransform.position, Is.EqualTo(new Vector3(1f, 2f, 3f) + attachOffset).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(grabInteractable.attachTransform.rotation, Is.EqualTo(attachRotation).Using(QuaternionEqualityComparer.Instance)); Assert.That(grabInteractable.transform.position, Is.EqualTo(new Vector3(1f, 2f, 3f)).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(grabInteractable.transform.rotation, Is.EqualTo(Quaternion.identity).Using(QuaternionEqualityComparer.Instance)); Assert.That(boxCollider.center, Is.EqualTo(centerOffset).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(rigidbody.position, Is.EqualTo(new Vector3(1f, 2f, 3f)).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(rigidbody.centerOfMass, Is.EqualTo(centerOffset).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(rigidbody.worldCenterOfMass, Is.EqualTo(new Vector3(1f, 2f, 3f) + centerOffset).Using(Vector3ComparerWithEqualsOperator.Instance)); // Create Interactor at some arbitrary point away from the Interactable var interactor = TestUtilities.CreateMockInteractor(); var targetPosition = Vector3.zero; var interactorAttachTransformRotation = Quaternion.identity; yield return null; Assert.That(grabInteractable.isSelected, Is.False); Assert.That(interactor.hasSelection, Is.False); Assert.That(interactor.interactablesSelected, Is.Empty); Assert.That(interactor.attachTransform, Is.Not.Null); Assert.That(interactor.attachTransform.position, Is.EqualTo(targetPosition).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(interactor.attachTransform.rotation, Is.EqualTo(interactorAttachTransformRotation).Using(QuaternionEqualityComparer.Instance)); // Set valid so it will be selected next frame by the Interaction Manager interactor.validTargets.Add(grabInteractable); yield return WaitForSteadyState(movementType); Assert.That(grabInteractable.isSelected, Is.True); Assert.That(grabInteractable.attachTransform.position, Is.EqualTo(targetPosition).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(grabInteractable.attachTransform.rotation, Is.EqualTo(attachRotation).Using(QuaternionEqualityComparer.Instance)); Assert.That(grabInteractable.transform.position, Is.EqualTo(targetPosition - attachOffset).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(grabInteractable.transform.rotation, Is.EqualTo(Quaternion.identity).Using(QuaternionEqualityComparer.Instance)); Assert.That(interactor.attachTransform.position, Is.EqualTo(targetPosition).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(interactor.attachTransform.rotation, Is.EqualTo(interactorAttachTransformRotation).Using(QuaternionEqualityComparer.Instance)); Assert.That(rigidbody.position, Is.EqualTo(targetPosition - attachOffset).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(rigidbody.rotation, Is.EqualTo(Quaternion.identity).Using(QuaternionEqualityComparer.Instance)); // Move and rotate the attach transform on the Interactor after being grabbed targetPosition = new Vector3(5f, 5f, 5f); interactorAttachTransformRotation = Quaternion.Euler(0f, 90f, 0f); interactor.attachTransform.position = targetPosition; interactor.attachTransform.rotation = interactorAttachTransformRotation; yield return WaitForSteadyState(movementType); // The expected object and its attached transform rotation remains unchanged since track rotation is disabled Assert.That(grabInteractable.isSelected, Is.True); Assert.That(grabInteractable.attachTransform.position, Is.EqualTo(targetPosition).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(grabInteractable.attachTransform.rotation, Is.EqualTo(attachRotation).Using(QuaternionEqualityComparer.Instance)); Assert.That(grabInteractable.transform.position, Is.EqualTo(targetPosition - attachOffset).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(grabInteractable.transform.rotation, Is.EqualTo(Quaternion.identity).Using(QuaternionEqualityComparer.Instance)); Assert.That(interactor.attachTransform.position, Is.EqualTo(targetPosition).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(interactor.attachTransform.rotation, Is.EqualTo(interactorAttachTransformRotation).Using(QuaternionEqualityComparer.Instance)); Assert.That(rigidbody.position, Is.EqualTo(targetPosition - attachOffset).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(rigidbody.rotation, Is.EqualTo(Quaternion.identity).Using(QuaternionEqualityComparer.Instance)); // The XR General Grab Transformer does not support the attach transform of the interactable being modified after // it is already grabbed (it only supports that with the interactor's attach transform changing). if (grabTransformerType != typeof(XRGeneralGrabTransformer)) { // Move the attach transform on the Interactable to the back lower-right corner of the cube attachOffset = new Vector3(0.5f, 0f, -0.5f); grabInteractable.attachTransform.localPosition = attachOffset; yield return WaitForSteadyState(movementType); Assert.That(grabInteractable.isSelected, Is.True); Assert.That(grabInteractable.attachTransform.position, Is.EqualTo(targetPosition).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(grabInteractable.attachTransform.rotation, Is.EqualTo(attachRotation).Using(QuaternionEqualityComparer.Instance)); Assert.That(grabInteractable.transform.position, Is.EqualTo(targetPosition - attachOffset).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(grabInteractable.transform.rotation, Is.EqualTo(Quaternion.identity).Using(QuaternionEqualityComparer.Instance)); Assert.That(interactor.attachTransform.position, Is.EqualTo(targetPosition).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(interactor.attachTransform.rotation, Is.EqualTo(interactorAttachTransformRotation).Using(QuaternionEqualityComparer.Instance)); Assert.That(rigidbody.position, Is.EqualTo(targetPosition - attachOffset).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(rigidbody.rotation, Is.EqualTo(Quaternion.identity).Using(QuaternionEqualityComparer.Instance)); } } [UnityTest] public IEnumerator DynamicAttachKeepsSamePose([ValueSource(nameof(s_MovementTypes))] XRBaseInteractable.MovementType movementType) { // Create Grab Interactable at some arbitrary point var grabInteractableGO = GameObject.CreatePrimitive(PrimitiveType.Cube); grabInteractableGO.name = "Grab Interactable"; grabInteractableGO.transform.localPosition = new Vector3(1f, 2f, 3f); grabInteractableGO.transform.localRotation = Quaternion.Euler(15f, 30f, 60f); var rigidbody = grabInteractableGO.AddComponent(); rigidbody.useGravity = false; rigidbody.isKinematic = true; var grabInteractable = grabInteractableGO.AddComponent(); grabInteractable.movementType = movementType; grabInteractable.useDynamicAttach = true; grabInteractable.matchAttachPosition = true; grabInteractable.matchAttachRotation = true; grabInteractable.snapToColliderVolume = false; TestUtilities.DisableDelayProperties(grabInteractable); // Wait for physics update to ensure the Rigidbody is stable and center of mass has been calculated yield return new WaitForFixedUpdate(); yield return null; Assert.That(grabInteractable.isSelected, Is.False); Assert.That(grabInteractable.transform.position, Is.EqualTo(new Vector3(1f, 2f, 3f)).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(grabInteractable.transform.rotation, Is.EqualTo(Quaternion.Euler(15f, 30f, 60f)).Using(QuaternionEqualityComparer.Instance)); // Create Interactor at some arbitrary point away from the Interactable var interactor = TestUtilities.CreateMockInteractor(); yield return null; Assert.That(grabInteractable.isSelected, Is.False); Assert.That(interactor.hasSelection, Is.False); Assert.That(interactor.interactablesSelected, Is.Empty); Assert.That(interactor.attachTransform, Is.Not.Null); Assert.That(interactor.attachTransform.position, Is.EqualTo(Vector3.zero).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(interactor.attachTransform.rotation, Is.EqualTo(Quaternion.identity).Using(QuaternionEqualityComparer.Instance)); // Set valid so it will be selected next frame by the Interaction Manager interactor.validTargets.Add(grabInteractable); yield return WaitForSteadyState(movementType); Assert.That(grabInteractable.isSelected, Is.True); Assert.That(grabInteractable.transform.position, Is.EqualTo(new Vector3(1f, 2f, 3f)).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(grabInteractable.transform.rotation, Is.EqualTo(Quaternion.Euler(15f, 30f, 60f)).Using(QuaternionEqualityComparer.Instance)); Assert.That(rigidbody.position, Is.EqualTo(new Vector3(1f, 2f, 3f)).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(rigidbody.rotation, Is.EqualTo(Quaternion.Euler(15f, 30f, 60f)).Using(QuaternionEqualityComparer.Instance)); } [UnityTest] public IEnumerator GrabTransformerMethodsInvoked([ValueSource(nameof(s_MockTransformerTypes))] Type mockTransformerType) { // This method will test a sequence of adds, selections, and removes to make sure the grab transformer methods // are called as expected after each change of state. This also tests some of the fallback rules. // Splitting each of these to their own different test would cause a huge amount of code duplication // since the setup needed for each depends a lot on the previous steps. // 1. Add -> OnLink // 2. Single Select -> OnGrab, OnGrabCountChanged; Process called on Single only // 3. No change -> Process called on Single only // 4. Single Select but Single can't process -> Process called on Multiple only as fallback // 5. Multiple Select -> OnGrabCountChanged; Process called on Multiple only // 6. No change -> Process called on Multiple only // 7. Multiple Select but Multiple can't process -> Process called on Single only as fallback // 8. Multiple Select but both can't process -> No method calls // 9. Remove -> OnUnlink var grabInteractable = TestUtilities.CreateGrabInteractable(); grabInteractable.ClearSingleGrabTransformers(); grabInteractable.ClearMultipleGrabTransformers(); grabInteractable.selectMode = InteractableSelectMode.Multiple; Assert.That(grabInteractable.singleGrabTransformersCount, Is.EqualTo(0)); Assert.That(grabInteractable.multipleGrabTransformersCount, Is.EqualTo(0)); MockGrabTransformer singleGrabTransformer; MockGrabTransformer multipleGrabTransformer; if (mockTransformerType == typeof(MockGrabTransformer)) { singleGrabTransformer = new MockGrabTransformer(); multipleGrabTransformer = new MockGrabTransformer(); } else if (mockTransformerType == typeof(MockDropTransformer)) { singleGrabTransformer = new MockDropTransformer(); multipleGrabTransformer = new MockDropTransformer(); } else { Assert.Fail($"Unhandled mock transformer type {mockTransformerType.Name}."); throw new NotImplementedException(); } Assert.That(singleGrabTransformer.canProcess, Is.True); Assert.That(multipleGrabTransformer.canProcess, Is.True); // 1. Add -> OnLink grabInteractable.AddSingleGrabTransformer(singleGrabTransformer); grabInteractable.AddMultipleGrabTransformer(multipleGrabTransformer); Assert.That(singleGrabTransformer.methodTraces, Is.EqualTo(new[] { MockGrabTransformer.MethodTrace.OnLink })); Assert.That(multipleGrabTransformer.methodTraces, Is.EqualTo(new[] { MockGrabTransformer.MethodTrace.OnLink })); ClearMethodTraces(); var interactor1 = TestUtilities.CreateMockInteractor(); var interactor2 = TestUtilities.CreateMockInteractor(); // 2. Single Select -> OnGrab, OnGrabCountChanged; Process called on Single only // Set valid so it will be selected next frame by the Interaction Manager interactor1.validTargets.Add(grabInteractable); yield return null; Assert.That(grabInteractable.interactorsSelecting, Is.EqualTo(new[] { interactor1 })); Assert.That(singleGrabTransformer.methodTraces, Is.EqualTo(new[] { MockGrabTransformer.MethodTrace.OnGrab, MockGrabTransformer.MethodTrace.OnGrabCountChanged, MockGrabTransformer.MethodTrace.ProcessDynamic, })); Assert.That(multipleGrabTransformer.methodTraces, Is.EqualTo(new[] { MockGrabTransformer.MethodTrace.OnGrab, MockGrabTransformer.MethodTrace.OnGrabCountChanged, })); ClearMethodTraces(); // 3. No change -> Process called on Single only yield return null; Assert.That(grabInteractable.interactorsSelecting, Is.EqualTo(new[] { interactor1 })); Assert.That(singleGrabTransformer.methodTraces, Is.EqualTo(new[] { MockGrabTransformer.MethodTrace.ProcessDynamic })); Assert.That(multipleGrabTransformer.methodTraces, Is.Empty); ClearMethodTraces(); // 4. Single Select but Single can't process -> Process called on Multiple only as fallback singleGrabTransformer.canProcess = false; multipleGrabTransformer.canProcess = true; yield return null; Assert.That(grabInteractable.interactorsSelecting, Is.EqualTo(new[] { interactor1 })); Assert.That(singleGrabTransformer.methodTraces, Is.Empty); Assert.That(multipleGrabTransformer.methodTraces, Is.EqualTo(new[] { MockGrabTransformer.MethodTrace.ProcessDynamic })); ClearMethodTraces(); // 5. Multiple Select -> OnGrabCountChanged; Process called on Multiple only singleGrabTransformer.canProcess = true; multipleGrabTransformer.canProcess = true; // Set valid so it will be selected next frame by the Interaction Manager interactor2.validTargets.Add(grabInteractable); yield return null; Assert.That(grabInteractable.interactorsSelecting, Is.EqualTo(new[] { interactor1, interactor2 })); Assert.That(singleGrabTransformer.methodTraces, Is.EqualTo(new[] { MockGrabTransformer.MethodTrace.OnGrabCountChanged, })); Assert.That(multipleGrabTransformer.methodTraces, Is.EqualTo(new[] { MockGrabTransformer.MethodTrace.OnGrabCountChanged, MockGrabTransformer.MethodTrace.ProcessDynamic, })); ClearMethodTraces(); // 6. No change -> Process called on Multiple only yield return null; Assert.That(grabInteractable.interactorsSelecting, Is.EqualTo(new[] { interactor1, interactor2 })); Assert.That(singleGrabTransformer.methodTraces, Is.Empty); Assert.That(multipleGrabTransformer.methodTraces, Is.EqualTo(new[] { MockGrabTransformer.MethodTrace.ProcessDynamic })); ClearMethodTraces(); // 7. Multiple Select but Multiple can't process -> Process called on Single only as fallback singleGrabTransformer.canProcess = true; multipleGrabTransformer.canProcess = false; yield return null; Assert.That(grabInteractable.interactorsSelecting, Is.EqualTo(new[] { interactor1, interactor2 })); Assert.That(singleGrabTransformer.methodTraces, Is.EqualTo(new[] { MockGrabTransformer.MethodTrace.ProcessDynamic })); Assert.That(multipleGrabTransformer.methodTraces, Is.Empty); ClearMethodTraces(); // 8. Multiple Select but both can't process -> No method calls singleGrabTransformer.canProcess = false; multipleGrabTransformer.canProcess = false; yield return null; Assert.That(grabInteractable.interactorsSelecting, Is.EqualTo(new[] { interactor1, interactor2 })); Assert.That(singleGrabTransformer.methodTraces, Is.Empty); Assert.That(multipleGrabTransformer.methodTraces, Is.Empty); // 9. Remove -> OnUnlink grabInteractable.RemoveSingleGrabTransformer(singleGrabTransformer); grabInteractable.RemoveMultipleGrabTransformer(multipleGrabTransformer); Assert.That(grabInteractable.interactorsSelecting, Is.EqualTo(new[] { interactor1, interactor2 })); Assert.That(singleGrabTransformer.methodTraces, Is.EqualTo(new[] { MockGrabTransformer.MethodTrace.OnUnlink })); Assert.That(multipleGrabTransformer.methodTraces, Is.EqualTo(new[] { MockGrabTransformer.MethodTrace.OnUnlink })); void ClearMethodTraces() { singleGrabTransformer.methodTraces.Clear(); multipleGrabTransformer.methodTraces.Clear(); } } [UnityTest] public IEnumerator DropTransformerMethodsInvoked([ValueSource(nameof(s_BooleanValues))] bool canProcessOnDrop) { // 1. Add -> OnLink // 2. Select -> OnGrab, OnGrabCountChanged // 3. No change -> Process called // 4. Deselect -> OnDrop (always), Process (if enabled) // 5. No change -> Process not called (since it only gets called a single time on drop) // 6. Remove -> OnUnlink var grabInteractable = TestUtilities.CreateGrabInteractable(); grabInteractable.ClearSingleGrabTransformers(); grabInteractable.ClearMultipleGrabTransformers(); grabInteractable.addDefaultGrabTransformers = false; Assert.That(grabInteractable.singleGrabTransformersCount, Is.EqualTo(0)); Assert.That(grabInteractable.multipleGrabTransformersCount, Is.EqualTo(0)); var dropTransformer = new MockDropTransformer { canProcessOnDrop = canProcessOnDrop, }; Assert.That(dropTransformer.canProcess, Is.True); // 1. Add -> OnLink grabInteractable.AddMultipleGrabTransformer(dropTransformer); Assert.That(dropTransformer.methodTraces, Is.EqualTo(new[] { MockGrabTransformer.MethodTrace.OnLink })); ClearMethodTraces(); // 2. Select -> OnGrab, OnGrabCountChanged var interactor = TestUtilities.CreateMockInteractor(); interactor.validTargets.Add(grabInteractable); yield return null; Assert.That(grabInteractable.interactorsSelecting, Is.EqualTo(new[] { interactor })); Assert.That(dropTransformer.methodTraces, Is.EqualTo(new[] { MockGrabTransformer.MethodTrace.OnGrab, MockGrabTransformer.MethodTrace.OnGrabCountChanged, MockGrabTransformer.MethodTrace.ProcessDynamic, })); ClearMethodTraces(); // 3. No change -> Process called yield return null; Assert.That(grabInteractable.interactorsSelecting, Is.EqualTo(new[] { interactor })); Assert.That(dropTransformer.methodTraces, Is.EqualTo(new[] { MockGrabTransformer.MethodTrace.ProcessDynamic })); ClearMethodTraces(); // 4. Deselect -> OnDrop (always), Process (if enabled) interactor.validTargets.Clear(); interactor.keepSelectedTargetValid = false; yield return null; Assert.That(grabInteractable.interactorsSelecting, Is.Empty); if (canProcessOnDrop) { Assert.That(dropTransformer.methodTraces, Is.EqualTo(new[] { MockGrabTransformer.MethodTrace.OnDrop, MockGrabTransformer.MethodTrace.ProcessDynamic, })); } else { Assert.That(dropTransformer.methodTraces, Is.EqualTo(new[] { MockGrabTransformer.MethodTrace.OnDrop })); } ClearMethodTraces(); // 5. No change -> Process not called (since it only gets called a single time on drop) yield return null; Assert.That(dropTransformer.methodTraces, Is.Empty); // 6. Remove -> OnUnlink grabInteractable.RemoveMultipleGrabTransformer(dropTransformer); Assert.That(dropTransformer.methodTraces, Is.EqualTo(new[] { MockGrabTransformer.MethodTrace.OnUnlink })); ClearMethodTraces(); void ClearMethodTraces() { dropTransformer.methodTraces.Clear(); } } [UnityTest] public IEnumerator GrabTransformerAddedAfterGrabHasGrabMethodsInvoked() { // Tests to make sure OnGrab is called when a new Grab Transformer is added when already selected var grabInteractable = TestUtilities.CreateGrabInteractable(); grabInteractable.ClearSingleGrabTransformers(); grabInteractable.ClearMultipleGrabTransformers(); Assert.That(grabInteractable.singleGrabTransformersCount, Is.EqualTo(0)); Assert.That(grabInteractable.multipleGrabTransformersCount, Is.EqualTo(0)); // The first will be added before the grab, the second will be added after the grab var grabTransformer1 = new MockGrabTransformer(); var grabTransformer2 = new MockGrabTransformer(); grabInteractable.AddSingleGrabTransformer(grabTransformer1); var grabTransformers = new List(); grabInteractable.GetSingleGrabTransformers(grabTransformers); Assert.That(grabTransformers, Is.EqualTo(new[] { grabTransformer1 })); Assert.That(grabTransformer1.methodTraces, Is.EqualTo(new[] { MockGrabTransformer.MethodTrace.OnLink })); grabTransformer1.methodTraces.Clear(); var interactor = TestUtilities.CreateMockInteractor(); // Set valid so it will be selected next frame by the Interaction Manager interactor.validTargets.Add(grabInteractable); yield return null; Assert.That(grabInteractable.isSelected, Is.True); Assert.That(grabInteractable.interactorsSelecting, Is.EqualTo(new[] { interactor })); Assert.That(grabTransformer1.methodTraces, Is.EqualTo(new[] { MockGrabTransformer.MethodTrace.OnGrab, MockGrabTransformer.MethodTrace.OnGrabCountChanged, MockGrabTransformer.MethodTrace.ProcessDynamic, })); grabTransformer1.methodTraces.Clear(); grabInteractable.AddSingleGrabTransformer(grabTransformer2); grabInteractable.GetSingleGrabTransformers(grabTransformers); Assert.That(grabTransformers, Is.EqualTo(new[] { grabTransformer1, grabTransformer2 })); Assert.That(grabTransformer2.methodTraces, Is.EqualTo(new[] { MockGrabTransformer.MethodTrace.OnLink, MockGrabTransformer.MethodTrace.OnGrab, })); grabTransformer2.methodTraces.Clear(); yield return null; Assert.That(grabTransformer1.methodTraces, Is.EqualTo(new[] { MockGrabTransformer.MethodTrace.ProcessDynamic })); Assert.That(grabTransformer2.methodTraces, Is.EqualTo(new[] { MockGrabTransformer.MethodTrace.OnGrabCountChanged, MockGrabTransformer.MethodTrace.ProcessDynamic, })); } [UnityTest] public IEnumerator GrabTransformerUnlinkedWhenInteractableDestroyed() { var grabInteractable = TestUtilities.CreateGrabInteractable(); grabInteractable.ClearSingleGrabTransformers(); grabInteractable.ClearMultipleGrabTransformers(); Assert.That(grabInteractable.singleGrabTransformersCount, Is.EqualTo(0)); Assert.That(grabInteractable.multipleGrabTransformersCount, Is.EqualTo(0)); var singleGrabTransformer = new MockGrabTransformer(); var multipleGrabTransformer = new MockGrabTransformer(); grabInteractable.AddSingleGrabTransformer(singleGrabTransformer); grabInteractable.AddMultipleGrabTransformer(multipleGrabTransformer); var grabTransformers = new List(); grabInteractable.GetSingleGrabTransformers(grabTransformers); Assert.That(grabTransformers, Is.EqualTo(new[] { singleGrabTransformer })); grabInteractable.GetMultipleGrabTransformers(grabTransformers); Assert.That(grabTransformers, Is.EqualTo(new[] { multipleGrabTransformer })); Assert.That(singleGrabTransformer.methodTraces, Is.EqualTo(new[] { MockGrabTransformer.MethodTrace.OnLink })); Assert.That(multipleGrabTransformer.methodTraces, Is.EqualTo(new[] { MockGrabTransformer.MethodTrace.OnLink })); singleGrabTransformer.methodTraces.Clear(); multipleGrabTransformer.methodTraces.Clear(); Object.Destroy(grabInteractable); yield return null; Assert.That(singleGrabTransformer.methodTraces, Is.EqualTo(new[] { MockGrabTransformer.MethodTrace.OnUnlink })); Assert.That(multipleGrabTransformer.methodTraces, Is.EqualTo(new[] { MockGrabTransformer.MethodTrace.OnUnlink })); grabInteractable.GetSingleGrabTransformers(grabTransformers); Assert.That(grabTransformers, Is.Empty); grabInteractable.GetMultipleGrabTransformers(grabTransformers); Assert.That(grabTransformers, Is.Empty); } [UnityTest] public IEnumerator AutomaticAddingOfDefaultGrabTransformersCanBeDisabled() { var grabInteractable = TestUtilities.CreateGrabInteractable(); grabInteractable.addDefaultGrabTransformers = false; yield return null; var grabTransformers = new List(); grabInteractable.GetSingleGrabTransformers(grabTransformers); Assert.That(grabTransformers, Is.Empty); grabInteractable.GetMultipleGrabTransformers(grabTransformers); Assert.That(grabTransformers, Is.Empty); Assert.That(grabInteractable.singleGrabTransformersCount, Is.EqualTo(0)); Assert.That(grabInteractable.multipleGrabTransformersCount, Is.EqualTo(0)); } [UnityTest] public IEnumerator AutomaticAddingOfDefaultMultipleGrabTransformerDoesNotReplaceExistingSingle() { // Test adding a Single Grab Transformer, and then adding the default XR General Transformer // (which registers as both a single and multiple transformer) // for an empty Multiple Grab Transformer list does not append it to the Single list. // Essentially, the XR Grab Interactable should override the registrationMode of the transform behavior. var grabInteractable = TestUtilities.CreateGrabInteractable(); grabInteractable.selectMode = InteractableSelectMode.Multiple; grabInteractable.addDefaultGrabTransformers = true; var singleGrabTransformer = new MockGrabTransformer(); grabInteractable.AddSingleGrabTransformer(singleGrabTransformer); var grabTransformers = new List(); grabInteractable.GetSingleGrabTransformers(grabTransformers); Assert.That(grabTransformers, Is.EqualTo(new[] { singleGrabTransformer })); grabInteractable.GetMultipleGrabTransformers(grabTransformers); Assert.That(grabTransformers, Is.Empty); Assert.That(grabInteractable.singleGrabTransformersCount, Is.EqualTo(1)); Assert.That(grabInteractable.multipleGrabTransformersCount, Is.EqualTo(0)); var interactor1 = TestUtilities.CreateMockInteractor(); var interactor2 = TestUtilities.CreateMockInteractor(); interactor1.validTargets.Add(grabInteractable); interactor2.validTargets.Add(grabInteractable); yield return null; Assert.That(grabInteractable.isSelected, Is.True); Assert.That(interactor1.IsSelecting(grabInteractable), Is.True); Assert.That(interactor2.IsSelecting(grabInteractable), Is.True); grabInteractable.GetSingleGrabTransformers(grabTransformers); Assert.That(grabTransformers, Is.EqualTo(new[] { singleGrabTransformer })); grabInteractable.GetMultipleGrabTransformers(grabTransformers); Assert.That(grabTransformers, Has.Count.EqualTo(1)); Assert.That(grabTransformers[0], Is.TypeOf()); Assert.That(grabInteractable.singleGrabTransformersCount, Is.EqualTo(1)); Assert.That(grabInteractable.multipleGrabTransformersCount, Is.EqualTo(1)); } [UnityTest] public IEnumerator AutomaticAddingOfDefaultSingleGrabTransformerDoesNotReplaceExistingMultiple() { // Test adding a Multiple Grab Transformer, and then adding the default XR General Transformer // (which registers as both a single and multiple transformer) // for an empty Single Grab Transformer list does not append it to the Multiple list. // Essentially, the XR Grab Interactable should override the registrationMode of the transform behavior. var grabInteractable = TestUtilities.CreateGrabInteractable(); grabInteractable.selectMode = InteractableSelectMode.Multiple; grabInteractable.addDefaultGrabTransformers = true; var multipleGrabTransformer = new MockGrabTransformer(); grabInteractable.AddMultipleGrabTransformer(multipleGrabTransformer); var grabTransformers = new List(); grabInteractable.GetSingleGrabTransformers(grabTransformers); Assert.That(grabTransformers, Is.Empty); grabInteractable.GetMultipleGrabTransformers(grabTransformers); Assert.That(grabTransformers, Is.EqualTo(new[] { multipleGrabTransformer })); Assert.That(grabInteractable.singleGrabTransformersCount, Is.EqualTo(0)); Assert.That(grabInteractable.multipleGrabTransformersCount, Is.EqualTo(1)); var interactor1 = TestUtilities.CreateMockInteractor(); var interactor2 = TestUtilities.CreateMockInteractor(); interactor1.validTargets.Add(grabInteractable); interactor2.validTargets.Add(grabInteractable); yield return null; Assert.That(grabInteractable.isSelected, Is.True); Assert.That(interactor1.IsSelecting(grabInteractable), Is.True); Assert.That(interactor2.IsSelecting(grabInteractable), Is.True); grabInteractable.GetSingleGrabTransformers(grabTransformers); Assert.That(grabTransformers, Has.Count.EqualTo(1)); Assert.That(grabTransformers[0], Is.TypeOf()); grabInteractable.GetMultipleGrabTransformers(grabTransformers); Assert.That(grabTransformers, Is.EqualTo(new[] { multipleGrabTransformer })); Assert.That(grabInteractable.singleGrabTransformersCount, Is.EqualTo(1)); Assert.That(grabInteractable.multipleGrabTransformersCount, Is.EqualTo(1)); } [UnityTest] public IEnumerator XRBaseGrabTransformersAutomaticallyLink() { var grabInteractable = TestUtilities.CreateGrabInteractable(); grabInteractable.addDefaultGrabTransformers = false; yield return null; var grabTransformers = new List(); grabInteractable.GetSingleGrabTransformers(grabTransformers); Assert.That(grabTransformers, Is.Empty); grabInteractable.GetMultipleGrabTransformers(grabTransformers); Assert.That(grabTransformers, Is.Empty); Assert.That(typeof(XRSingleGrabFreeTransformer).IsSubclassOf(typeof(XRBaseGrabTransformer)), Is.True); var singleGrabTransformer = grabInteractable.gameObject.AddComponent(); Assert.That(typeof(XRDualGrabFreeTransformer).IsSubclassOf(typeof(XRBaseGrabTransformer)), Is.True); var multipleGrabTransformer = grabInteractable.gameObject.AddComponent(); yield return null; grabInteractable.GetSingleGrabTransformers(grabTransformers); Assert.That(grabTransformers, Is.EqualTo(new[] { singleGrabTransformer })); grabInteractable.GetMultipleGrabTransformers(grabTransformers); Assert.That(grabTransformers, Is.EqualTo(new[] { multipleGrabTransformer })); Object.Destroy(singleGrabTransformer); Object.Destroy(multipleGrabTransformer); yield return null; grabInteractable.GetSingleGrabTransformers(grabTransformers); Assert.That(grabTransformers, Is.Empty); grabInteractable.GetMultipleGrabTransformers(grabTransformers); Assert.That(grabTransformers, Is.Empty); } [UnityTest] public IEnumerator GrabTransformerCanSetTargetPose([ValueSource(nameof(s_MovementTypes))] XRBaseInteractable.MovementType movementType) { var grabInteractable = TestUtilities.CreateGrabInteractable(); grabInteractable.ClearSingleGrabTransformers(); grabInteractable.ClearMultipleGrabTransformers(); grabInteractable.movementType = movementType; TestUtilities.DisableDelayProperties(grabInteractable); grabInteractable.transform.SetPositionAndRotation(new Vector3(1f, 2f, 3f), Quaternion.Euler(15f, 30f, 60f)); Assert.That(grabInteractable.singleGrabTransformersCount, Is.EqualTo(0)); Assert.That(grabInteractable.multipleGrabTransformersCount, Is.EqualTo(0)); var grabTransformer = new MockGrabTransformer(); grabInteractable.AddSingleGrabTransformer(grabTransformer); var grabTransformers = new List(); grabInteractable.GetSingleGrabTransformers(grabTransformers); Assert.That(grabTransformers, Is.EqualTo(new[] { grabTransformer })); yield return WaitForSteadyState(movementType); grabInteractable.GetSingleGrabTransformers(grabTransformers); Assert.That(grabTransformers, Is.EqualTo(new[] { grabTransformer })); Assert.That(grabInteractable.isSelected, Is.False); Assert.That(grabInteractable.transform.position, Is.EqualTo(new Vector3(1f, 2f, 3f)).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(grabInteractable.transform.rotation, Is.EqualTo(Quaternion.Euler(15f, 30f, 60f)).Using(QuaternionEqualityComparer.Instance)); var interactor = TestUtilities.CreateMockInteractor(); // Set valid so it will be selected next frame by the Interaction Manager interactor.validTargets.Add(grabInteractable); yield return WaitForSteadyState(movementType); // Keeps the same pose if Process does not change the values Assert.That(grabInteractable.isSelected, Is.True); Assert.That(grabInteractable.transform.position, Is.EqualTo(new Vector3(1f, 2f, 3f)).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(grabInteractable.transform.rotation, Is.EqualTo(Quaternion.Euler(15f, 30f, 60f)).Using(QuaternionEqualityComparer.Instance)); grabTransformer.targetPoseValue = new Pose(new Vector3(4f, 5f, 6f), Quaternion.Euler(80f, 20f, -100f)); yield return WaitForSteadyState(movementType); Assert.That(grabInteractable.transform.position, Is.EqualTo(grabTransformer.targetPoseValue.Value.position).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(grabInteractable.transform.rotation, Is.EqualTo(grabTransformer.targetPoseValue.Value.rotation).Using(QuaternionEqualityComparer.Instance)); } [UnityTest] public IEnumerator GrabTransformerCanSetScale([ValueSource(nameof(s_MovementTypes))] XRBaseInteractable.MovementType movementType) { var grabInteractable = TestUtilities.CreateGrabInteractable(); grabInteractable.ClearSingleGrabTransformers(); grabInteractable.ClearMultipleGrabTransformers(); grabInteractable.movementType = movementType; TestUtilities.DisableDelayProperties(grabInteractable); grabInteractable.transform.localScale = Vector3.one; Assert.That(grabInteractable.singleGrabTransformersCount, Is.EqualTo(0)); Assert.That(grabInteractable.multipleGrabTransformersCount, Is.EqualTo(0)); var grabTransformer = new MockGrabTransformer(); grabInteractable.AddSingleGrabTransformer(grabTransformer); var grabTransformers = new List(); grabInteractable.GetSingleGrabTransformers(grabTransformers); Assert.That(grabTransformers, Is.EqualTo(new[] { grabTransformer })); yield return WaitForSteadyState(movementType); grabInteractable.GetSingleGrabTransformers(grabTransformers); Assert.That(grabTransformers, Is.EqualTo(new[] { grabTransformer })); Assert.That(grabInteractable.isSelected, Is.False); Assert.That(grabInteractable.transform.localScale, Is.EqualTo(Vector3.one).Using(Vector3ComparerWithEqualsOperator.Instance)); var interactor = TestUtilities.CreateMockInteractor(); // Set valid so it will be selected next frame by the Interaction Manager interactor.validTargets.Add(grabInteractable); yield return WaitForSteadyState(movementType); // Keeps the same scale if Process does not change the values Assert.That(grabInteractable.isSelected, Is.True); Assert.That(grabInteractable.transform.localScale, Is.EqualTo(Vector3.one).Using(Vector3ComparerWithEqualsOperator.Instance)); grabTransformer.localScaleValue = new Vector3(0.5f, 0.25f, 0.75f); yield return WaitForSteadyState(movementType); Assert.That(grabInteractable.transform.localScale, Is.EqualTo(grabTransformer.localScaleValue.Value).Using(Vector3ComparerWithEqualsOperator.Instance)); } [TestCase(false, false)] [TestCase(false, true)] [TestCase(true, false)] [TestCase(true, true)] public void InitializesDynamicAttachTransformToInteractorAttachPose(bool matchAttachPosition, bool matchAttachRotation) { var interactor = TestUtilities.CreateMockInteractor(); interactor.transform.localPosition = new Vector3(1f, 2f, 3f); interactor.transform.localRotation = Quaternion.Euler(15f, 30f, 60f); var grabInteractableGO = GameObject.CreatePrimitive(PrimitiveType.Cube); grabInteractableGO.transform.SetWorldPose(Pose.identity); var grabInteractable = grabInteractableGO.AddComponent(); grabInteractable.useDynamicAttach = true; grabInteractable.matchAttachPosition = matchAttachPosition; grabInteractable.matchAttachRotation = matchAttachRotation; grabInteractable.snapToColliderVolume = false; var dynamicAttachTransform = new GameObject("Dynamic Attach Transform").transform; dynamicAttachTransform.SetLocalPose(Pose.identity); dynamicAttachTransform.SetParent(grabInteractable.transform, false); grabInteractable.InitializeDynamicAttachPose(interactor, dynamicAttachTransform); var expectedPosition = matchAttachPosition ? new Vector3(1f, 2f, 3f) : Vector3.zero; var expectedRotation = matchAttachRotation ? Quaternion.Euler(15f, 30f, 60f) : Quaternion.identity; Assert.That(dynamicAttachTransform.position, Is.EqualTo(expectedPosition).Using(Vector3ComparerWithEqualsOperator.Instance)); Assert.That(dynamicAttachTransform.rotation, Is.EqualTo(expectedRotation).Using(QuaternionEqualityComparer.Instance)); } [Test] public void MatchAttachPropertiesNotOverriddenByBaseInteractor() { var interactor = TestUtilities.CreateMockInteractor(); var grabInteractableGO = GameObject.CreatePrimitive(PrimitiveType.Cube); var grabInteractable = grabInteractableGO.AddComponent(); grabInteractable.useDynamicAttach = true; grabInteractable.matchAttachPosition = true; grabInteractable.matchAttachRotation = true; grabInteractable.snapToColliderVolume = true; Assert.That(grabInteractable.ShouldMatchAttachPosition(interactor), Is.True); Assert.That(grabInteractable.ShouldMatchAttachRotation(interactor), Is.True); Assert.That(grabInteractable.ShouldSnapToColliderVolume(interactor), Is.True); grabInteractable.matchAttachPosition = false; grabInteractable.matchAttachRotation = false; grabInteractable.snapToColliderVolume = false; Assert.That(grabInteractable.ShouldMatchAttachPosition(interactor), Is.False); Assert.That(grabInteractable.ShouldMatchAttachRotation(interactor), Is.False); Assert.That(grabInteractable.ShouldSnapToColliderVolume(interactor), Is.False); } [Test] public void MatchAttachPropertiesOverriddenBySocketInteractor() { var interactor = TestUtilities.CreateSocketInteractor(); var grabInteractableGO = GameObject.CreatePrimitive(PrimitiveType.Cube); var grabInteractable = grabInteractableGO.AddComponent(); grabInteractable.useDynamicAttach = true; grabInteractable.matchAttachPosition = true; grabInteractable.matchAttachRotation = true; grabInteractable.snapToColliderVolume = true; Assert.That(grabInteractable.ShouldMatchAttachPosition(interactor), Is.False); Assert.That(grabInteractable.ShouldMatchAttachRotation(interactor), Is.False); Assert.That(grabInteractable.ShouldSnapToColliderVolume(interactor), Is.True); grabInteractable.matchAttachPosition = false; grabInteractable.matchAttachRotation = false; grabInteractable.snapToColliderVolume = false; Assert.That(grabInteractable.ShouldMatchAttachPosition(interactor), Is.False); Assert.That(grabInteractable.ShouldMatchAttachRotation(interactor), Is.False); Assert.That(grabInteractable.ShouldSnapToColliderVolume(interactor), Is.False); } [TestCase(false)] [TestCase(true)] public void MatchAttachPropertiesOverriddenByRayInteractor(bool useForceGrab) { var interactor = TestUtilities.CreateRayInteractor(); interactor.useForceGrab = useForceGrab; var grabInteractableGO = GameObject.CreatePrimitive(PrimitiveType.Cube); var grabInteractable = grabInteractableGO.AddComponent(); grabInteractable.useDynamicAttach = true; grabInteractable.matchAttachPosition = true; grabInteractable.matchAttachRotation = true; grabInteractable.snapToColliderVolume = true; Assert.That(grabInteractable.ShouldMatchAttachPosition(interactor), Is.Not.EqualTo(useForceGrab)); Assert.That(grabInteractable.ShouldMatchAttachRotation(interactor), Is.True); Assert.That(grabInteractable.ShouldSnapToColliderVolume(interactor), Is.True); grabInteractable.matchAttachPosition = false; grabInteractable.matchAttachRotation = false; grabInteractable.snapToColliderVolume = false; Assert.That(grabInteractable.ShouldMatchAttachPosition(interactor), Is.False); Assert.That(grabInteractable.ShouldMatchAttachRotation(interactor), Is.False); Assert.That(grabInteractable.ShouldSnapToColliderVolume(interactor), Is.False); } [UnityTest] public IEnumerator GrabbedObjectCannotCollideWithPlayer() { const float characterControllerRadius = 0.5f; const float noOverlapDistance = 2f; TestUtilities.CreateInteractionManager(); var characterControllerGO = new GameObject("Character Controller"); var characterController = characterControllerGO.AddComponent(); characterController.radius = characterControllerRadius; var interactor = TestUtilities.CreateMockInteractor(); interactor.transform.SetParent(characterControllerGO.transform); // Place object far enough from player so that it won't collide, and ensure object will jump directly to interactor on grab var grabInteractableGO = GameObject.CreatePrimitive(PrimitiveType.Cube); grabInteractableGO.name = "Grab Interactable"; var initialInteractablePosition = new Vector3(0f, 0f, characterControllerRadius + noOverlapDistance); var grabInteractableTrans = grabInteractableGO.transform; grabInteractableTrans.localPosition = initialInteractablePosition; grabInteractableTrans.localRotation = Quaternion.identity; var interactableCollider = grabInteractableGO.GetComponent(); var rigidbody = grabInteractableGO.AddComponent(); rigidbody.useGravity = false; var grabInteractable = grabInteractableGO.AddComponent(); grabInteractable.useDynamicAttach = false; grabInteractable.attachEaseInTime = 0f; grabInteractable.smoothPosition = false; var collisionCount = 0; var collisionChecker = grabInteractableGO.AddComponent(); collisionChecker.onCollisionEntered += collision => { if (collision.collider == characterController) collisionCount++; }; yield return new WaitForFixedUpdate(); yield return null; Assert.That(grabInteractable.isSelected, Is.False); Assert.That(collisionCount, Is.EqualTo(0)); interactor.validTargets.Add(grabInteractable); yield return new WaitForFixedUpdate(); yield return null; Assert.That(grabInteractable.isSelected, Is.True); // Wait for physics update after object is grabbed yield return new WaitForFixedUpdate(); yield return null; Assert.That(interactableCollider.bounds.Intersects(characterController.bounds), Is.True); Assert.That(collisionCount, Is.EqualTo(0)); interactor.validTargets.Clear(); interactor.keepSelectedTargetValid = false; yield return new WaitForFixedUpdate(); yield return null; Assert.That(grabInteractable.isSelected, Is.False); // Wait for physics update after dropping object. Collision should then only be able to occur after the // colliders have stopped overlapping. yield return new WaitForFixedUpdate(); yield return null; Assert.That(collisionCount, Is.EqualTo(0)); // Move player back so colliders stop overlapping var moveCollisionFlags = characterController.Move(Vector3.back * noOverlapDistance); Assert.That(interactableCollider.bounds.Intersects(characterController.bounds), Is.False); Assert.That(moveCollisionFlags, Is.EqualTo(CollisionFlags.None)); // Wait for ignore collision state to reset, then try to move player forward enough to trigger collision yield return new WaitForFixedUpdate(); yield return null; Assert.That(characterController.Move(grabInteractableTrans.position - characterControllerGO.transform.position), Is.Not.EqualTo(CollisionFlags.None)); // Move everything back to where it was, then explicitly ignore collision before grabbing again, // to ensure collision is still ignored after grabInteractableTrans.localPosition = initialInteractablePosition; characterControllerGO.transform.localPosition = Vector3.zero; yield return new WaitForFixedUpdate(); yield return null; Physics.IgnoreCollision(interactableCollider, characterController); interactor.validTargets.Add(grabInteractable); yield return new WaitForFixedUpdate(); yield return null; Assert.That(grabInteractable.isSelected, Is.True); // Wait for physics update after object is grabbed yield return new WaitForFixedUpdate(); yield return null; Assert.That(interactableCollider.bounds.Intersects(characterController.bounds), Is.True); Assert.That(collisionCount, Is.EqualTo(0)); interactor.validTargets.Clear(); yield return new WaitForFixedUpdate(); yield return null; Assert.That(grabInteractable.isSelected, Is.False); // Wait for physics update after dropping object yield return new WaitForFixedUpdate(); yield return null; // Move player back and forward again. This time collision should not occur since the object was already set // to ignore collision. moveCollisionFlags = characterController.Move(Vector3.back * noOverlapDistance); Assert.That(interactableCollider.bounds.Intersects(characterController.bounds), Is.False); Assert.That(moveCollisionFlags, Is.EqualTo(CollisionFlags.None)); yield return new WaitForFixedUpdate(); yield return null; moveCollisionFlags = characterController.Move(Vector3.forward * noOverlapDistance); Assert.That(interactableCollider.bounds.Intersects(characterController.bounds), Is.True); Assert.That(moveCollisionFlags, Is.EqualTo(CollisionFlags.None)); Assert.That(collisionCount, Is.EqualTo(0)); } [UnityTest] public IEnumerator GrabbedObjectSelectedByAnotherInteractorCannotCollideWithPlayer() { const float characterControllerRadius = 0.5f; TestUtilities.CreateInteractionManager(); var characterControllerGO = new GameObject("Character Controller"); var characterController = characterControllerGO.AddComponent(); characterController.radius = characterControllerRadius; var playerInteractor = TestUtilities.CreateMockInteractor(); playerInteractor.transform.SetParent(characterControllerGO.transform); // Place external interactor far enough from player so that its grabbed interactable won't collide with the player var initialInteractablePosition = new Vector3(0f, 0f, characterControllerRadius + 2f); var externalInteractor = TestUtilities.CreateMockInteractor(); var externalInteractorTrans = externalInteractor.transform; externalInteractorTrans.localPosition = initialInteractablePosition; externalInteractorTrans.localRotation = Quaternion.identity; // Ensure object will jump directly to interactor on grab var grabInteractableGO = GameObject.CreatePrimitive(PrimitiveType.Cube); grabInteractableGO.name = "Grab Interactable"; var interactableCollider = grabInteractableGO.GetComponent(); var rigidbody = grabInteractableGO.AddComponent(); rigidbody.useGravity = false; var grabInteractable = grabInteractableGO.AddComponent(); grabInteractable.useDynamicAttach = false; grabInteractable.attachEaseInTime = 0f; grabInteractable.smoothPosition = false; grabInteractable.selectMode = InteractableSelectMode.Multiple; externalInteractor.validTargets.Add(grabInteractable); yield return new WaitForFixedUpdate(); yield return null; Assert.That(grabInteractable.isSelected, Is.True); Assert.That(externalInteractor.IsSelecting(grabInteractable), Is.True); Assert.That(playerInteractor.IsSelecting(grabInteractable), Is.False); // Wait for physics update after object is grabbed by external interactor, to ensure it is placed away from player yield return new WaitForFixedUpdate(); yield return null; Assert.That(interactableCollider.bounds.Intersects(characterController.bounds), Is.False); // Now have the player grab the object playerInteractor.validTargets.Add(grabInteractable); yield return new WaitForFixedUpdate(); yield return null; Assert.That(grabInteractable.isSelected, Is.True); Assert.That(externalInteractor.IsSelecting(grabInteractable), Is.True); Assert.That(playerInteractor.IsSelecting(grabInteractable), Is.True); // Move player to where the interactable is. The interactable should be intersecting the player but collision should not occur. var playerToInteractableDelta = grabInteractable.transform.position - characterControllerGO.transform.position; var moveCollisionFlags = characterController.Move(playerToInteractableDelta); Assert.That(interactableCollider.bounds.Intersects(characterController.bounds), Is.True); Assert.That(moveCollisionFlags, Is.EqualTo(CollisionFlags.None)); yield return new WaitForFixedUpdate(); yield return null; // Now move the player back to the origin and then deselect the interactable. // Then attempting to move the player to where the interactable is should result in a collision. characterController.Move(-playerToInteractableDelta); playerInteractor.validTargets.Clear(); playerInteractor.keepSelectedTargetValid = false; yield return new WaitForFixedUpdate(); yield return null; Assert.That(grabInteractable.isSelected, Is.True); Assert.That(externalInteractor.IsSelecting(grabInteractable), Is.True); Assert.That(playerInteractor.IsSelecting(grabInteractable), Is.False); Assert.That(interactableCollider.bounds.Intersects(characterController.bounds), Is.False); yield return new WaitForFixedUpdate(); Assert.That(characterController.Move(playerToInteractableDelta), Is.Not.EqualTo(CollisionFlags.None)); } class PublicAccessGrabInteractable : XRGrabInteractable { public new bool ShouldMatchAttachPosition(IXRSelectInteractor interactor) => base.ShouldMatchAttachPosition(interactor); public new bool ShouldMatchAttachRotation(IXRSelectInteractor interactor) => base.ShouldMatchAttachRotation(interactor); public new bool ShouldSnapToColliderVolume(IXRSelectInteractor interactor) => base.ShouldSnapToColliderVolume(interactor); public new void InitializeDynamicAttachPose(IXRSelectInteractor interactor, Transform dynamicAttachTransform) => base.InitializeDynamicAttachPose(interactor, dynamicAttachTransform); } class CollisionChecker : MonoBehaviour { public event Action onCollisionEntered; void OnCollisionEnter(Collision collision) { onCollisionEntered?.Invoke(collision); } } } }