Search Unity

The same UI for different sources

Discussion in 'UI Toolkit' started by yuemco, Feb 24, 2021.

  1. yuemco

    yuemco

    Joined:
    Jul 1, 2018
    Posts:
    36
    Hi all,

    What is the best practice of generating or building the same UI for different sources (It can be Oculus VR UI, can be a tablet UI or can be desktop UI)? The problem is each source uses different dependencies and Is it possible to exclude these dependencies to create the same UI for the different sources. If it doesn't clear I can elaborate on it.

    Thanks.
     
  2. Emiles

    Emiles

    Joined:
    Jan 22, 2014
    Posts:
    62
    [Edit] Also found this link https://learn.unity.com/tutorial/be...ces-ui-in-vr-with-the-xr-interaction-toolkit#


    If i remember correctly the demos for Vive, Oculus, etc use custom classes you can drop onto your UI elements. Things like VRClickable and the such like. I assume your talking about those type of platform specific classes that tie your UI implementation into a specific XR platform.

    If so, then my approach as been to first look at how to not use any of those classes at all. Period, consign them to the bin. Oculus framework code is in my oppinion garbage.

    Unity i believe as tried to resolve this issue with their XR input code. You can begin to read about it here https://docs.unity3d.com/Manual/XR.html

    I think that the idea behind Unity XR is that you can use Unity's built in classes to manage inputs such as Hands, controllers etc. Which means you don't have to worry so much about the specific platforms implementation of controllers etc.

    I haven't tried it in anger yet, so not 100% sure how it works, but thats where i'd start looking.

    Regards UI, i currently stick a raycaster object on the hand controllers and have a custom raycast listener on the UIcanvas that triggers the existing events (akin to mouse events). Saves having to add specific classes to the UIelements (buttons, and so forthe). God knows why this isn't the standard. I'm assuming theres a better way, but this is how i do it at the moment and its done just enough to trigger buttons.

    Drop the below script on your EventSystem object. Add to pointerTransforms anything you want to be able to point on a UI with. i.e. could be a laserPointer like object or the vr hand instances.


    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. using UnityEngine.EventSystems;
    5. using UnityEngine.UI;
    6.  
    7.  
    8. // Reference Articles
    9. // https://github.com/tenpn/unity3d-ui/blob/master/UnityEngine.UI/EventSystem/InputModules/PointerInputModule.cs
    10. // https://unfragilecoding.blogspot.com/2019/01/how-to-make-all-gui-components.html
    11. // https://ritchielozada.com/2016/01/01/part-8-creating-a-gaze-based-input-module-for-unity/
    12. // https://medium.com/@naszrai.andras/virtual-reality-ui-with-unity-6e3d02f671e4
    13. // https://answers.unity.com/questions/1248512/graphicraycaster-seems-to-ignore-toggle-elements.html
    14. // https://docs.unity3d.com/Manual/OculusControllers.html
    15. // https://answers.unity.com/questions/945299/how-to-trigger-a-button-click-from-script.html TOGGLE
    16.  
    17. public class VRHand_EventSystem_InputModule : PointerInputModule
    18. {
    19.  
    20.     public LayerMask layerMask;
    21.  
    22.     public List<Transform> pointerTransforms = new List<Transform>();
    23.  
    24.     private List<RaycastResult> lastResults =new List<RaycastResult>();
    25.  
    26.     public LineRenderer debugLineRenderer;
    27.  
    28.     /// <summary>
    29.     /// If the raycast appears off set, check for invisible colliders that will report an incorrect screen position.
    30.     /// </summary>
    31.     public override void Process()
    32.     {
    33.         Vector3 endPosition = Vector3.zero;
    34.  
    35.         if (pointerTransforms.Count > 0)
    36.         {
    37.             // TODO multiple points cancel each other out. Fix
    38.             pointerTransforms.ForEach((transform) =>
    39.             {
    40.                 endPosition = transform.position + (0.1f * transform.forward);
    41.                 Ray ray = new Ray(transform.position, transform.forward);
    42.                 RaycastHit hit;
    43.                 //Debug.DrawRay(manipulator.gameObject.transform.position, manipulator.gameObject.transform.forward);
    44.                 if (Physics.Raycast(ray, out hit, layerMask))
    45.                 {
    46.                     endPosition = new Vector3( hit.point.x, hit.point.y, hit.point.z);
    47.                     // given the world position of the ray cast on a UI, calcult screen position and let EventSystem do everythyinhg else.
    48.                     // TODO remove Camera.main
    49.                     if (Camera.main != null) {
    50.                         //castUIRay(Camera.main.WorldToScreenPoint(hit.point));
    51.                         castUISelectableRay(Camera.main.WorldToScreenPoint(hit.point));
    52.                     }
    53.                 }
    54.  
    55.                 if (debugLineRenderer)
    56.                 {
    57.                     debugLineRenderer.SetPosition(0, transform.position + transform.forward * 0.1f);
    58.                  
    59.                     debugLineRenderer.SetPosition(1, endPosition);
    60.                 }
    61.             });
    62.         }
    63.      
    64.  
    65.     }
    66.  
    67.  
    68.     GameObject lastActivatedTarget;
    69.     GameObject target;
    70.     void castUISelectableRay(Vector3 point)
    71.     {
    72.         // Create Pointer Event
    73.         PointerEventData pointer = new PointerEventData(EventSystem.current);
    74.         pointer.position = new Vector2(point.x, point.y);
    75.         pointer.button = PointerEventData.InputButton.Left;
    76.  
    77.         List<RaycastResult> raycastResults = new List<RaycastResult>();
    78.         EventSystem.current.RaycastAll(pointer, raycastResults);
    79.  
    80.         raycastResults.Sort((x, y) => x.distance.CompareTo(y.distance));
    81.  
    82.         if (raycastResults.Count > 0)
    83.         {
    84.             // Target is being activating -> fade in anim
    85.             if (target == raycastResults[0].gameObject && target != lastActivatedTarget)
    86.             {
    87.              
    88.                 if (target.GetComponent<Selectable>())
    89.                     target.GetComponent<Selectable>().OnPointerEnter(pointer);
    90.  
    91.                 bool indexTrigger = false;
    92. #if  OVRINPUT_USE
    93.                 indexTrigger = OVRInput.GetUp(OVRInput.Button.One, OVRInput.GetActiveController());
    94. #endif
    95.                 // if (Time.time >= endFocusTime && target != lastActivatedTarget) //Input.GetButtonUp("Magic_Leap_Trigger") ||
    96.                 if (Input.GetButtonUp("Fire1") ||  OVRInput.Get(OVRInput.Button.One) || indexTrigger)
    97.                 {
    98.                     lastActivatedTarget = target;
    99.  
    100.                     if (target.GetComponent<ISubmitHandler>() != null)
    101.                         target.GetComponent<ISubmitHandler>().OnSubmit(pointer);
    102.                     else if (target.GetComponentInParent<ISubmitHandler>() != null)
    103.                         target.GetComponentInParent<ISubmitHandler>().OnSubmit(pointer);
    104.                     //else if (target.GetComponentInParent<Slider>() != null)
    105.                     //{
    106.                     //    lastActivatedTarget = null;
    107.                     //    endFocusTime = Time.time + loadingTime;
    108.  
    109.                     //    if (target.GetComponentInParent<Slider>().normalizedValue < 1f - sliderIncrement)
    110.                     //        target.GetComponentInParent<Slider>().normalizedValue += sliderIncrement;
    111.                     //    else if (target.GetComponentInParent<Slider>().normalizedValue != 1)
    112.                     //        target.GetComponentInParent<Slider>().normalizedValue = 1;
    113.                     //    else
    114.                     //        target.GetComponentInParent<Slider>().normalizedValue = 0;
    115.                     //}
    116.                 }
    117.             }
    118.  
    119.             // Target activated -> fade out anim
    120.             else
    121.             {
    122.                 if (target && target.GetComponent<Selectable>())
    123.                     target.GetComponent<Selectable>().OnPointerExit(pointer);
    124.  
    125.                 if (target != raycastResults[0].gameObject)
    126.                 {
    127.                     target = raycastResults[0].gameObject;
    128.                 }
    129.  
    130.             }
    131.         }
    132.  
    133.         // No target -> reset
    134.         else
    135.         {
    136.             lastActivatedTarget = null;
    137.  
    138.             if (target && target.GetComponent<Selectable>())
    139.                 target.GetComponent<Selectable>().OnPointerExit(pointer);
    140.  
    141.             target = null;
    142.  
    143.          
    144.         }
    145.     }
    146.     /// a furst attempt.
    147.     void castUIRay(Vector3 point)
    148.     {
    149.         // Got from examples, seems to work ok.
    150.         PointerEventData pointerEventData = new PointerEventData(EventSystem.current);
    151.  
    152.         // ******************************************
    153.         // Generate screen coords based on virtual point.
    154.         // Instead of mouse position pass in hit point of canvas collider convertered to camera screen space. Essentially a world coordinate as a mouse coordinate relative to camera.
    155.         // ******************************************
    156.         pointerEventData.position = new Vector2(point.x, point.y);
    157.  
    158.         // ******************************************
    159.         // Get Raycast and sort results.
    160.         // ******************************************
    161.         List<RaycastResult> results = new List<RaycastResult>();
    162.         EventSystem.current.RaycastAll(pointerEventData, results);
    163.         results.Sort((x, y) => x.distance.CompareTo(y.distance));
    164.  
    165.  
    166.  
    167.         // ******************************************
    168.         // Clear last selection
    169.         // ******************************************
    170.         if (lastResults.Count > 0)
    171.         {
    172.             lastResults.ForEach((result) =>
    173.             {
    174.                 ExecuteEvents.ExecuteHierarchy(result.gameObject, pointerEventData, ExecuteEvents.deselectHandler);
    175.             });
    176.         }
    177.  
    178.  
    179.  
    180.         // ******************************************
    181.         // Update New hit tartgets.
    182.         // For every result returned, output the name of the GameObject on the Canvas hit by the Ray
    183.         // ******************************************
    184.         foreach (RaycastResult result in results)
    185.         {
    186.             //Debug.Log("Hit " + result.gameObject.name);
    187.             ExecuteEvents.ExecuteHierarchy(result.gameObject, pointerEventData,ExecuteEvents.selectHandler);
    188.  
    189.             // Rather than Execute evetns. just set the selected object to whatever is under the virtual mouse position.
    190.             EventSystem.current.SetSelectedGameObject(result.gameObject);
    191.  
    192.             // Not sure you'd ever want to do this. but hey its possible.
    193.             //if (Input.GetButtonUp("Fire1"))
    194.             //{
    195.             //    ExecuteEvents.ExecuteHierarchy(result.gameObject, pointerEventData, ExecuteEvents.pointerClickHandler);
    196.             //}
    197.  
    198.         }
    199.         lastResults = results;
    200.  
    201.  
    202.  
    203.         // ******************************************
    204.         // Map standard Input events to trigger click
    205.         // ******************************************
    206.         if (Input.GetButtonUp("Fire1") || Input.GetButtonUp("Magic_Leap_Trigger"))
    207.         {
    208.             GameObject target = EventSystem.current.currentSelectedGameObject;
    209.             //Debug.Log("Submit " + target);
    210.             if (target)
    211.                 ExecuteEvents.ExecuteHierarchy(target, pointerEventData, ExecuteEvents.pointerClickHandler);
    212.  
    213.          
    214.         }
    215.  
    216.  
    217.     }
    218. }
    219.  
     
    Last edited: Feb 28, 2021
  3. Emiles

    Emiles

    Joined:
    Jan 22, 2014
    Posts:
    62
    I should also point out that in the Asset Store there are some really good VR input Assets, that done really good grabbing of objects, and UI input management. If you can afford it, its worth considering using a well updated and cohesive system someones already put the time and effort into.
     
  4. yuemco

    yuemco

    Joined:
    Jul 1, 2018
    Posts:
    36
    Absolutely, for HoloLens for example uses its own interactable libs for interactions but Oculus uses different sources.

    I assume there is no template or tutorials for this. I need to dirt my hand :) As you said, regarding runtime, there is no guarantee for AR and VR systems at the same time but thank you for the code block and I will try it out for HoloLens at my first try.

    Have you ever use the same UI for AR and VR systems?
     
  5. Emiles

    Emiles

    Joined:
    Jan 22, 2014
    Posts:
    62
    Sort of, in that i was always irritated by the fact most UI solutions required you to drop specific scripts on components in the UI to detect interactions, and didn't use the built in functionality available for mouse users. Hence the above script allows you to mimick mouse cursor events by transforming an in scene cursor into a mouse point position and trigger the appropriate events. It's by no means complete but certainly works for me.

    Also, a big issue i had with the first version of Hololens was when building a project to work for both Vive and Hololens. None of the packages had been wrapped in a USES_VIVE or USES_HOLOLENS define, so it was a real pain in the ass trying to seperate those packages in a single project. Can't remember what my solution for that was, I think i setup a seperate project as a dll import to the vive and hololens specific projects.