// -------------------------------------------------------------------------------------------------------------------- // // Copyright (c) VRMADA, All rights reserved. // // -------------------------------------------------------------------------------------------------------------------- using System.Collections; using System.Collections.Generic; using System.Linq; using System.Reflection; using UltimateXR.Avatar; using UltimateXR.Core; using UltimateXR.Core.Settings; using UltimateXR.Extensions.Unity; using UltimateXR.Haptics; using UltimateXR.UI.UnityInputModule.Controls; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI; namespace UltimateXR.UI.UnityInputModule { /// /// Input module for Unity that enables interaction in virtual reality using either touch gestures (via /// ) or laser pointers (via ). /// Using it is possible to automatically set up existing Unity canvases ( /// components), otherwise it is required to add a component on each /// GameObject having a to enable interaction. /// /// /// components instantiated at runtime should have a already present in /// order for VR interaction to work, since only works for objects present in /// the scene after loading. /// public class UxrPointerInputModule : PointerInputModule { #region Inspector Properties/Serialized Fields [SerializeField] protected bool _disableOtherInputModules; [SerializeField] protected bool _autoEnableOnWorldCanvases = true; [SerializeField] protected bool _autoAssignEventCamera = true; [SerializeField] protected UxrInteractionType _interactionTypeOnAutoEnable = UxrInteractionType.FingerTips; [SerializeField] protected float _fingerTipMinHoverDistance = UxrFingerTipRaycaster.FingerTipMinHoverDistanceDefault; [SerializeField] protected int _dragThreshold = 40; #endregion #region Public Types & Data /// /// Gets the singleton instance. /// public static UxrPointerInputModule Instance { get; private set; } /// /// Gets whether the input module will try to find all components after loading a scene, in order /// to add a component to those that have not been set up. /// public bool AutoEnableOnWorldCanvases => _autoEnableOnWorldCanvases; /// /// Gets whether to assign the event camera to the components to that they use the local /// . /// public bool AutoAssignEventCamera => _autoAssignEventCamera; /// /// Gets, for those canvases that have been set up automatically using , the /// type of interaction that will be used. /// public UxrInteractionType InteractionTypeOnAutoEnable => _interactionTypeOnAutoEnable; /// /// Gets the minimum distance from a finger tip to a canvas in order to generate hovering events, when /// is , /// public float FingerTipMinHoverDistance => _fingerTipMinHoverDistance; #endregion #region Public Overrides BaseInputModule /// /// Updates the input module. This is additional functionality to enable the UXR input module to coexist with Unity's /// input module for screen UI. /// /// From user Cind13 in https://forum.unity.com/threads/multiple-processing-inputmodules.369578/ public override void UpdateModule() { MethodInfo changeEventModuleMethod = EventSystem.current.GetType().GetMethod("ChangeEventModule", BindingFlags.NonPublic | BindingFlags.Instance, null, new[] { typeof(BaseInputModule) }, null); changeEventModuleMethod.Invoke(EventSystem.current, new object[] { this }); EventSystem.current.UpdateModules(); List activeInputModules = GetInputModules(); activeInputModules.Remove(this); activeInputModules.Insert(0, this); SetInputModules(activeInputModules); } /// public override bool IsModuleSupported() { return true; } /// public override void Process() { // Execute other input modules if they are not requested to be disabled if (!_disableOtherInputModules) { foreach (BaseInputModule module in GetInputModules()) { if (module != this) { module.Process(); } } } if (UxrManager.Instance == null) { return; } // Update this input module bool usedEvent = SendUpdateEventToSelectedObject(); if (UxrAvatar.LocalAvatar != null && UxrAvatar.LocalAvatar.RenderMode.HasFlag(UxrAvatarRenderModes.Avatar)) { foreach (UxrFingerTip fingerTip in UxrFingerTip.EnabledComponentsInLocalAvatar) { ProcessPointerEvents(GetFingerTipPointerEventData(fingerTip)); } } foreach (UxrLaserPointer laserPointer in UxrLaserPointer.EnabledComponentsInLocalAvatar) { ProcessPointerEvents(GetLaserPointerEventData(laserPointer)); } /* TODO: Create navigation events using controller input? if (eventSystem.sendNavigationEvents) { if (!usedEvent) { usedEvent |= SendMoveEventToSelectedObject(); } if (!usedEvent) { SendSubmitEventToSelectedObject(); } }*/ } #endregion #region Public Overrides PointerInputModule /// /// Overrides Object.ToString(). /// /// From user chrpetry in https://forum.unity.com/threads/multiple-processing-inputmodules.369578/ /// String description of the class public override string ToString() { var moduleStringList = new List(); foreach (var module in GetInputModules()) { if (module != this) { moduleStringList.Add(module.ToString()); } } return string.Join("\n\n", moduleStringList); } #endregion #region Public Methods /// /// Checks if the given GameObject is interactive. An object is considered interactive when it is able to handle either /// pointer down or pointer drag events. /// Since other non-interactive objects may be in front of interactive objects, the whole hierarchy is checked up to /// the first found. /// /// UI GameObject to check /// Whether the given GameObject is interactive or not public static bool IsInteractive(GameObject uiGameObject) { if (uiGameObject == null) { return false; } UxrCanvas canvas = uiGameObject.GetComponentInParent(); Transform currentTransform = uiGameObject.transform; if (canvas == null) { return false; } while (currentTransform != canvas.transform) { if (ExecuteEvents.CanHandleEvent(currentTransform.gameObject) || ExecuteEvents.CanHandleEvent(currentTransform.gameObject)) { return true; } currentTransform = currentTransform.parent; } return false; } /// /// Gets the pointer event data of a given if it exists. /// /// Finger tip to get the event data of /// Pointer event data if it exists or null if not public UxrPointerEventData GetPointerEventData(UxrFingerTip fingerTip) { return _fingerTipEventData.TryGetValue(fingerTip, out UxrPointerEventData pointerEventData) ? pointerEventData : null; } /// /// Gets the pointer event data of a given if it exists. /// /// Laser pointer to get the event data of /// Pointer event data if it exists or null if not public UxrPointerEventData GetPointerEventData(UxrLaserPointer laserPointer) { return _laserPointerEventData.TryGetValue(laserPointer, out UxrPointerEventData pointerEventData) ? pointerEventData : null; } #endregion #region Unity /// /// Initializes the component. /// protected override void Awake() { base.Awake(); if (Instance != null) { if (UxrGlobalSettings.Instance.LogLevelUI >= UxrLogLevel.Errors) { Debug.LogError($"{UxrConstants.UiModule} There is already an active {nameof(UxrPointerInputModule)} in the scene. Only one {nameof(UxrPointerInputModule)} can be used."); } } else { Instance = this; if (_disableOtherInputModules) { BaseInputModule[] baseInputModules = FindObjectsOfType(); foreach (BaseInputModule inputModule in baseInputModules) { if (inputModule != this) { inputModule.enabled = false; } } } } } /// /// Subscribes to events and sets up the haptics coroutine. /// protected override void OnEnable() { base.OnEnable(); UxrControlInput.GlobalPressed += UxrControlInput_GlobalPressed; UxrControlInput.GlobalReleased += UxrControlInput_GlobalReleased; UxrControlInput.GlobalClicked += UxrControlInput_GlobalClicked; _coroutineDragHaptics = StartCoroutine(CoroutineDragHaptics()); } /// /// Unsubscribes from events and stops the haptics coroutine. /// protected override void OnDisable() { base.OnDisable(); UxrControlInput.GlobalPressed -= UxrControlInput_GlobalPressed; UxrControlInput.GlobalReleased -= UxrControlInput_GlobalReleased; UxrControlInput.GlobalClicked -= UxrControlInput_GlobalClicked; StopCoroutine(_coroutineDragHaptics); } /// /// Sets the drag threshold. /// protected override void Start() { base.Start(); if (eventSystem != null) { eventSystem.pixelDragThreshold = _dragThreshold; } } #endregion #region Event Handling Methods /// /// Called whenever a was pressed. /// /// Control that was interacted with /// Event parameters private void UxrControlInput_GlobalPressed(UxrControlInput controlInput, PointerEventData eventData) { if (eventData is UxrPointerEventData pointerEventData) { TrySendButtonFeedback(controlInput, controlInput.FeedbackOnDown, pointerEventData); } } /// /// Called whenever a was released after being pressed. /// /// Control that was interacted with /// Event parameters private void UxrControlInput_GlobalReleased(UxrControlInput controlInput, PointerEventData eventData) { if (eventData is UxrPointerEventData pointerEventData) { TrySendButtonFeedback(controlInput, controlInput.FeedbackOnUp, pointerEventData); } } /// /// Called whenever a was clicked. Depending on the operating mode, a click is either a /// release after a press or a press. /// /// Control that was interacted with /// Event parameters private void UxrControlInput_GlobalClicked(UxrControlInput controlInput, PointerEventData eventData) { if (eventData is UxrPointerEventData pointerEventData) { TrySendButtonFeedback(controlInput, controlInput.FeedbackOnClick, pointerEventData); } } #endregion #region Protected Methods /// /// Finds the raycast that will be used to find out which UI element the user interacted with. /// /// List of candidates, sorted in increasing distance order /// Pointer data /// The raycast that describes the UI element that the user interacted with protected static RaycastResult FindFirstRaycast(List candidates, UxrPointerEventData pointerEventData) { int first = -1; int candidatesCount = candidates.Count; // First search for the first raycast that shares canvas with the pointerEnter event UxrCanvas initialCanvas = pointerEventData.pointerEnter != null ? pointerEventData.pointerEnter.GetTopmostCanvas() : null; for (int i = 0; i < candidatesCount; ++i) { UxrGraphicRaycaster module = candidates[i].module as UxrGraphicRaycaster; if (candidates[i].gameObject == null || module == null) { continue; } if (first == -1) { first = i; } if (initialCanvas != null && candidates[i].gameObject.GetTopmostCanvas() == initialCanvas) { return candidates[i]; } } // Not found? return first if (first != -1) { return candidates[first]; } // No results? return empty return new RaycastResult(); } /// /// Processes the pointer events. /// /// Pointer event data protected virtual void ProcessPointerEvents(UxrPointerEventData pointerEventData) { // Handle events bool pressedBefore = pointerEventData.pointerPress != null; bool isDraggingBefore = pointerEventData.dragging; ProcessPointerPressRelease(pointerEventData); ProcessMove(pointerEventData); ProcessDrag(pointerEventData); if (!isDraggingBefore && pointerEventData.dragging) { if (UxrGlobalSettings.Instance.LogLevelUI >= UxrLogLevel.Relevant) { Debug.Log($"{UxrConstants.UiModule} BeginDrag on {GetObjectLogName(pointerEventData.pointerDrag)})."); } } bool pressedNow = pointerEventData.pointerPress != null; // Default haptic feedback if we don't have UxrControlInput if (pressedNow && !pressedBefore && pointerEventData.pointerPress.GetComponent() == null) { if (UxrAvatar.LocalAvatarInput) { UxrAvatar.LocalAvatarInput.SendHapticFeedback(pointerEventData.HandSide, UxrHapticClipType.Click, 0.2f); } } if (pointerEventData.GameObjectClicked) { if (pointerEventData.GameObjectClicked.GetComponent() == null) { if (UxrAvatar.LocalAvatarInput) { UxrAvatar.LocalAvatarInput.SendHapticFeedback(pointerEventData.HandSide, UxrHapticClipType.Click, 0.6f); } } pointerEventData.GameObjectClicked = null; } pointerEventData.Speed = pointerEventData.delta.magnitude / Time.deltaTime; } /// /// Processes the pointer press and release events. /// /// Pointer event data protected virtual void ProcessPointerPressRelease(UxrPointerEventData pointerEventData) { if (ShouldIgnoreEventData(pointerEventData)) { return; } GameObject currentOverGo = pointerEventData.pointerCurrentRaycast.gameObject; // PointerDown notification if (pointerEventData.PressedThisFrame) { pointerEventData.eligibleForClick = true; pointerEventData.delta = Vector2.zero; pointerEventData.dragging = false; pointerEventData.useDragThreshold = true; pointerEventData.pressPosition = pointerEventData.position; pointerEventData.pointerPressRaycast = pointerEventData.pointerCurrentRaycast; DeselectIfSelectionChanged(currentOverGo, pointerEventData); // search for the control that will receive the press // if we can't find a press handler set the press // handler to be what would receive a click. GameObject newPressed = ExecuteEvents.ExecuteHierarchy(currentOverGo, pointerEventData, ExecuteEvents.pointerDownHandler); if (newPressed != null && UxrGlobalSettings.Instance.LogLevelUI >= UxrLogLevel.Relevant) { Debug.Log($"{UxrConstants.UiModule} Press on {GetObjectLogName(newPressed)}."); } // didnt find a press handler... search for a click handler if (newPressed == null) { newPressed = ExecuteEvents.GetEventHandler(currentOverGo); } float time = Time.unscaledTime; if (newPressed == pointerEventData.lastPress) { float diffTime = time - pointerEventData.clickTime; if (diffTime < 0.3f) { ++pointerEventData.clickCount; } else { pointerEventData.clickCount = 1; } pointerEventData.clickTime = time; } else { pointerEventData.clickCount = 1; } pointerEventData.pointerPress = newPressed; pointerEventData.rawPointerPress = currentOverGo; pointerEventData.clickTime = time; pointerEventData.pointerDrag = ExecuteEvents.GetEventHandler(currentOverGo); if (pointerEventData.pointerDrag != null) { ExecuteEvents.Execute(pointerEventData.pointerDrag, pointerEventData, ExecuteEvents.initializePotentialDrag); } // If the UI element has scrolling, click will require press+release to support dragging. // If not, it's a little more user friendly in VR to require just a press to avoid missing clicks. // TODO: Be able to control if this feature is enabled via an inspector parameter. // TODO: Check compatibility with drag&drop. if (pointerEventData.pointerPress && !RequiresScrolling(pointerEventData.pointerPress)) { // UI element doesn't require scrolling. Perform a click on press instead of a click on release. pointerEventData.eligibleForClick = false; ExecuteEvents.Execute(pointerEventData.pointerPress, pointerEventData, ExecuteEvents.pointerClickHandler); pointerEventData.GameObjectClicked = pointerEventData.pointerPress; if (UxrGlobalSettings.Instance.LogLevelUI >= UxrLogLevel.Relevant && pointerEventData.pointerPress) { Debug.Log($"{UxrConstants.UiModule} Click on {GetObjectLogName(pointerEventData.pointerPress)}."); } } } // PointerUp notification if (pointerEventData.ReleasedThisFrame) { if (ExecuteEvents.Execute(pointerEventData.pointerPress, pointerEventData, ExecuteEvents.pointerUpHandler)) { if (UxrGlobalSettings.Instance.LogLevelUI >= UxrLogLevel.Relevant) { Debug.Log($"{UxrConstants.UiModule} Release on {GetObjectLogName(pointerEventData.pointerPress)}."); } } // see if the release is on the same element that was pressed... GameObject pointerUpHandler = ExecuteEvents.GetEventHandler(currentOverGo); // PointerClick and Drop events if (pointerEventData.pointerPress == pointerUpHandler && pointerEventData.eligibleForClick) { if (ExecuteEvents.Execute(pointerEventData.pointerPress, pointerEventData, ExecuteEvents.pointerClickHandler)) { if (UxrGlobalSettings.Instance.LogLevelUI >= UxrLogLevel.Relevant) { Debug.Log($"{UxrConstants.UiModule} Click on {GetObjectLogName(pointerEventData.pointerPress)}."); } } pointerEventData.GameObjectClicked = pointerEventData.pointerPress; } else if (pointerEventData.pointerDrag != null) { GameObject dropGo = ExecuteEvents.ExecuteHierarchy(currentOverGo, pointerEventData, ExecuteEvents.dropHandler); if (dropGo && UxrGlobalSettings.Instance.LogLevelUI >= UxrLogLevel.Relevant) { Debug.Log($"{UxrConstants.UiModule} Drop on {GetObjectLogName(dropGo)}."); } } pointerEventData.eligibleForClick = false; pointerEventData.pointerPress = null; pointerEventData.rawPointerPress = null; pointerEventData.pointerClick = null; if (pointerEventData.pointerDrag != null && pointerEventData.dragging) { if (ExecuteEvents.Execute(pointerEventData.pointerDrag, pointerEventData, ExecuteEvents.endDragHandler)) { if (UxrGlobalSettings.Instance.LogLevelUI >= UxrLogLevel.Relevant) { Debug.Log($"{UxrConstants.UiModule} EndDrag on {GetObjectLogName(pointerEventData.pointerDrag)}."); } } } pointerEventData.dragging = false; pointerEventData.pointerDrag = null; // redo pointer enter / exit to refresh state // so that if we moused over something that ignored it before // due to having pressed on something else // it now gets it. if (currentOverGo != pointerEventData.pointerEnter) { HandlePointerExitAndEnter(pointerEventData, null); HandlePointerExitAndEnter(pointerEventData, currentOverGo); } } } #endregion #region Private Methods /// /// Checks whether the given pointer event data should be ignored. Event data coming from non-UXR modules will be /// ignored. /// /// Pointer event data /// Whether the event data should be ignored private static bool ShouldIgnoreEventData(UxrPointerEventData pointerEventData) { return pointerEventData.pointerCurrentRaycast.module as UxrGraphicRaycaster == null && !pointerEventData.ReleasedThisFrame; } /// /// Gets a string to represent the name of a GameObject in a Debug.Log line. /// /// GameObject to get the name for /// Name string private static string GetObjectLogName(GameObject gameObject) { return $"{gameObject.name} ({gameObject.GetPathUnderScene()})"; } /// /// Gets the list of active input modules. This is additional functionality to enable the UXR input module to coexist /// with Unity's input module for screen UI. /// /// From user Cind13 in https://forum.unity.com/threads/multiple-processing-inputmodules.369578/ /// List of input modules private static List GetInputModules() { EventSystem current = EventSystem.current; FieldInfo m_SystemInputModules = current.GetType().GetField("m_SystemInputModules", BindingFlags.NonPublic | BindingFlags.Instance); return m_SystemInputModules.GetValue(current) as List; } /// /// Sets the list of active input modules. This is additional functionality to enable the UXR input module to coexist /// with Unity's input module for screen UI. /// /// From user Cind13 in https://forum.unity.com/threads/multiple-processing-inputmodules.369578/ private static void SetInputModules(List inputModules) { EventSystem current = EventSystem.current; FieldInfo m_SystemInputModules = current.GetType().GetField("m_SystemInputModules", BindingFlags.NonPublic | BindingFlags.Instance); m_SystemInputModules.SetValue(current, inputModules); } /// /// Gets whether a ray-casted UI element requires auto-enabling the laser pointer. /// /// Raycast to check /// The laser pointer /// Whether the UI element will auto-enable the laser pointer private static bool DoesAutoEnableLaserPointer(RaycastResult raycast, UxrLaserPointer laserPointer) { if (laserPointer.IgnoreAutoEnable) { return false; } if (raycast.isValid && raycast.gameObject) { UxrCanvas[] canvasVR = raycast.gameObject.GetComponentsInParent(); foreach (UxrCanvas canvas in canvasVR) { if (canvas.CanvasInteractionType == UxrInteractionType.LaserPointers && canvas.AutoEnableLaserPointer && canvas.IsCompatible(laserPointer.HandSide) && raycast.distance <= canvas.AutoEnableDistance) { return true; } } } return false; } /// /// Coroutine that sends haptic feedback when elements are being dragged. /// /// Coroutine enumerator private IEnumerator CoroutineDragHaptics() { void SendDragHapticFeedback(UxrHandSide handSide, float dragSpeed) { float quantityPos = (dragSpeed - HapticsMinSpeed) / (HapticsMaxSpeed - HapticsMinSpeed); float frequencyPos = Mathf.Lerp(HapticsMinFrequency, HapticsMaxFrequency, Mathf.Clamp01(quantityPos)); float amplitudePos = Mathf.Lerp(HapticsMinAmplitude, HapticsMaxAmplitude, Mathf.Clamp01(quantityPos)); UxrAvatar.LocalAvatarInput.SendHapticFeedback(handSide, frequencyPos, amplitudePos, UxrConstants.InputControllers.HapticSampleDurationSeconds); } while (true) { float maxLeftSpeed = Mathf.Max(GetMaxDragSpeed(_fingerTipEventData, UxrHandSide.Left), GetMaxDragSpeed(_laserPointerEventData, UxrHandSide.Left)); float maxRightSpeed = Mathf.Max(GetMaxDragSpeed(_fingerTipEventData, UxrHandSide.Right), GetMaxDragSpeed(_laserPointerEventData, UxrHandSide.Right)); if (maxLeftSpeed >= HapticsMinSpeed) { SendDragHapticFeedback(UxrHandSide.Left, maxLeftSpeed); } if (maxRightSpeed >= HapticsMinSpeed) { SendDragHapticFeedback(UxrHandSide.Right, maxRightSpeed); } yield return new WaitForSeconds(UxrConstants.InputControllers.HapticSampleDurationSeconds); } } /// /// Returns whether the given GameObject is part of a UI that requires scrolling. /// /// UI GameObject to test /// Whether the GameObject requires scrolling private bool RequiresScrolling(GameObject pointerPress) { ScrollRect scrollRect = pointerPress.GetComponentInParent(); if (scrollRect != null) { if (scrollRect.viewport == null || scrollRect.content == null) { // We've found cases where null viewports may mean special handling of scroll views to virtualize visible area (SRDebugger) return true; } if (scrollRect.horizontal && scrollRect.content.rect.width > scrollRect.viewport.rect.width) { // Scrollable content is wider than viewport. return true; } if (scrollRect.vertical && scrollRect.content.rect.height > scrollRect.viewport.rect.height) { // Scrollable content is taller than viewport. return true; } } return false; } /// /// Checks whether to send UpdateSelected events. /// /// Whether an UpdateSelected event was processed private bool SendUpdateEventToSelectedObject() { if (eventSystem.currentSelectedGameObject == null) { return false; } var data = GetBaseEventData(); ExecuteEvents.Execute(eventSystem.currentSelectedGameObject, data, ExecuteEvents.updateSelectedHandler); return data.used; } /// /// Sends button audio/haptic feedback depending on the events processed. /// private void TrySendButtonFeedback(UxrControlInput controlInput, UxrControlFeedback controlFeedback, UxrPointerEventData pointerEventData) { if (UxrAvatar.LocalAvatarInput) { UxrAvatar.LocalAvatarInput.SendHapticFeedback(pointerEventData.HandSide, controlFeedback.HapticClip); } if (controlFeedback.AudioClip != null && controlInput.Interactable) { Vector3 audioPosition = !controlFeedback.UseAudio3D && UxrAvatar.LocalAvatarCamera ? UxrAvatar.LocalAvatar.CameraPosition : controlInput.transform.position; AudioSource.PlayClipAtPoint(controlFeedback.AudioClip, audioPosition, controlFeedback.AudioVolume); } } /// /// Gets or creates the pointer event data for a finger tip. /// /// Finger tip to process /// Returns the pointer event data for the given finger tip /// Whether to create the data if an entry for the given finger tip doesn't exist private void FetchPointerEventData(UxrFingerTip fingerTip, out UxrPointerEventData data, bool create) { if (!_fingerTipEventData.TryGetValue(fingerTip, out data) && create) { data = new UxrPointerEventData(eventSystem, fingerTip); _fingerTipEventData.Add(fingerTip, data); } } /// /// Gets or creates the pointer event data for a laser pointer. /// /// Laser pointer to process /// Returns the pointer event data for the given laser pointer /// Whether to create the data if an entry for the given laser pointer doesn't exist private void FetchPointerEventData(UxrLaserPointer laserPointer, out UxrPointerEventData data, bool create) { if (!_laserPointerEventData.TryGetValue(laserPointer, out data) && create) { data = new UxrPointerEventData(eventSystem, laserPointer); _laserPointerEventData.Add(laserPointer, data); } } /// /// Computes the event data for the given finger tip. /// /// Finger tip to compute the event data for /// Pointer event data private UxrPointerEventData GetFingerTipPointerEventData(UxrFingerTip fingerTip) { // Get/create the event data FetchPointerEventData(fingerTip, out UxrPointerEventData data, true); data.Reset(); // TODO: Add scroll support using thumbstick? // leftData.scrollDelta = ... data.button = PointerEventData.InputButton.Left; // Finger tip worldpos/previousworldpos initialization data.PreviousWorldPos = data.WorldPos; data.WorldPos = fingerTip.WorldPos; if (!data.WorldPosInitialized) { data.WorldPosInitialized = true; return data; } // Raycast RaycastResult raycastResult = data.pointerCurrentRaycast; raycastResult.worldPosition = fingerTip.WorldPos; raycastResult.worldNormal = fingerTip.WorldDir; data.pointerCurrentRaycast = raycastResult; eventSystem.RaycastAll(data, m_RaycastResultCache); RaycastResult raycast = FindFirstRaycast(m_RaycastResultCache, data); m_RaycastResultCache.Clear(); // Check if it is a compatible hand using our UxrCanvas. // TODO: Support 2D/3D objects in the UxrFingerTipRaycaster? data.IgnoredGameObject = null; data.GameObject2D = raycast.isValid && raycast.depth == UxrConstants.UI.Depth2DObject ? raycast.gameObject : null; data.GameObject3D = raycast.isValid && raycast.depth == UxrConstants.UI.Depth3DObject ? raycast.gameObject : null; data.IsInteractive = raycast.isValid && !data.IsNonUI && IsInteractive(raycast.gameObject); bool isHandCompatible = IsHandCompatible(fingerTip.Side, raycast.gameObject); if (data.IsNonUI || !isHandCompatible) { // If finger tip should be ignored, null the raycast object so that events still gets processed as if nothing was hit. // This is mainly to process things correctly if the finger tip is invalidated at runtime. data.IgnoredGameObject = raycast.gameObject; raycast.gameObject = null; } data.pointerCurrentRaycast = raycast; // Try to find our ray casting module UxrFingerTipRaycaster raycaster = raycast.module as UxrFingerTipRaycaster; if (raycaster) { // Get screen position generated by our module data.position = raycast.screenPosition; } // First force a release if some conditions are met. We basically want to avoid using fingertips whenever a user tries to grab something bool fingerTipValid = fingerTip.gameObject.activeInHierarchy && fingerTip.enabled && fingerTip.Avatar.AvatarController.CanHandInteractWithUI(fingerTip.Side); data.PressedThisFrame = false; data.ReleasedThisFrame = data.pointerPress != null && !fingerTipValid; // Check for presses/releases by comparing the finger tip current/last positions against the UI object's plane if (data.pointerEnter && fingerTipValid) { if (!IsFingerTipOutside(data, data.pointerEnter) && WasFingerTipPreviousPosOutside(data, data.pointerEnter)) { data.PressedThisFrame = true; } else if (IsFingerTipOutside(data, data.pointerEnter) && !WasFingerTipPreviousPosOutside(data, data.pointerEnter)) { data.ReleasedThisFrame = true; } } // Make sure here that UI events will get called appropriately if (data.pointerCurrentRaycast.gameObject == null && data.pointerEnter != null) { data.ReleasedThisFrame = true; } return data; } /// /// Computes the event data for the laser pointer. /// /// Laser pointer to compute the event data for /// Pointer event data private UxrPointerEventData GetLaserPointerEventData(UxrLaserPointer laserPointer) { // Get/create the event data FetchPointerEventData(laserPointer, out UxrPointerEventData data, true); data.Reset(); // TODO: Add scroll support using thumbstick? // leftData.scrollDelta = ... data.button = PointerEventData.InputButton.Left; // Raycast RaycastResult raycastResult = data.pointerCurrentRaycast; raycastResult.worldPosition = laserPointer.LaserPos; raycastResult.worldNormal = laserPointer.LaserDir; data.pointerCurrentRaycast = raycastResult; data.IgnoredGameObject = null; eventSystem.RaycastAll(data, m_RaycastResultCache); RaycastResult raycast = FindFirstRaycast(m_RaycastResultCache, data); m_RaycastResultCache.Clear(); // Raycasts are performed using length to canvas. If no raycast was found, raycast using laser length first. bool colliderRaycastProcessed = false; if (!raycast.isValid) { if (laserPointer.TargetTypes.HasFlag(UxrLaserPointerTargetTypes.Colliders3D)) { if (Physics.Raycast(new Ray(laserPointer.LaserPos, laserPointer.LaserDir), out RaycastHit hit, laserPointer.MaxRayLength, laserPointer.BlockingMask, laserPointer.TriggerCollidersInteraction)) { raycast.gameObject = hit.collider.gameObject; raycast.distance = hit.distance; data.GameObject2D = null; data.GameObject3D = hit.collider.gameObject; data.IsInteractive = false; colliderRaycastProcessed = true; } } else if (laserPointer.TargetTypes.HasFlag(UxrLaserPointerTargetTypes.Colliders2D)) { RaycastHit2D hit = Physics2D.Raycast(laserPointer.LaserPos, laserPointer.LaserDir, laserPointer.MaxRayLength, laserPointer.BlockingMask); if (hit.collider) { raycast.gameObject = hit.collider.gameObject; raycast.distance = hit.distance; data.GameObject2D = hit.collider.gameObject; data.GameObject3D = null; data.IsInteractive = false; colliderRaycastProcessed = true; } } } if (!colliderRaycastProcessed) { // Check if the current ray-casted element is interactive data.GameObject2D = raycast.isValid && raycast.depth == UxrConstants.UI.Depth2DObject ? raycast.gameObject : null; data.GameObject3D = raycast.isValid && raycast.depth == UxrConstants.UI.Depth3DObject ? raycast.gameObject : null; data.IsInteractive = raycast.isValid && !data.IsNonUI && IsInteractive(raycast.gameObject); } laserPointer.IsAutoEnabled = !data.IsNonUI && DoesAutoEnableLaserPointer(raycast, laserPointer); bool isHandCompatible = IsHandCompatible(laserPointer.HandSide, raycast.gameObject); if (data.IsNonUI || !laserPointer.IsLaserEnabled || !isHandCompatible) { // If laser should be ignored, null the raycast object so that events still gets processed as if nothing was hit. // TODO: Make sure that this is called after controller input processing during this frame data.IgnoredGameObject = raycast.gameObject; raycast.gameObject = null; data.IsInteractive = false; } data.pointerCurrentRaycast = raycast; // Try to find our ray casting module UxrLaserPointerRaycaster raycaster = raycast.module as UxrLaserPointerRaycaster; if (raycaster) { // Get screen position generated by our module data.position = raycast.screenPosition; } // Make sure here that UI events will get called appropriately data.PressedThisFrame = isHandCompatible && laserPointer.IsLaserEnabled && laserPointer.IsClickedThisFrame(); if (data.pointerPress != null && !laserPointer.IsLaserEnabled) { data.ReleasedThisFrame = true; } else { data.ReleasedThisFrame = laserPointer.IsLaserEnabled && laserPointer.IsReleasedThisFrame(); } if (data.pointerCurrentRaycast.gameObject == null && data.pointerEnter != null) { data.ReleasedThisFrame = true; } return data; } /// /// Gets whether the given hand is compatible with the target GameObject. /// /// Which hand /// UI element GameObject /// Whether the hand is compatible /// private bool IsHandCompatible(UxrHandSide handSide, GameObject uiGameObject) { if (uiGameObject == null) { return false; } UxrCanvas canvas = uiGameObject.GetComponentInParent(); return !canvas || canvas.IsCompatible(handSide); } /// /// Gets the maximum drag speed of the pointers that are interacting with a given control. /// /// Event data for all pointers /// Which hand /// Pointer type /// Maximum drag speed in units/second private float GetMaxDragSpeed(Dictionary eventData, UxrHandSide handSide) { return eventData.Where(d => d.Value.HandSide == handSide && d.Value.dragging && d.Value.Avatar.AvatarController.CanHandInteractWithUI(handSide)) .Select(d => d.Value.Speed) .DefaultIfEmpty(0.0f) .Max(); } /// /// Gets whether a finger tip is on the front side of the plane where the control lies. /// /// Pointer event data /// Control /// Whether the finger tip is on the front side private bool IsFingerTipOutside(UxrPointerEventData pointerEventData, GameObject uiGameObject) { return Vector3.Dot(uiGameObject.transform.position - pointerEventData.WorldPos, uiGameObject.transform.forward) > 0.0f; } /// /// Gets whether a finger tip was on the front side of the plane where the control lies, during the previous frame. /// /// Pointer event data /// Control /// Whether the finger tip was on the front side the previous frame private bool WasFingerTipPreviousPosOutside(UxrPointerEventData pointerEventData, GameObject uiGameObject) { return Vector3.Dot(uiGameObject.transform.position - pointerEventData.PreviousWorldPos, uiGameObject.transform.forward) > 0.0f; } #endregion #region Private Types & Data private const float HapticsMinAmplitude = 0.01f; private const float HapticsMaxAmplitude = 0.2f; private const float HapticsMinFrequency = 200.0f; private const float HapticsMaxFrequency = 200.0f; private const float HapticsMinSpeed = 30.0f; private const float HapticsMaxSpeed = 12000.0f; private readonly Dictionary _fingerTipEventData = new Dictionary(); private readonly Dictionary _laserPointerEventData = new Dictionary(); private Coroutine _coroutineDragHaptics; #endregion } }