While hovering over UI, how do I get the controller being used?

The Quest has 2 controllers, which both are being used to interact with the UI.
The Haptics events on the XRRayInteractor do not trigger. All of the XRBaseInteractable don’t fire events when they’re attached on UI components.
I guess the Event system is not written to fire off those events on UI. Only on physical objects.

So what I am trying to do is whenever I hover over UI that is interactable, I want the controller to give off a short little vibration. Like the Quest does when you’re navigating through the menu’s

I’d like to make it a component so it can be managed what and what not interaction should vibrate.
But however, when implementing the IPointerEnter / Exit you’re going to need the Input Device to actually trigger haptic feedback. And this haptic feedback should happen on the controller being used. But the event given does not have this information.

So is there a simple way to do this other than creating a custom input module just to fire off those Hover events? And also something that does not involve checking an Update loop every frame.

Edit:
I have adapted it a bit of what @C-Through suggested.

public class XRHapticFeedbackComponent : MonoBehaviour, IPointerEnterHandler
{
    public float FeedBackForce = 0.4f;
    public float Duration = 0.02f;
 
    private XRUIInputModule GetXRInputModule() => EventSystem.current.currentInputModule as XRUIInputModule;

    private bool TryGetXRRayInteractor(int pointerID, out XRRayInteractor rayInteractor)
    {
        var inputModule = GetXRInputModule();
        if (inputModule == null)
        {
            rayInteractor = null;
            return false;
        }

        rayInteractor = inputModule.GetInteractor(pointerID) as XRRayInteractor;
        return rayInteractor != null;
    }

    public void OnPointerEnter(PointerEventData eventData)
    {
        if (TryGetXRRayInteractor(eventData.pointerId, out var rayInteractor))
        {
            rayInteractor.SendHapticImpulse(FeedBackForce, Duration);
        }
    }
}

I honestly have no idea how I figured this out. But, this works and is implemented as you described, I imagine you can also do it with the pointer-events from the Input Module directly.

using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.XR.Interaction.Toolkit;
using UnityEngine.XR.Interaction.Toolkit.UI;

public class HoverHaptic : MonoBehaviour, IPointerEnterHandler
{
    private XRUIInputModule InputModule => EventSystem.current.currentInputModule as XRUIInputModule;

    public void OnPointerEnter(PointerEventData eventData)
    {
        XRRayInteractor interactor = InputModule.GetInteractor(eventData.pointerId) as XRRayInteractor;
        interactor.xrController.SendHapticImpulse(0.25f, 0.25f);
    }
}

It is a very clever solution

Getting the XR Input Module through the EventSystem to then get the Interactor via the pointer ID.
I had actually not even thought of that. I’m so used to the Event data being all I need that I didn’t even think of requesting a reference from the XR Input Module like this.

This worked perfectly for me. Thought I’d never get haptics working. Thank you very much—both of you! :slight_smile:

Another way to do this would be to check the eventData itself by casting to TrackedDeviceEventData:

public void OnPointerEnter(PointerEventData eventData)
{
    if (eventData is TrackedDeviceEventData trackedDeviceEventData)
    {
        if (trackedDeviceEventData.interactor is XRBaseControllerInteractor xrInteractor)
        {
            xrInteractor.SendHapticImpulse(0.25f, 0.25f);
        }
    }
}

Using the pattern matching magic of C# 8 this can be shortened to:

public void OnPointerEnter(PointerEventData eventData)
{
    if (eventData is TrackedDeviceEventData { interactor: XRBaseControllerInteractor xrInteractor })
    {
        xrInteractor.SendHapticImpulse(0.25f, 0.25f);
    }
}

hi guys! where should i put the script in editor?

