Creating a Mouseless Context Menu in Unity

I was prototyping a 2D Side Scroller game in Unity a week ago and started implementing an Inventory System (Equipments, slots, etc…).
Unfortunately, Unity didn’t have a Context Menu out of the box so I searched a bit and found this, which helped me get an idea of how to implement one as well as make it completely mouseless, since my game was mouseless itself.

Preparing the UI

A panel for our Context Menu

The first step is to create a panel (in a canvas as usual) for our Context Menu, you can customize it however you like.
The panel will have to have these components to save us some code:

A VerticalLayoutGroup so that the items take the whole width of the menu

A VerticalLayoutGroup so that the items take the whole width of the menu

A ContentSizeFitter with VerticalFit set to Min Size so that the Context Menu's height is always the sum of its children

A ContentSizeFitter with VerticalFit set to Min Size so that the Context Menu's height is always the sum of its children

A button prefab for our Context Menu Items

Next, we’ll have to create a prefab of how the items will be shown in the menu.
Using a Button makes things easier since it has a onClick.

Layout Element

Layout Element

The button has to have a LayoutElement set to a height you want, since we’re using a VerticalLayoutGroup in its parent.

Scripts

Items

We will represent each item in the menu with the following class:

using System;

namespace Assets.Scripts.ContextMenu
{
    public class ContextMenuItem
    {

        #region Properties

        // The item's text/name
        public string Text { get; }

        // What will this menu item do once it's clicked
        public Action Action { get; }

        #endregion

        public ContextMenuItem(string text, Action action)
        {
            Text = text;
            Action = action;
        }

    }
}

We’ll need a couple of fields:

// The prefab we created previously
[SerializeField] private Button _itemButtonPrefab;
// To position the menu where it was clicked/pressed
private RectTransform _panelRectTransform;
// To help us go back to our previous action
// For example: Navigating items in an Inventory,
// once we're done, we need to re-select the slot
// we were in so that the player can keep navigating.
private Selectable _lastSelectableObject;

Unity methods:

private void Awake()
{
    // Since this is a Singleton.
    _mInstance = this;
    // Assuming you're using the script on the Context Menu
    // panel we created earlier.
    _panelRectTransform = GetComponent<RectTransform>();
    // Hide the menu by default.
    gameObject.SetActive(false);
}

private void Update()
{
    // Hide menu if Escape is pressed
    if (gameObject.activeSelf && Input.GetKeyDown(KeyCode.Escape))
    {
        Hide();
    }
}

Showing the menu:

public void Show(List<ContextMenuItem> items, Vector2 position)
{
    // If the menu is already open
    if (gameObject.activeSelf || items.Count == 0)
        return;

    // Save the object that was selected before we open the menu
    _lastSelectableObject = EventSystem.current.currentSelectedGameObject.GetComponent<Selectable>();
    Button firstBtn = null;

    // Set new items
    foreach (var item in items)
    {
        var btn = Instantiate(_itemButtonPrefab);
        btn.GetComponentInChildren<Text>().text = item.Text;

        // For future use
        if (firstBtn == null)
            firstBtn = btn;

        // Add a listener to the button's click event to call the item's action, then hide the menu
        btn.onClick.AddListener(() =>
        {
            item.Action();
            Hide();
        });

        // Set the item's parent to the menu
        btn.transform.SetParent(gameObject.transform);
    }

    // Show menu in position
    _panelRectTransform.anchoredPosition = position;
    gameObject.SetActive(true);

    // Select first button after showing menu by default
    firstBtn.Select();
}

Hiding the menu:

public void Hide()
{
    if (!gameObject.activeSelf)
        return;

    // Clear items
    foreach (Transform child in gameObject.transform)
        Destroy(child.gameObject);

    // Hide
    gameObject.SetActive(false);

    // Re-select previous object
    _lastSelectableObject?.Select();
}

I got a problem when hiding the menu directly if the player presses Space to click items.
Since Space was the key the player needed to press on an Item slot to show the menu, it would hide the menu and show it directly.

To fix this, I ended up hiding the menu the next frame (after the Space event is fired):

public void Hide()
{
    if (!gameObject.activeSelf)
        return;

    // Clear items
    foreach (Transform child in gameObject.transform)
        Destroy(child.gameObject);

    // Hide the next frame
    StartCoroutine(CloseNextFrame());

    // Re-select previous object
    _lastSelectableObject?.Select();
}

private IEnumerator CloseNextFrame()
{
    yield return null;
    gameObject.SetActive(false);
}

Usage Example

I will show you how I personally use this for my Inventory System.

I have a ItemSlotBehaviour that handles each slot in the inventory.
When the player presses Space:

private void Update()
{
    // If the slot contains an item and is currently selected/highlighted
    if (_item == null || currentSelectionState != SelectionState.Highlighted)
        return;

    if (Input.GetKeyDown(KeyCode.Space))
    {
        ContextMenuBehaviour.Instance.Show(GetContextMenuItems(), transform.position);
    }
}

GetContextMenuItems() simply returns a list of ContextMenuItem depending on the type of item (usable, equipment, others…).

For example, if the item is an equipable:

if (_item is EquipableItem equipableItem)
{
    items.Add(new ContextMenuItem("Equip", () => InventoryBehaviour.Instance.EquipItem(equipableItem)));
}

The action is simply to equip the item using the Inventory System.

Conclusion

I hope this post helps someone in the future and I’d like to thank Experion again for his answer in Stackoverflow as his idea was my starting point.
If you have any suggestions, feel free to comment/message me.
See you around!

Zanid Haytam Written by:

Zanid Haytam is an enthusiastic programmer that enjoys coding, reading code, hunting bugs and writing blog posts.

comments powered by Disqus