Unity Navigation System

Summary

A static class that handles Breadcrumbing for easy menu stacking and navigation. Easy to call/use from any input system as well as with in-game buttons.

Full script below (Creative Commons 4.0)

Script

The MenuBase which should be on all canvases you inted to open. Upon enabling an object of type MenuBase, said base will be added to the list of ActiveMenus

//Copyright ArmanDoesStuff 2020
using UnityEngine;
using UnityEngine.UI;

namespace ArmanDoesStuff.Utilities
{
    public class MenuBase : MonoBehaviour
    {
        public Selectable firstSelected;

        public virtual void ManualClose()
        {
            NavigationHelper.CloseMenu();
        }

        public virtual void Close()
        {
            Destroy(gameObject);
        }

        protected virtual void OnEnable()
        {
            this.OpenMenu();
        }
        public void DelayedSelect()
        {
            this.Invoke(() =>
            {
                firstSelected.Select();  //doing this on the same frame doesn't always highlight the button (when spawned from addressable system)
            });
        }
    }
}

The main NavigationHelper which forms the core of this technique

//Copyright ArmanDoesStuff 2020
using System.Collections.Generic;
using UnityEngine.EventSystems;
using UnityEngine.UI;

namespace ArmanDoesStuff.Utilities
{
    public static class NavigationHelper
    {
        public static List<MenuBase> activeMenus = new List<MenuBase>();
        public static List<Selectable> breadcrumbs = new List<Selectable>();

        public static bool AnyMenusOpen { get => activeMenus.Count > 0; }


        public static void OpenMenu(this MenuBase _menuBase)
        {
            if (EventSystem.current.currentSelectedGameObject != null)
                breadcrumbs.Add(EventSystem.current.currentSelectedGameObject.GetComponent<Selectable>());
            activeMenus.Add(_menuBase);

            EventSystem.current.SetSelectedGameObject(null);
            _menuBase.DelayedSelect();
        }

        //Also called by pressing Back (circle)
        public static void CloseMenu(bool _crumb = true)
        {
            if (!AnyMenusOpen)
                return;
            EventSystem.current.SetSelectedGameObject(null);
            int lastIndex = activeMenus.Count - 1;
            activeMenus[lastIndex].Close();
            activeMenus.RemoveAt(lastIndex);

            if (!_crumb)
                return;

            lastIndex = breadcrumbs.Count - 1;
            if (breadcrumbs.Count > 0)
            {
                if (breadcrumbs[lastIndex] != null)
                {
                    breadcrumbs[lastIndex].Select();
                    breadcrumbs.RemoveAt(lastIndex);
                    return;
                }
                else
                {
                    breadcrumbs.RemoveAt(lastIndex);
                }
            }

            if (EventSystem.current.firstSelectedGameObject != null)
            {
                EventSystem.current.firstSelectedGameObject.GetComponent<Selectable>().Select();
            }
        }

        public static void CloseAllMenus()
        {
            while (AnyMenusOpen)
            {
                CloseMenu();
            }
        }
    }
}

A snippet of ArmanLibrary with the Invoke extension method for MonoBehaviour (used in DelayedSelect)

//Invoke Lambda Expression (next frame or with delay)
//https://forum.unity.com/threads/tip-invoke-any-function-with-delay-also-with-parameters.978273/
public static void Invoke(this MonoBehaviour mb, Action f, float delay = 0)
{
    mb.StartCoroutine(InvokeRoutine(f, delay));
}

private static IEnumerator InvokeRoutine(Action f, float delay)
{
    if (delay > 0)
    {
        yield return new WaitForSeconds(delay);
    }
    else
    {
        yield return null;
    }
    f();
}

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.