I put it in a script at the root object of any UI of interest. I added this all after i had my menus set up and the buttons weren’t prefabbed, so this is a nice solution. only downside is that it adds it all at runtime. you could add an EventTrigger component in the inspector though and manuall do what my code is doing.

    void Awake()
    {
        buttons = GetComponentsInChildren<Button>(true);
        AddEventsComponents();
    }

    void AddEventsComponents(){
        for(int i = 0; i < buttons.Length; i++){
            Button btn = buttons[i];

            EventTrigger trigger = btn.gameObject.AddComponent<EventTrigger>();

            //hover enter
            EventTrigger.Entry hoverEnterEntry = new EventTrigger.Entry();
            hoverEnterEntry.eventID = EventTriggerType.PointerEnter;
            hoverEnterEntry.callback.AddListener(HandleHoverEnter);
            trigger.triggers.Add(hoverEnterEntry);
        }
    }

    void HandleHoverEnter(BaseEventData eventData = null){

        if (eventData is TrackedDeviceEventData trackedDeviceEventData)
        {
            if (trackedDeviceEventData.interactor is XRBaseControllerInteractor xrInteractor)
            {
                xrInteractor.SendImpulse(.1f, .1f);
            }
        }
    }

For anyone like me finding this thread trying to doing it with Oculus Interaction SDK I found a working solution. Hopefully it works for you.

You attach it to the Unity Canvas GameObject that’s already set up with a RayInteractable/PointableCanvas Make sure you have a PointableCanvasModule in your scene.

using System;
using System.Collections;
using Oculus.Interaction;
using Oculus.Interaction.Input;
using UnityEngine;

[RequireComponent(typeof(Canvas))]
public class RayCanvasHapticPulse: MonoBehaviour
{

    [Range(0,1)]
    [SerializeField] private float _pulseAmplitude = 0.1f;
   
    private RayInteractor[] _rayInteractors;
    private Canvas _canvas;

    private void Awake()
    {
        _canvas = GetComponent<Canvas>();
        _rayInteractors = FindObjectsOfType<RayInteractor>();
    }

    private void OnEnable()
    {
        PointableCanvasModule.WhenSelectableHovered += Hovered;
    }

    private void OnDisable()
    {
        PointableCanvasModule.WhenSelectableHovered -= Hovered;
    }

    private void Hovered(PointableCanvasEventArgs eventArgs)
    {
        if (eventArgs.Canvas != _canvas)
        {
            return; // wrong canvas
        }
       
        foreach (RayInteractor rayInteractor in _rayInteractors)
        {
            if (rayInteractor.State == InteractorState.Hover)
            {
                Controller controller = rayInteractor.GetComponentInParent<Controller>();
                Debug.Log($"Hand {controller.Handedness}");
                switch (controller.Handedness)
                {
                    case Handedness.Left:
                        StartCoroutine(Pulse(OVRInput.Controller.LTouch));
                        break;
                    case Handedness.Right:
                        StartCoroutine(Pulse(OVRInput.Controller.RTouch));
                        break;
                    default:
                        throw new ArgumentOutOfRangeException();
                }
            }
        }
    }

    private IEnumerator Pulse(OVRInput.Controller controller)
    {
        OVRInput.SetControllerVibration(0, _pulseAmplitude, controller);
        yield return null;
        OVRInput.SetControllerVibration(0, 0, controller);
    }
}

Thank you!

To show your appreciation, please use the “Like” button so you don’t end up necroposting.

Thanks.

I was really struggling with this until I came across this thread, so thanks to everyone for their suggestions!

This solution worked for me, but since I’m using Unity 2022.3.58f1, I was prompted to change ‘XRBaseControllerInteractor’ to ‘XRBaseInputInteractor’. Both the long and shortened methods worked perfectly for me after that. Here’s what they became:

public void OnPointerEnter(PointerEventData eventData)
{
    if (eventData is TrackedDeviceEventData trackedDeviceEventData)
    {
        if (trackedDeviceEventData.interactor is XRBaseInputInteractor xrInteractor)
        {
            xrInteractor.SendHapticImpulse(0.25f, 0.25f);
        }
    }
}

And

public void OnPointerEnter(PointerEventData eventData)
{
    if (eventData is TrackedDeviceEventData { interactor: XRBaseInputInteractor xrInteractor })
    {
        xrInteractor.SendHapticImpulse(0.25f, 0.25f);
    }
}

Thanks again! :slightly_smiling_face: