using System; using System.Collections.Generic; using NUnit.Framework; using Unity.XR.CoreUtils; using UnityEngine.EventSystems; using UnityEngine.SceneManagement; using UnityEngine.UI; using UnityEngine.XR.Interaction.Toolkit.Attachment; using UnityEngine.XR.Interaction.Toolkit.Filtering; using UnityEngine.XR.Interaction.Toolkit.Interactables; using UnityEngine.XR.Interaction.Toolkit.Interactors; using UnityEngine.XR.Interaction.Toolkit.Interactors.Visuals; using UnityEngine.XR.Interaction.Toolkit.Locomotion; using UnityEngine.XR.Interaction.Toolkit.Locomotion.Teleportation; using UnityEngine.XR.Interaction.Toolkit.Transformers; using UnityEngine.XR.Interaction.Toolkit.UI; namespace UnityEngine.XR.Interaction.Toolkit.Tests { static class TestUtilities { internal static void DestroyAllSceneObjects() { for (var index = 0; index < SceneManager.sceneCount; ++index) { var scene = SceneManager.GetSceneAt(index); foreach (var go in scene.GetRootGameObjects()) { if (go.name.Contains("tests runner")) continue; Object.DestroyImmediate(go); } } } internal static void DisableDelayProperties(XRGrabInteractable grabInteractable) { grabInteractable.velocityDamping = 1f; grabInteractable.velocityScale = 1f; grabInteractable.angularVelocityDamping = 1f; grabInteractable.angularVelocityScale = 1f; grabInteractable.attachEaseInTime = 0f; var rigidbody = grabInteractable.GetComponent(); rigidbody.maxAngularVelocity = float.PositiveInfinity; } internal static BoxCollider CreateGOBoxCollider(GameObject go, bool isTrigger = true) { BoxCollider collider = go.AddComponent(); collider.size = new Vector3(2.0f, 2.0f, 2.0f); collider.isTrigger = isTrigger; return collider; } internal static SphereCollider CreateGOSphereCollider(GameObject go, bool isTrigger = true) { SphereCollider collider = go.AddComponent(); collider.radius = 1.0f; collider.isTrigger = isTrigger; return collider; } internal static XRInteractionManager CreateInteractionManager() { GameObject managerGO = new GameObject("Interaction Manager"); XRInteractionManager manager = managerGO.AddComponent(); return manager; } internal static NearFarInteractor CreateNearFarInteractor() { GameObject interactorGO = new GameObject("Near-Far Interactor"); NearFarInteractor interactor = interactorGO.AddComponent(); var attachController = interactorGO.GetComponent(); // Distance based velocity scaling is disabled because it is not supported without an XR Origin attachController.useDistanceBasedVelocityScaling = false; return interactor; } internal static NearFarInteractor CreateNearFarInteractorWithXROrigin(out Camera camera) { var xrOrigin = CreateXROrigin(); camera = xrOrigin.Camera; var leftTesthand = new GameObject("Left Hand Test Controller"); leftTesthand.transform.SetParent(xrOrigin.CameraFloorOffsetObject.transform, false); GameObject interactorGO = new GameObject("Near-Far Interactor"); interactorGO.transform.SetParent(leftTesthand.transform, false); NearFarInteractor interactor = interactorGO.AddComponent(); interactor.handedness = InteractorHandedness.Left; return interactor; } internal static XRDirectInteractor CreateDirectInteractor() { GameObject interactorGO = new GameObject("Direct Interactor"); CreateGOSphereCollider(interactorGO); XRDirectInteractor interactor = interactorGO.AddComponent(); return interactor; } internal static XRPokeInteractor CreatePokeInteractor() { GameObject interactorGO = new GameObject("Poke Interactor"); XRPokeInteractor interactor = interactorGO.AddComponent(); return interactor; } internal static XROrigin CreateXROrigin() { var xrOriginGO = new GameObject("XR Origin"); xrOriginGO.SetActive(false); var xrOrigin = xrOriginGO.AddComponent(); xrOrigin.Origin = xrOriginGO; // Add camera offset var cameraOffsetGO = new GameObject("CameraOffset"); cameraOffsetGO.transform.SetParent(xrOrigin.transform, false); xrOrigin.CameraFloorOffsetObject = cameraOffsetGO; xrOrigin.transform.SetPositionAndRotation(Vector3.zero, Quaternion.identity); // Add camera var cameraGO = new GameObject("Camera"); var camera = cameraGO.AddComponent(); cameraGO.transform.SetParent(cameraOffsetGO.transform, false); xrOrigin.Camera = camera; xrOriginGO.SetActive(true); return xrOrigin; } internal static TeleportationArea CreateTeleportAreaPlane() { var plane = GameObject.CreatePrimitive(PrimitiveType.Plane); plane.name = "plane"; plane.transform.localScale = new Vector3(10f, 1f, 10f); return plane.AddComponent(); } internal static TeleportationAnchor CreateTeleportAnchorPlane() { var plane = GameObject.CreatePrimitive(PrimitiveType.Plane); plane.name = "plane"; return plane.AddComponent(); } internal static XRRayInteractor CreateRayInteractor() { GameObject interactorGO = new GameObject("Ray Interactor"); XRRayInteractor interactor = interactorGO.AddComponent(); interactor.enableUIInteraction = false; interactorGO.AddComponent(); return interactor; } internal static XRGazeInteractor CreateGazeInteractor() { GameObject interactorGO = new GameObject("Gaze Interactor"); XRGazeInteractor interactor = interactorGO.AddComponent(); interactor.enableUIInteraction = false; return interactor; } internal static XRSocketInteractor CreateSocketInteractor() { GameObject interactorGO = new GameObject("Socket Interactor"); CreateGOSphereCollider(interactorGO); XRSocketInteractor interactor = interactorGO.AddComponent(); return interactor; } internal static MockInteractor CreateMockInteractor() { var interactorGO = new GameObject("Mock Interactor"); interactorGO.transform.localPosition = Vector3.zero; interactorGO.transform.localRotation = Quaternion.identity; var interactor = interactorGO.AddComponent(); return interactor; } internal static MockClassInteractable CreateMockClassInteractable() { var interactableGO = new GameObject("Mock Interactable"); interactableGO.transform.localPosition = Vector3.zero; interactableGO.transform.localRotation = Quaternion.identity; var interactable = new MockClassInteractable(interactableGO.transform); return interactable; } internal static XRGrabInteractable CreateGrabInteractable() { GameObject interactableGO = new GameObject("Grab Interactable"); CreateGOSphereCollider(interactableGO, false); XRGrabInteractable interactable = interactableGO.AddComponent(); interactable.throwOnDetach = false; var rigidBody = interactableGO.GetComponent(); rigidBody.useGravity = false; rigidBody.isKinematic = true; return interactable; } internal static XRSimpleInteractable CreateSimpleInteractable() { GameObject interactableGO = new GameObject("Simple Interactable"); CreateGOSphereCollider(interactableGO, false); XRSimpleInteractable interactable = interactableGO.AddComponent(); Rigidbody rigidBody = interactableGO.AddComponent(); rigidBody.useGravity = false; rigidBody.isKinematic = true; return interactable; } internal static XRSimpleInteractable CreateTriggerInteractable() { GameObject interactableGO = new GameObject("Trigger Interactable"); var collider = CreateGOSphereCollider(interactableGO, false); XRSimpleInteractable interactable = interactableGO.AddComponent(); Rigidbody rigidBody = interactableGO.AddComponent(); rigidBody.useGravity = false; rigidBody.isKinematic = true; collider.isTrigger = true; // We set the trigger here, rather than using the function argument so that the collider gets added to the interactable's list of colliders return interactable; } internal static XRInteractableSnapVolume CreateSnapVolume() { GameObject snapVolumeGO = new GameObject("Snap Volume"); CreateGOBoxCollider(snapVolumeGO); var boxCollider = snapVolumeGO.GetComponent(); XRInteractableSnapVolume snapVolume = snapVolumeGO.AddComponent(); snapVolume.snapCollider = boxCollider; return snapVolume; } internal static GameObject CreateUICanvas(Camera worldCamera) { var canvasGo = new GameObject("Canvas", typeof(Canvas), typeof(CanvasScaler), typeof(GraphicRaycaster), typeof(TrackedDeviceGraphicRaycaster)); var canvas = canvasGo.GetComponent(); canvas.worldCamera = worldCamera; canvas.renderMode = RenderMode.WorldSpace; // Set up a GameObject hierarchy that we send events to. In a real setup, // this would be a hierarchy involving UI components. var parentGameObject = new GameObject("Parent"); var parentTransform = parentGameObject.AddComponent(); var leftChildGameObject = new GameObject("Left Child"); var leftChildTransform = leftChildGameObject.AddComponent(); leftChildGameObject.AddComponent(); leftChildGameObject.AddComponent(); var rightChildGameObject = new GameObject("Right Child"); var rightChildTransform = rightChildGameObject.AddComponent(); rightChildGameObject.AddComponent(); rightChildGameObject.AddComponent(); parentTransform.SetParent(canvas.transform, false); leftChildTransform.SetParent(parentTransform, false); rightChildTransform.SetParent(parentTransform, false); // Parent occupies full space of canvas. parentTransform.sizeDelta = new Vector2(640, 480); // Left child occupies left half of parent. const int quarterSize = 640 / 4; leftChildTransform.anchoredPosition = new Vector2(-quarterSize, 0); leftChildTransform.sizeDelta = new Vector2(320, 480); // Right child occupies right half of parent. rightChildTransform.anchoredPosition = new Vector2(quarterSize, 0); rightChildTransform.sizeDelta = new Vector2(320, 480); return canvasGo; } internal static XRSimpleInteractable CreateMultiSelectableSimpleInteractable() { var interactable = CreateSimpleInteractable(); interactable.selectMode = InteractableSelectMode.Multiple; return interactable; } internal static XRSimpleInteractable CreateSimpleInteractableWithColliders() { GameObject interactableGO = new GameObject("Simple Interactable with Colliders"); CreateGOSphereCollider(interactableGO, false); CreateGOBoxCollider(interactableGO, false); XRSimpleInteractable interactable = interactableGO.AddComponent(); Rigidbody rigidBody = interactableGO.AddComponent(); rigidBody.useGravity = false; rigidBody.isKinematic = true; return interactable; } internal static XRControllerRecorder CreateControllerRecorder(XRBaseInputInteractor interactor, Action addRecordingFrames) { var controllerRecorder = interactor.gameObject.AddComponent(); controllerRecorder.SetInteractor(interactor); controllerRecorder.recording = ScriptableObject.CreateInstance(); addRecordingFrames(controllerRecorder.recording); controllerRecorder.recording.SetFrameDependentData(); return controllerRecorder; } internal static XRTargetFilter CreateTargetFilter() { GameObject filterGO = new GameObject("Target Filter"); return filterGO.AddComponent(); } internal static XRInteractionGroup CreateInteractionGroup() { var groupGO = new GameObject("Interaction Group"); groupGO.transform.localPosition = Vector3.zero; groupGO.transform.localRotation = Quaternion.identity; var group = groupGO.AddComponent(); return group; } internal static XRInteractionGroup CreateGroupWithMockInteractors(out MockInteractor memberInteractor1, out MockInteractor memberInteractor2, out MockInteractor memberInteractor3) { var group = CreateInteractionGroup(); memberInteractor1 = CreateMockInteractor(); memberInteractor2 = CreateMockInteractor(); memberInteractor3 = CreateMockInteractor(); group.AddGroupMember(memberInteractor1); group.AddGroupMember(memberInteractor2); group.AddGroupMember(memberInteractor3); return group; } internal static XRInteractionGroup CreateGroupWithHoverOnlyMockInteractors(out MockInteractor memberInteractor1, out MockInteractor memberInteractor2, out MockInteractor memberInteractor3) { var group = CreateInteractionGroup(); memberInteractor1 = CreateMockInteractor(); memberInteractor2 = CreateMockInteractor(); memberInteractor3 = CreateMockInteractor(); memberInteractor1.allowSelect = false; memberInteractor2.allowSelect = false; memberInteractor3.allowSelect = false; group.AddGroupMember(memberInteractor1); group.AddGroupMember(memberInteractor2); group.AddGroupMember(memberInteractor3); return group; } internal static XRInteractionGroup CreateGroupWithEmptyGroups(out XRInteractionGroup memberGroup1, out XRInteractionGroup memberGroup2, out XRInteractionGroup memberGroup3) { var group = CreateInteractionGroup(); memberGroup1 = CreateInteractionGroup(); memberGroup2 = CreateInteractionGroup(); memberGroup3 = CreateInteractionGroup(); group.AddGroupMember(memberGroup1); group.AddGroupMember(memberGroup2); group.AddGroupMember(memberGroup3); return group; } internal static XRBodyTransformer CreateXRBodyTransformer(XROrigin xrOrigin) { var xrBodyTransformerGO = new GameObject("XR Body Transformer"); var xrBodyTransformer = xrBodyTransformerGO.AddComponent(); xrBodyTransformer.xrOrigin = xrOrigin; return xrBodyTransformer; } internal static LocomotionMediator CreateLocomotionMediatorWithXROrigin() { var xrOrigin = CreateXROrigin(); var mediator = xrOrigin.gameObject.AddComponent(); return mediator; } internal static MockLocomotionProvider CreateMockLocomotionProvider(LocomotionMediator mediator) { var provider = new GameObject("Mock Locomotion Provider").AddComponent(); provider.mediator = mediator; return provider; } internal static GameObject CreateCubeGameObject() { var cube = new GameObject("Cube"); CreateGOBoxCollider(cube, false); return cube; } } class MockInteractor : XRBaseInteractor { public event Action preprocessed; public event Action processed; public List validTargets { get; } = new List(); /// public override void PreprocessInteractor(XRInteractionUpdateOrder.UpdatePhase updatePhase) { base.PreprocessInteractor(updatePhase); preprocessed?.Invoke(updatePhase); } /// public override void ProcessInteractor(XRInteractionUpdateOrder.UpdatePhase updatePhase) { base.ProcessInteractor(updatePhase); processed?.Invoke(updatePhase); } /// public override void GetValidTargets(List targets) { targets.Clear(); targets.AddRange(validTargets); } } /// /// An interactable that is a plain C# object that uses a given GameObject. /// class MockClassInteractable : IXRInteractable { /// public event Action registered; /// public event Action unregistered; /// /// Invoked when is called. /// public event Action processed; /// public InteractionLayerMask interactionLayers { get; set; } = 1; /// public List colliders { get; } = new List(); /// public Transform transform { get; } /// /// Whether this interactable is registered; /// public bool isRegistered { get; private set; } /// /// Constructs a new interactable. Populates with non-trigger colliders. /// /// The associated with the Interactable. public MockClassInteractable(Transform transform) { this.transform = transform; transform.GetComponentsInChildren(colliders); colliders.RemoveAll(col => col.isTrigger); } /// public Transform GetAttachTransform(IXRInteractor interactor) { return transform; } /// public void ProcessInteractable(XRInteractionUpdateOrder.UpdatePhase updatePhase) { processed?.Invoke(updatePhase); } /// public float GetDistanceSqrToInteractor(IXRInteractor interactor) { var interactorAttachTransform = interactor?.GetAttachTransform(this); if (interactorAttachTransform == null) return float.MaxValue; return (transform.position - interactorAttachTransform.position).sqrMagnitude; } /// public void OnRegistered(InteractableRegisteredEventArgs args) { isRegistered = true; registered?.Invoke(args); } /// public void OnUnregistered(InteractableUnregisteredEventArgs args) { isRegistered = false; unregistered?.Invoke(args); } } enum TargetFilterCallback { Link, Unlink, Process, } class MockTargetFilter : IXRTargetFilter { public readonly List callbackExecution = new List(); public bool canProcess { get; set; } = true; public void Link(IXRInteractor interactor) { callbackExecution.Add(TargetFilterCallback.Link); } public void Unlink(IXRInteractor interactor) { callbackExecution.Add(TargetFilterCallback.Unlink); } public void Process(IXRInteractor interactor, List targets, List results) { results.Clear(); callbackExecution.Add(TargetFilterCallback.Process); results.AddRange(targets); } } class MockInversionTargetFilter : IXRTargetFilter { public readonly List callbackExecution = new List(); public bool canProcess { get; set; } = true; public void Link(IXRInteractor interactor) { callbackExecution.Add(TargetFilterCallback.Link); } public void Unlink(IXRInteractor interactor) { callbackExecution.Add(TargetFilterCallback.Unlink); } public void Process(IXRInteractor interactor, List targets, List results) { results.Clear(); callbackExecution.Add(TargetFilterCallback.Process); for (int i = targets.Count - 1; i >= 0; i--) { results.Add(targets[i]); } } } class MockGrabTransformer : IXRGrabTransformer { public enum MethodTrace { OnLink, OnGrab, OnGrabCountChanged, OnDrop, ProcessFixed, ProcessDynamic, ProcessLate, ProcessOnBeforeRender, OnUnlink, } public List methodTraces { get; } = new List(); public Dictionary phasesTraced { get; } = new Dictionary { { XRInteractionUpdateOrder.UpdatePhase.Fixed, false}, { XRInteractionUpdateOrder.UpdatePhase.Dynamic, true}, { XRInteractionUpdateOrder.UpdatePhase.Late, false}, { XRInteractionUpdateOrder.UpdatePhase.OnBeforeRender, false}, }; /// /// The value to set in . /// Set to to skip using it. /// public Pose? targetPoseValue { get; set; } /// /// The local scale value to set in . /// Set to to skip using it. /// public Vector3? localScaleValue { get; set; } /// public bool canProcess { get; set; } = true; /// public void OnLink(XRGrabInteractable grabInteractable) { methodTraces.Add(MethodTrace.OnLink); } /// public void OnGrab(XRGrabInteractable grabInteractable) { methodTraces.Add(MethodTrace.OnGrab); } /// public void OnGrabCountChanged(XRGrabInteractable grabInteractable, Pose targetPose, Vector3 localScale) { methodTraces.Add(MethodTrace.OnGrabCountChanged); } /// public void Process(XRGrabInteractable grabInteractable, XRInteractionUpdateOrder.UpdatePhase updatePhase, ref Pose targetPose, ref Vector3 localScale) { switch (updatePhase) { case XRInteractionUpdateOrder.UpdatePhase.Fixed: if (phasesTraced[updatePhase]) methodTraces.Add(MethodTrace.ProcessFixed); break; case XRInteractionUpdateOrder.UpdatePhase.Dynamic: if (phasesTraced[updatePhase]) methodTraces.Add(MethodTrace.ProcessDynamic); break; case XRInteractionUpdateOrder.UpdatePhase.Late: if (phasesTraced[updatePhase]) methodTraces.Add(MethodTrace.ProcessLate); break; case XRInteractionUpdateOrder.UpdatePhase.OnBeforeRender: if (phasesTraced[updatePhase]) methodTraces.Add(MethodTrace.ProcessOnBeforeRender); break; default: Assert.Fail($"Unhandled {nameof(XRInteractionUpdateOrder.UpdatePhase)}={updatePhase}"); break; } if (targetPoseValue.HasValue) targetPose = targetPoseValue.Value; if (localScaleValue.HasValue) localScale = localScaleValue.Value; } /// public void OnUnlink(XRGrabInteractable grabInteractable) { methodTraces.Add(MethodTrace.OnUnlink); } } class MockDropTransformer : MockGrabTransformer, IXRDropTransformer { /// public bool canProcessOnDrop { get; set; } = true; /// public void OnDrop(XRGrabInteractable grabInteractable, DropEventArgs args) { Assert.That(args, Is.Not.Null); Assert.That(args.selectExitEventArgs, Is.Not.Null); Assert.That(args.selectExitEventArgs.interactableObject, Is.SameAs(grabInteractable)); Assert.That(args.selectExitEventArgs.manager, Is.SameAs(grabInteractable.interactionManager)); methodTraces.Add(MethodTrace.OnDrop); } } // TODO: This is a placeholder until we can get mock touches to work with XRUIInputModule. See ARTests. class MockInputModule : BaseInputModule { public override void Process() { } public override bool IsPointerOverGameObject(int pointerId) => true; } class MockLocomotionProvider : LocomotionProvider { /// public override bool canStartMoving => m_FinishedPreparation; public DelegateXRBodyTransformation delegateTransformation { get; } = new DelegateXRBodyTransformation(); bool m_FinishedPreparation; public bool InvokeTryPrepareLocomotion() { m_FinishedPreparation = false; return TryPrepareLocomotion(); } public void FinishPreparation() { m_FinishedPreparation = true; } public bool InvokeTryStartLocomotionImmediately() { return TryStartLocomotionImmediately(); } public bool InvokeTryEndLocomotion() { return TryEndLocomotion(); } public bool InvokeTryQueueTransformation() { return TryQueueTransformation(delegateTransformation); } } }