// --------------------------------------------------------------------------------------------------------------------
//
// 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
}
}