Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

How do I know current navigation is on specific UI Button?

Discussion in 'Scripting' started by leegod, Jul 7, 2020.

  1. leegod

    leegod

    Joined:
    May 5, 2010
    Posts:
    2,472
    So I want to make some bright color focus graphic image positioning on to current UI button which is current selected.

    Assumed now is user using console's controller.

    How to know that? For moving focus graphic image, I should know where and what is current focused UI element from UI button's native navigation system.
     
  2. Antistone

    Antistone

    Joined:
    Feb 22, 2014
    Posts:
    2,836
    EventSystem.current.currentSelectedGameObject

    (Link to legacy documentation because this has mysteriously disappeared from the current version of the documentation. Bad Unity! No biscuit!)
     
    JakesFable likes this.
  3. leegod

    leegod

    Joined:
    May 5, 2010
    Posts:
    2,472
    Thx.
    But for calling function that move highlight image to current selected gameobject, I should know when it occurs.

    Now assuming situation is user is using console controller, should I set if(Input.GetKeyDown("ArrowUpkey")?

    Or because now is under default UIButton's each other navigation is working, don't need to use above if sentence, then how to know UIButton's navigation worked?

    -> of course we know it because UIButton's color changed, but how to know from script? To call custom function.
     
  4. Antistone

    Antistone

    Joined:
    Feb 22, 2014
    Posts:
    2,836
    I don't think there are dedicated events specifically for selection change. You can always just check every frame in Update (and optionally remember the value from the previous frame, if it matters).

    If you want to detect when the player has pressed a button that could cause input change, the input axes that control that are defined in your Standalone Input Module. (Of course, you probably also want to update for things like changing screens. So it's unlikely to be as simple as listening for those inputs.)
     
  5. Elango

    Elango

    Joined:
    Jan 27, 2016
    Posts:
    107
    Not entirely sure if this is what you need but if you need an event on select than you can derive any Selectable class and override OnSelect method. Not 100% sure if it's work for console but for mouse and keyboard arrows it works.
    Code (CSharp):
    1. public class TestButton : Button
    2. {
    3.     [SerializeField] UnityEvent m_OnSelect = new UnityEvent();
    4.  
    5.     public override void OnSelect(BaseEventData eventData)
    6.     {
    7.         base.OnSelect(eventData);
    8.         Debug.Log("OnSelect " + EventSystem.current.currentSelectedGameObject);
    9.         m_OnSelect.Invoke();
    10.     }
    11. }
    And you'll need new editor for it to show event in inspector.
    Code (CSharp):
    1. [CustomEditor(typeof(TestButton), true)]
    2. [CanEditMultipleObjects]
    3. public class TestButtonEditor : ButtonEditor
    4. {
    5.     SerializedProperty m_OnSelectProperty;
    6.  
    7.     protected override void OnEnable()
    8.     {
    9.         base.OnEnable();
    10.         m_OnSelectProperty = serializedObject.FindProperty("m_OnSelect");
    11.     }
    12.  
    13.     public override void OnInspectorGUI()
    14.     {
    15.         base.OnInspectorGUI();
    16.         EditorGUILayout.Space();
    17.         serializedObject.Update();
    18.         EditorGUILayout.PropertyField(m_OnSelectProperty);
    19.         serializedObject.ApplyModifiedProperties();
    20.     }
    21. }
    Or you can just AddListener through script.
     
    Last edited: Jul 7, 2020
  6. leegod

    leegod

    Joined:
    May 5, 2010
    Posts:
    2,472
    as you can see in this video,


    I want green highlight image follow the current activated UI object.

    So when user press up or down arrow key (or console controller's equivalent key), unity should notice it and navigate through UI buttons (UI objects), then how script can notice it?

    Is it [ OnSelect ] function?
     
  7. Elango

    Elango

    Joined:
    Jan 27, 2016
    Posts:
    107
    If you mean the green sparkle then yes. Assign sparkle position method to OnSelect (to all buttons... yes it's not a very convenient way) and get currently selected button by
    EventSystem.current.currentSelectedGameObject

    to reposition it.
    Or you can dispose of m_OnSelect event and instead of
    m_OnSelect.Invoke();

    call your position method directly from button.
     
    Last edited: Jul 8, 2020
  8. leegod

    leegod

    Joined:
    May 5, 2010
    Posts:
    2,472
    Normally, why keyboard arrow up/down/left/right keys not work when playing game in unity editor?

    I set default Unity UI Buttons as-is, not touched, so it basically has Navigation on and Automatic, then it should works with keyboards arrow keys, isn't it?
     
  9. Elango

    Elango

    Joined:
    Jan 27, 2016
    Posts:
    107
    For Navigation to work, it requires that any of the Selectable objects be selected first. EventSystem has a field for the default element "First Selected", but it will work only when the scene starts. In other cases, you need to make sure that the default menu item is selected through the code when opening the UI.
    And if you are using the new Input System, make sure to add arrows to the Actions Asset file.
    Also, I totally forgot about it but EventTrigger component has Select event that doing exact the same as OnSelect so you can use it, no need extra coding here.
     
    Last edited: Jul 9, 2020
  10. leegod

    leegod

    Joined:
    May 5, 2010
    Posts:
    2,472
    I didn't touched Input manager, and I checked, it has default keyboard arrow keys in it.

    Then how to know navigation's default element is what and how to change it?
     
  11. Elango

    Elango

    Joined:
    Jan 27, 2016
    Posts:
    107
    Old Standalone Input Module works out of the box for arrows and gamepad UI navigation, just tested it. New project, two default buttons with bright selected colors, one set as FirstSelected in EventSystem, on play first button is selected, arrows and gamepad works to switch between two.
    Are you sure arrows not working? Default selection color is barely noticeable. Set it to something bright. And make sure something selected first (can do it in-game with mouse click). And make sure "Send Navigation Events" on EventSystem is "On".
    There's only one default selected - EventSystem.current.firstSelectedGameObject. Everything else you have to select on your own with Select(). Or use assets/plugins for more advanced UI system.
     
    Last edited: Jul 10, 2020
  12. leegod

    leegod

    Joined:
    May 5, 2010
    Posts:
    2,472
    FirstSelected in Evensystem is null at default. Yes not works. I have many UI objects in one scene but most of them are hidden (deactivated on start). Is this cause?

    Set it to something bright. -> yes

    And make sure something selected first (can do it in-game with mouse click).
    -> not works even after I click some button with mouse.

    And make sure "Send Navigation Events" on -> yes default on, didn't touched.
     
  13. Elango

    Elango

    Joined:
    Jan 27, 2016
    Posts:
    107
    Try in new project.
    Selecting inactive Selectable won't trigger it's animation (hence you won't see it) but it will work as selected. Although you can force it to change state with OnSelect().
     
    Last edited: Jul 11, 2020
  14. leegod

    leegod

    Joined:
    May 5, 2010
    Posts:
    2,472
    I forcibly set by EventSystem.current.SetSelectedGameObject(FirstEquipObj);
    and checked EventSystem while in play mode,

    It seems navigate another UI scene objects not currently visible in screen.

    How to restrict navigate UI movement within current user can see screen (camera visible) and activated UI gameobject?
     
  15. Elango

    Elango

    Joined:
    Jan 27, 2016
    Posts:
    107
    Try attaching this component to your default button (remove "First Selected" from EventSystem).
    Code (CSharp):
    1. [RequireComponent(typeof(Selectable))]
    2. [DisallowMultipleComponent]
    3. public class FirstSelected : MonoBehaviour
    4. {
    5.     [SerializeField] Selectable prev;
    6.     Selectable selectable;
    7.  
    8.     void Awake()
    9.     {
    10.         selectable = GetComponent<Selectable>();
    11.     }
    12.  
    13.     void OnEnable()
    14.     {
    15.         StartCoroutine(C_Delay());
    16.     }
    17.  
    18.     void OnDisable()
    19.     {
    20.         if (prev != null) prev.Select();
    21.     }
    22.  
    23.     IEnumerator C_Delay()
    24.     {
    25.         yield return new WaitForEndOfFrame();
    26.         selectable.Select();
    27.     }
    28. }
     
  16. leegod

    leegod

    Joined:
    May 5, 2010
    Posts:
    2,472
    I generate equipment represent UI buttons when game start, so its dynamic, and various at numbers by actual equipments, slots user had now.

    If I set this script to each UI prefab, the last generated UI become first focused (Selected) navigation starting point.

    And still navigation go outside current view camera, select outside UI other buttons (not shown in current camera screen)

    How to restrict navigation within camera view?

    Navigation area only exist at navmesh? Not on UI navigation? Very annoying and uncomfortable.
     
    Last edited: Jul 11, 2020
  17. Elango

    Elango

    Joined:
    Jan 27, 2016
    Posts:
    107
    This component is intended for only one default button per active UI panel. If you generate them dynamically then you can just select first button with Select() in your code.
    You want exclude UI elements that outside camera view from selection navigation? I'm not aware of native way to do this... I'm afraid that you have to check if position of each element is outside of view and set them to non-interactable and visa versa.
    Actually... there is small trick that you can use:
    https://docs.unity3d.com/2019.1/Doc...ce/UI.MaskableGraphic-onCullStateChanged.html
    Put mask on whole screen (RectMask2D). This will trigger event on maskable elements.
    And for buttons:
    Code (CSharp):
    1. [RequireComponent(typeof(Selectable))]
    2. [DisallowMultipleComponent]
    3. public class VisibilityTest : MonoBehaviour
    4. {
    5.     Selectable selectable;
    6.     void Awake()
    7.     {
    8.         selectable = GetComponent<Selectable>();
    9.         GetComponent<MaskableGraphic>().onCullStateChanged.AddListener((bool hidden) => CullStateChanged(hidden));
    10.     }
    11.    
    12.     void CullStateChanged(bool hidden)
    13.     {
    14.         selectable.interactable = !hidden;
    15.     }
    16. }
     
    Last edited: Jul 11, 2020
  18. leegod

    leegod

    Joined:
    May 5, 2010
    Posts:
    2,472
    Thanks.
    So I think not only outside camera UIs, but also UIs that is inside camera but below of current viewing UI's layer, also affect to navigation.

    So for disabling this, interactible un-check? With canvas group and manually by code?
     
  19. Elango

    Elango

    Joined:
    Jan 27, 2016
    Posts:
    107
    Toggling interactable is simplest way to exclude element from navigation.
    If you want exclude whole group of elements you can put CanvasGroup component on panel and toggle interactable on it instead of doing it for each element individually.
    Also you can control position from which onCullStateChanged be triggered if you put small transparent image inside button and use it as a trigger instead of whole button image.
    Code (CSharp):
    1. [RequireComponent(typeof(Selectable))]
    2. [DisallowMultipleComponent]
    3. public class VisibilityTest : MonoBehaviour
    4. {
    5.     [SerializeField] MaskableGraphic maskable;
    6.     Selectable selectable;
    7.  
    8.     void Awake()
    9.     {
    10.         selectable = GetComponent<Selectable>();
    11.         maskable.onCullStateChanged.AddListener((bool hidden) => CullStateChanged(hidden));
    12.     }
    13.    
    14.     void CullStateChanged(bool hidden)
    15.     {
    16.         selectable.interactable = !hidden;
    17.     }
    18. }
    Image must be assigned to maskable field in inspector.
     
  20. leegod

    leegod

    Joined:
    May 5, 2010
    Posts:
    2,472
    What is MaskableGraphic? I can't find it. When I addcomponent and type "Mask", then only I can see Mask and Rect Mask 2D, and both of them cause nullreference error from this line,

    GetComponent<MaskableGraphic>().onCullStateChanged.AddListener((bool hidden) =>
     
  21. leegod

    leegod

    Joined:
    May 5, 2010
    Posts:
    2,472
    I feel it is more faster way that manually code myself that select and focus specific UI buttons and navigate through them only from things that I want...

    I just want navigation field scope limit option, but there seems none.

    I have many UI layers (Adventure mode HUD, character powerups, equips, items, inventory, story, options),

    I just want only current screen camera taken UIs only focused and not navigation go outside or below (turned on and activated but not seen because covered by current UIs), but this seems hard..
     
  22. leegod

    leegod

    Joined:
    May 5, 2010
    Posts:
    2,472
    And I don't understand how to use this and for what?

    And when this (onCullStateChanged) is called? When it is Not visible (covered by upward UI) from UI Camera?
     
  23. Elango

    Elango

    Joined:
    Jan 27, 2016
    Posts:
    107
    MaskableGraphic is a base class for graphics components like Image or Text (they have Maskable bool field in inspector).
    RectMask2D must be on root panel. VisibilityTest on each buttons within this panel.
    [SerializeField] MaskableGraphic maskable;
    must be assigned to any MaskableGraphic component (Image or Text). It position/size will affect when event will be called. Event called when state changed (became visible or invisible).
     
    Last edited: Jul 12, 2020
  24. leegod

    leegod

    Joined:
    May 5, 2010
    Posts:
    2,472
    Is this script should attached to every UI buttons? Can it be attached to just topmost object that has Canvas Group and control all childrens?
     
  25. Elango

    Elango

    Joined:
    Jan 27, 2016
    Posts:
    107
    Yes. With slight changes:
    Code (CSharp):
    1. [RequireComponent(typeof(CanvasGroup))]
    2. [DisallowMultipleComponent]
    3. public class PanelVisibility : MonoBehaviour
    4. {
    5.     [SerializeField] MaskableGraphic maskable;
    6.     CanvasGroup cg;
    7.  
    8.     void Awake()
    9.     {
    10.         cg = GetComponent<CanvasGroup>();
    11.         maskable.onCullStateChanged.AddListener(CullStateChanged);
    12.     }
    13.  
    14.     void CullStateChanged(bool hidden)
    15.     {
    16.         cg.interactable = !hidden;
    17.     }
    18. }
     
    Last edited: Jul 12, 2020
  26. leegod

    leegod

    Joined:
    May 5, 2010
    Posts:
    2,472
    visibility.PNG

    From Canvas3 object, I am working on [SealManageUI] and below,

    all problem I posted here is caused by another UI buttons under other mothers like "Adventure-Related", "Character-Related", "LobbyScene".

    Those buttons hinders my working UI (SealManageUI) and its children buttons's navigation each other.

    My working UI (SealManageUI) covers all camera view.

    visibility2.PNG

    Problem is, if I press up-arrow key from topmost UI button (green rectangle box here), another UI button become selected object that is outside of this view, it is under other UI mother, "LobbyScene".

    So in this case, I should attach your script to all UI mothers like "BattleRelatedUI, Adventure-Related, Character-Related, etc under Canvas3)?

    Then what "maskable" variable should be?
     
  27. Elango

    Elango

    Joined:
    Jan 27, 2016
    Posts:
    107
    Yes.
    Maskable must be transparent Image (with Raycast Target disabled and Maskable enabled) inside Panel that has PanelVisibility script.
    It position and size will affect when event will be triggered.
    On Canvas3 must be placed RectMask2D.

    The rest of the elements (Images/Text) are not required to be set "Maskable" bool as enabled in inspector if they don't need it.
     
  28. leegod

    leegod

    Joined:
    May 5, 2010
    Posts:
    2,472
    hm...not work..

    I set RectMask2D at Canvas3, and like this at LobbyScene,

    visibility3.PNG

    But when game start, now viewing UI is SealManageUI, so not LobbyScene's buttons, but its Canvas Group component still have interactable checked.

    Tested again with transparent image's size as 2000 width and 1000 height, still interactable being checked.
     
  29. Elango

    Elango

    Joined:
    Jan 27, 2016
    Posts:
    107
    But LobbyScene right in center of screen... Didn't you wanted to exclude from selection what outside of view?
     
  30. leegod

    leegod

    Joined:
    May 5, 2010
    Posts:
    2,472
    LobbyScene is in center of screen but below layer of SealManageUI.

    Yes I want exclude from outside of view, but also exclude UIs that are below layer of current UI.
     
  31. Elango

    Elango

    Joined:
    Jan 27, 2016
    Posts:
    107
    Welp... this method only works when object moved outside/inside of screen.
    For layers you have to do something else...
     
  32. leegod

    leegod

    Joined:
    May 5, 2010
    Posts:
    2,472
    So then I should move all other UI mothers position outside of camera and only current UI being center?
     
  33. Elango

    Elango

    Joined:
    Jan 27, 2016
    Posts:
    107
    If you know exact moment when you want enable/disable this panels you can just set CanvasGroup.interactable through code on this panels.
     
  34. leegod

    leegod

    Joined:
    May 5, 2010
    Posts:
    2,472
    Yes that is one way, but brutal way... I should set all other 5~7 UI's disable when some one become enable.

    If UI become increasing, all code should be revised...

    So then moving all other UIs outside the camera is also manual coding, so same brutal way.

    If this is only way, I should. But uncomfortable remains.
     
  35. Elango

    Elango

    Joined:
    Jan 27, 2016
    Posts:
    107
    Mask component cost a performance so if you can do it without it, it would be better solution.
     
  36. leegod

    leegod

    Joined:
    May 5, 2010
    Posts:
    2,472
    Why onCullStateChanged only for position outside of camera?

    Positioned center but below current UI that covered whole screen, then it is same result (became visible or invisible) from User's eye, Camera's eye.

    Are there no solution for dealing with this layer problem?
     
  37. Elango

    Elango

    Joined:
    Jan 27, 2016
    Posts:
    107
    It's not works from camera but from mask rect size.
     
  38. leegod

    leegod

    Joined:
    May 5, 2010
    Posts:
    2,472
    canvas3.PNG

    Yes canvas3 is full screen size, so then RectMask2D too maybe.
     
  39. leegod

    leegod

    Joined:
    May 5, 2010
    Posts:
    2,472
    How about using Raycast center when some UI window opened, from the raycast results, can we know the first hit result?

    Then make that first hit UI object's topmost mother's canvas group interactable, and make all other UI to un-interactable?
     
  40. Elango

    Elango

    Joined:
    Jan 27, 2016
    Posts:
    107
    You can make middle-object between canvas/panels with mask and set it to different size but i don't see how it may help...
    How are you moving panels above/below? Why raycasting center? Is the active panel moving to the center?
     
  41. leegod

    leegod

    Joined:
    May 5, 2010
    Posts:
    2,472
    I don't move panels above/below. Above/Below position is static after UI is created.

    UIs just opened when it is needed, and normally specific UI covered whole screen rect. Equipment, Inventory, Character, Seals, etc.
     
  42. Elango

    Elango

    Joined:
    Jan 27, 2016
    Posts:
    107
    So they just sorted in hierarchy and enabled/disabled? And some of them active at the same time over another panels?
    Then you can use OnEnable/OnDisable to get all CanvasGroup in canvas and set interactable. Although for disable you have to know which panel was active before so you probably need to store it and activate it back OnDisable.
     
  43. leegod

    leegod

    Joined:
    May 5, 2010
    Posts:
    2,472
    Yes maybe it is only reasonable way for now..

    What is OnEnable/OnDisable ? Isn't it called only when game object become SetActive(true/false)?
     
  44. Elango

    Elango

    Joined:
    Jan 27, 2016
    Posts:
    107
    Yes, when enabled(inc instantiated) and disabled(inc destroyed).
    Here's two examples.
    "A" without raycast. No additional settings, just put in on panels. It doesn't store prev panel. The principle is the same as Raycast, topmost panel will be interactable.
    Code (CSharp):
    1. [RequireComponent(typeof(CanvasGroup))]
    2. [DisallowMultipleComponent]
    3. public class CanvasGroupCheckA : MonoBehaviour
    4. {
    5.     static CanvasGroup[] cg;
    6.  
    7.     void Awake()
    8.     {
    9.         if (cg == null) cg = transform.root.GetComponentsInChildren<CanvasGroup>(true);
    10.     }
    11.  
    12.     void OnEnable()
    13.     {
    14.         Check();
    15.     }
    16.  
    17.     void OnDisable()
    18.     {
    19.         Check();
    20.     }
    21.  
    22.     void Check()
    23.     {
    24.         bool topmost = true;
    25.         for (int i = cg.Length - 1; i > -1; i--)
    26.         {
    27.             if (cg[i].gameObject.activeInHierarchy)
    28.             {
    29.                 cg[i].interactable = topmost;
    30.                 topmost = false;
    31.             }
    32.         }
    33.     }
    34. }
    "B" is raycast version. Slighly more complex. It requires coroutine to wait for UI finish drawing and since coroutine cannot be started on disabled object it also use canvas as target for disabled coroutine. Also, obviously, it requires some raycastable object in center of the screen (in this case Image with "Raycast Target" enabled must be attached to the panel).
    Code (CSharp):
    1. [RequireComponent(typeof(CanvasGroup))]
    2. [RequireComponent(typeof(Image))]
    3. [DisallowMultipleComponent]
    4. public class CanvasGroupCheckB : MonoBehaviour
    5. {
    6.     GraphicRaycaster gr;
    7.     PointerEventData ed;
    8.     EventSystem es;
    9.  
    10.     private void Awake()
    11.     {
    12.         gr = GetComponentInParent<GraphicRaycaster>();
    13.         es = EventSystem.current;
    14.     }
    15.  
    16.     void Raycast()
    17.     {
    18.         ed = new PointerEventData(es);
    19.         ed.position = new Vector2(Screen.width / 2, Screen.height / 2);
    20.         List<RaycastResult> results = new List<RaycastResult>();
    21.         gr.Raycast(ed, results);
    22.         bool topmost = true;
    23.         for (int i = 0; i < results.Count; i++)
    24.         {
    25.             var cg = results[i].gameObject.GetComponent<CanvasGroup>();
    26.             if(cg != null)
    27.             {
    28.                 cg.interactable = topmost;
    29.                 topmost = false;
    30.             }
    31.         }
    32.     }
    33.  
    34.     void OnEnable()
    35.     {
    36.         StartCoroutine(C_Raycast());
    37.     }
    38.  
    39.     void OnDisable()
    40.     {
    41.         if (gr.gameObject.activeInHierarchy) gr.StartCoroutine(C_Raycast());
    42.     }
    43.  
    44.     IEnumerator C_Raycast()
    45.     {
    46.         yield return new WaitForEndOfFrame();
    47.         Raycast();
    48.     }
    49. }
    Keep in mind that if you have other CanvasGroup components in canvas then you'll need to change check from <CanvasGroup> to this component itself <CanvasGroupCheckA/B> (for both variants).
     
  45. leegod

    leegod

    Joined:
    May 5, 2010
    Posts:
    2,472
    So version A, its premise is there are manual code that SetActive(true/false) gameobject that has canvas group?

    On the other hand, version B, does not need to be turn on/off other canvas groups,
    but just only make sure that current active CanvasGroup is most below positioned in unity hierarchy?
    (Then it will be topmost viewing UI now and raycast will detect it first?)
     
  46. Elango

    Elango

    Joined:
    Jan 27, 2016
    Posts:
    107
    No, they both rely on enable/disable of the panel gameobject.
    In both cases topmost active panel (lowest in the hierarchy) will become interactable, and the other active panel will become non-interactive.
     
  47. leegod

    leegod

    Joined:
    May 5, 2010
    Posts:
    2,472
    Then simple version A is most good?
     
  48. Elango

    Elango

    Joined:
    Jan 27, 2016
    Posts:
    107
    I didn't notice a significant difference in profiler between them (like 5 panels with 5 button in each), perhaps with more complex setup it will appear. You should see it for yourself. I would stick with "A" for now (not big fan of coroutines and raycasts).
     
  49. leegod

    leegod

    Joined:
    May 5, 2010
    Posts:
    2,472
    I tested with version A, but its premise is very hard to achieve...

    Manual coding of UI's gameobject SetActive(true/false) is very hard coding and almost impossible to manage.

    Already there are many sub object that has CanvasGroup, turn only one of them and all other off, is already hard management work, but also if some more top layer UI should pop up (pop up window, system message, etc), it need to be turn on again. If this UI is children of some parent UI that is already turned off, then be complicated..
     
  50. Elango

    Elango

    Joined:
    Jan 27, 2016
    Posts:
    107
    As i said <CanvasGroup> can be replaced with <CanvasGroupCheckA> to exclude other CanvasGroup elements. If you already have event for switching panels you can hook Check() method there instead of OnEnable/OnDisable. You can make it static so no reference needed.