Scrollable Menu in Unity with button or key controller

 by • Reading Time: 14 minutes

Introduction

In this tutorial I’m going to show the implementation of a button- or key-controlled scrollable menu in Unity. This type of menu is fairly common and popular in RPG games especially on console platforms during turn-based battles. The reason for this is that during the story lines the player often collects a great amount of items and learns thousands of magic spells. To that end, they need to be nicely organised in a menu so that the player is aware of their existence in his inventory.

The example of scrollable menu in RPG genre game

However, due to the vast number of potential collectables it is preferable to have a panel that allows a flexible way of fitting all of them inside its boundaries. By doing so, we want to avoid a situation in which we have to stretch various panels that would occupy entire screen to list of all of our items. One of the possible ways to achieve that is the scrollable menu, which we are going to implement in this post!

Table of contents:

  1. The scrollable menu wrapper prefab in Unity
  2. The menu button prefab
  3. Scripts configuration of scrollable menu in Unity
  4. The dynamic population of the scrollable menu in Unity (optional)

1. The scrollable menu wrapper prefab in Unity

For the purpose of this article I’m going to focus on creating the “battle menu” with multiple options. This menu will display maximum 3 positions from a long list of actions the player can execute during a battle. Let’s start by creating a wrapper that will contain the list.

  • Right-click inside the “Hierarchy” panel and select “UI -> Canvas” to create the foundation.
  • Create a “Panel” and parent it to newly-created “Canvas“. Resize it to your desired width and height. This will be the size of the “Battle Menu”.
  • Inside the Panel create yet another Panel and add a “Mask” component to it. We are going to use it to hide the excessive positions on the list at any given moment.
  • Inside the Panel with a “Mask” component from previous step add one more Panel. Add “Vertical Group Layout” component to it. We are going to use it to evenly lay out the menu buttons.
  • Select “Upper Left” for “Child Alignment” and make sure that both “Width” and “Height” checkboxes are checked next to “Child Force Expand” field. Please refer to official documentation for more details.
Vertical Layout Group components settings for battle menu

The structure of the UI for battle menu wrapper should now look like this:

The hierarchy of battle menu UI assets and components

Now that we have the base structure of the battle menu UI we need to write a script that will control the selection. I will attach this script to the Panel containing the “Vertical Layout Group“. Let’s get right to it!

The script controlling the menu list indexing

The menu button controller script will keep track of the current position on the list currently selected by the player. I will define few global fields that shall be used to control the index, the size of the list and whether the player has pressed the button or key to change the position. In order to scroll the panel and display the correct options at any given time I’m also going to need a reference to “RectTransform” of the inner-most Panel asset.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MenuButtonController : MonoBehaviour
{
    public int index;
    public int maxIndex;
    [SerializeField] bool keyDown;
    [SerializeField] RectTransform rectTransform;

    void Start()
    {
        rectTransform = GetComponent<RectTransform>();
    }
}

Now we have to make sure that we capture the keystrokes or button presses coming from the player.

Changing the position on the list with buttons

I will add 3 more boolean fields that will be either true or false depending on whether the player pressed up, down or “confirm” buttons or keys. After that I’ll add the listener functions. These will be executed when appropriate button or key is pressed by the player. I need to declare two functions per button: one for when the player pressed the button and one for when the player releases it.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MenuButtonController : MonoBehaviour
{
    public int index;
    public int maxIndex;
    [SerializeField] bool keyDown;
    [SerializeField] RectTransform rectTransform;
    bool isPressUp, isPressDown, isPressConfirm;

    void Start()
    {
        rectTransform = GetComponent<RectTransform>();
        isPressUp = isPressDown = false;
    }

    public void onPressUp()
    {
        isPressUp = true;
    }

    public void onReleaseUp()
    {
        isPressUp = false;
    }

    public void onPressDown()
    {
        isPressDown = true;
    }

    public void onReleaseDown()
    {
        isPressDown = false;
    }

    public void onPressConfirm()
    {
        isPressConfirm = true;
    }

    public void onReleaseConfirm()
    {
        isPressConfirm= false;
    }
}

Please note that for the sake of this tutorial I’m placing the “up” and “down” buttons methods here. However, for practical reasons and depending on your game scripts structure these may be defined elsewhere. The most important thing here is that these methods need to be assigned to UI elements that will “listen” to player’s input. I will now assign them to buttons using the “EventTrigger” component, like so:

Assigning listening methods with Event Trigger component

After that I have to make sure to capture the intention of the player – whether he wants to move the selection up or down on the list. I’ll do it by declaring one more field of integer type. I’ll assign 1 to it when I press up. Analogically, I’ll assign -1 to this field every time I press down. Finally, I’ll assign 0 if no movement is registered.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MenuButtonController : MonoBehaviour
{
    public int index;
    public int maxIndex;
    [SerializeField] bool keyDown;
    [SerializeField] RectTransform rectTransform;
    bool isPressUp, isPressDown, isPressConfirm;
    int VerticalMovement;

    void Start()
    {
        rectTransform = GetComponent<RectTransform>();
        isPressUp = isPressDown = false;
    }

    void Update()
    {
        if (isPressUp)   VerticalMovement = 1; 
        if (isPressDown) VerticalMovement = -1;
        if (!isPressUp &amp;&amp; !isPressDown) VerticalMovement = 0;
    }

    public void onPressUp()
    {
        isPressUp = true;
    }

    public void onReleaseUp()
    {
        isPressUp = false;
    }

    public void onPressDown()
    {
        isPressDown = true;
    }

    public void onReleaseDown()
    {
        isPressDown = false;
    }

    public void onPressConfirm()
    {
        isPressConfirm = true;
    }

    public void onReleaseConfirm()
    {
        isPressConfirm = false;
    }
}

Now we have to write the main logic of transitioning between different positions on the list.

The core of scrollable menu functionality in Unity

In order to transition from one position to the next on the list we have to keep in mind two things: the current position and the state of the RectTransform offsets. In addition, we want to move to the next position upon button press and keep it selected. I’m going to use all the declared fields and variables in order to ensure the natural flow of the selection procedure. Everything will be executed in the Update() method of the MenuButtonController script. First I’m checking if the player has pressed either up or down to select the desired option. After that I’m making sure that the movement will occur only once to select next or previous option on the list. The keyDown boolean works like a blocker that prevents going “too far” down the list in one go.

...
void Update()
{
    if (isPressUp)   VerticalMovement = 1; 
    if (isPressDown) VerticalMovement = -1;
    if (!isPressUp &amp;&amp; !isPressDown) VerticalMovement = 0;

    if (VerticalMovement != 0)
    {
        if(!keyDown)
        {
            keyDown = true;
        } 
    } else {
        keyDown = false;
    }
}
...

After that I’m going to check whether the movement needs to be made up or down. This depends on the value held by VerticalMovement variable. Then I’ll adjust the value of the current index value. If the boundary elements on the list are selected and the player chooses to move to position beyond it then the element on the opposite side of the list will become the current one. For example, if the current selection is the last on the list and the player moves down then the first option is going to be selected.

...
void Update()
{
    if (isPressUp)   VerticalMovement = 1; 
    if (isPressDown) VerticalMovement = -1;
    if (!isPressUp &amp;&amp; !isPressDown) VerticalMovement = 0;

    if (VerticalMovement != 0)
    {
        if(!keyDown)
        {
            if(VerticalMovement < 0)
            {
                if (index < maxIndex)
                {
                    index++;
                }
                else
                {
                    index = 0;
                }

            } else if(VerticalMovement > 0) {
                
                if (index > 0)
                {
                    index--;
                } 
                else
                {
                    index = maxIndex;
                }
            }

            keyDown = true;
        } 
    } else {
        keyDown = false;
    }
}
...

Scrolling effect with RectTransform offsets

After that I’ll set the correct offset of the RectTransform component to display the correct portion of the list at any given time. The amount by which I need to make an offset depends on the height of individual button position inside the list. In my case the height of the single list element is 64 and that is the value which I’m going to use. Of course I’m going to ensure that the offsets are reset when the first or last position is selected by the player.

...
void Update()
{
    if (isPressUp)   VerticalMovement = 1; 
    if (isPressDown) VerticalMovement = -1;
    if (!isPressUp &amp;&amp; !isPressDown) VerticalMovement = 0;

    if (VerticalMovement != 0)
    {
        if(!keyDown)
        {
            if(VerticalMovement < 0)
            {
                if (index < maxIndex)
                {
                    index++;
                    if (index > 1 &amp;&amp; index < maxIndex)
                    {
                        rectTransform.offsetMax -= new Vector2(0, -64);
                    }
                }
                else
                {
                    index = 0;
                    rectTransform.offsetMax = Vector2.zero;
                }

            } else if(VerticalMovement > 0) {
                
                if (index > 0)
                {
                    index--;
                    if(index < maxIndex - 1 &amp;&amp; index > 0)
                    {
                        rectTransform.offsetMax -= new Vector2(0, 64);
                    }
                } 
                else
                {
                    index = maxIndex;
                    rectTransform.offsetMax = new Vector2(0, (maxIndex - 2) * 64);
                }
            }

            keyDown = true;
        } 
    } else {
        keyDown = false;
    }
}
...

Line 41 requires more explanation. The battle menu in its current form contains the maximum 3 visible elements from the list at any given time. That means that when the last element is selected I want to set the offset so that there is no empty space displayed below it. To ensure that, I have to calculate the difference between the maxIndex and the number of ‘visible’ options minus 1. Then I need to multiply the result by the height of a single element (in my case 64).

 vector.y = (maxIndex - (visibleOptions - 1)) * height

The main button controller script is done! However, I still need a prefab representing a single element on the list. I’ll describe it in the next chapter but first let me quickly show you how you can change the position on the list with keys.

Changing the position on the list with keys

In order to change the position on the list with keystrokes (or any kind of peripheral keys) we need to first adjust the Project Settings. Go to “Edit -> Project Settings…” and select “Input Manager”. Inside it, set “Gravity” to 1000 and “Dead” to 0.001 for “Vertical” movement.

Project Settings for Vertical movement

This will ensure that the “Vertical” input coming from the player immediately changes value to 1, 0 or -1 when corresponding button is pressed. After that all we have to do is to adjust the MenuButtonController function so that it reads that input.

void Update()
{
    if (isPressUp)   VerticalMovement = 1; 
    if (isPressDown) VerticalMovement = -1;
    if (!isPressUp &amp;&amp; !isPressDown) VerticalMovement = 0;

    if (Input.GetAxis("Vertical") != 0 || VerticalMovement != 0)
    {
        if(!keyDown)
        {
            if(Input.GetAxis("Vertical") < 0 || VerticalMovement < 0)
            {
                if (index < maxIndex)
                {
                    index++;
                    if (index > 1 &amp;&amp; index < maxIndex)
                    {
                        rectTransform.offsetMax -= new Vector2(0, -64);
                    }
                }
                else
                {
                    index = 0;
                    rectTransform.offsetMax = Vector2.zero;
                }

            } else if(Input.GetAxis("Vertical") > 0 || VerticalMovement > 0) {
                
                if (index > 0)
                {
                    index--;
                    if(index < maxIndex - 1 &amp;&amp; index > 0)
                    {
                        rectTransform.offsetMax -= new Vector2(0, 64);
                    }
                } 
                else
                {
                    index = maxIndex;
                    rectTransform.offsetMax = new Vector2(0, (maxIndex - 2) * 64);
                }
            }

            keyDown = true;
        } 
    } else {
        keyDown = false;
    }
}

I’ll now move on to the menu button prefab. I’ll use it to represent an individual position on our scrollable menu list!

I’ll now move on to defining a prefab representing a single element on the list. It is entirely up to you how you wish to structure it. However, please keep in mind that whatever design you’ll go for you have to take note of its height and update the MenuButtonController script described earlier accordingly.

  • Start by creating a Panel, which is going to be a wrapper for a single element position on the list. To comply with the MenuButtonController script defined in previous chapter, I’ll set its height to 64.
  • Add whatever graphical and text elements you wish to have inside the Panel. I’ll add an Image and TextMeshPro.
  • Assign “arrow” graphics to Image so that it points to currently selected position on the list.

Once we have created this foundation we need few animations to bring our menu to life.

The “position” animations

We are going to need 3 animations corresponding to 3 different states a single position can be in. These states are the “selected”, “deselected” and “pressed”. Once again, this is entirely up to you how you wish to define these animations. I’ll gently hover the arrow from left to right when player hovers a given option on the list. When the player actually chooses the selected position, I’ll make a text slightly smaller and execute the action immediately. The deselected state will simply make the arrow invisible on a first frame.

Once the animations are created you’ll see them in your project assets. Disable the “Loop Time” on “deselected” and “pressed” animations. However, leave it on the “selected” animation since we want the arrow to go back and forth when the position is hovered.

Loop Time option in the Inspector window for the animation in scrollable menu in Unity

The animator setup

At this stage I’m going to set up the transitions between the animations in the “Animator” graph panel.

  • Right-Click on the “deselected” node and choose “Set as Layer Default State” option. This is going to be our default animation.
  • Right-Click on the “deselected” node again. This time choose “Make Transition” and drag the arrow to “selected” node. Next, right-click on the “selected” node and drag the arrow to “deselected” node to create a bi-directional transitions.
  • Create a bi-directional transitions between “selected” and “pressed” nodes by repeating the same process.
  • Right-Click on the “pressed” node and “Make Transition” to the “deselected” node.
  • In the “Parameters” tab of the “Animator” panel, click on the “+” sign and add two parameters of bool type. Call one “selected” and one “pressed”. We are going to use those to define the conditions that will specify when given transitions are made.

By the end of this process, your nodes graph should look like this:

Animator setup for individual menu item in scrollable menu in Unity

The animations transitions setup

The last thing we need to do here is to configure the settings of the transitions.

  • Left-Click on the white arrow going from the “deselected” to “selected”. Untick “Has Exit Time” and “Fixed Duration” checkboxes. Set “Transition Offset” and “Transition Duration (%)” to 0. In the “Conditions” panel set “selected” to “true”.
  • Left-Click on the arrow going in the opposite direction (“selected” -> “deselected”. Make the same configuration, but this time set “selected” to “false” in the “Conditions” panel.
  • Left-Click on the arrow going in “selected” -> “press” direction. Make the same configuration like in the first point but this time set “pressed” to “true” in the “Conditions” panel.
  • Left-Click on the arrow going in “press” -> “selected” direction. Tick “Has Exit Time” and “Fixed Duration” checkboxes here. Set “Exit Time” to 0.75, “Transition Duration” to 0.25 and “Transition Offset” to 0. In the “Conditions” panel set “pressed” to “false” and “selected” to “true”. This setup will ensure a proper behaviour when player chooses a given option on the list.
  • Left-Click on the arrow going in “press” -> “deselected” direction. Make the same configuration as in previous step but don’t set any conditions in the “Conditions” panel.

At this point our animations have been created and animator properly set. It’s time to move on to the script that will be responsible for controlling the flow of these animations!

The script controlling the button behaviour

I’ll write the script that will be attached to individual list element prefab I created in previous chapter. It will be relatively straightforward and its main task will be to ensure the proper transition between different states. To achieve this I’m going to need few references to keep track of the indexes of list positions: the MenuButtonController script, Animator component and the Index assigned to the element. If you wish to open a different menu panel upon selecting an option from the list, you’ll also need to include a reference to it.

public class MenuButton : MonoBehaviour
{
    public MenuButtonController menuButtonController;
    public Animator animator;
    public int thisIndex;
    [SerializeField] GameObject menuPanelToOpen;
}

In the Update() function I will check if the current index is equal to the element’s. Then I’ll make sure that the animator makes the transition to the “selected” animation by setting its “selected” parameter to true. After that, I’ll make a transition to the “pressed” animation if the player confirms his choice and open another panel if it is set. At the end I’ll reset both “pressed” and “selected” bool parameters in the animator if given conditions are not fulfilled. The entire function should look like this:

void Update()
{
    if(menuButtonController.index == thisIndex)
    {
        animator.SetBool("selected", true);
        if(menuButtonController.isPressConfirm)
        {
            animator.SetBool("pressed", true);
            if(menuPanelToOpen != null)
            {
              
  menuButtonController.gameObject.SetActive(false);
                 menuPanelToOpen.SetActive(true);
            }
        } 
        else if(animator.GetBool("pressed"))
        {
            animator.SetBool("pressed", false);
        }
    } else {
        animator.SetBool("selected", false);
    }
}

Attach the script to a prefab and you’re done! All that’s left is the proper configuration of the fields inside the editor.

3. Scripts configuration of scrollable menu in Unity

I still need to configure both scripts of the scrollable menu in Unity for its proper functionality. I’ll start with MenuButtonController. Set the ‘Max Index’ to the number corresponding to the number of positions on the list. This is simply the amount of instances of single menu button prefab under it. Make sure that the counting will start from zero by setting the “index” field to 0.

MenuButtonController script setup inside the Unity editor.

As for the individual menu items script I’ll add references they need to control the execution of animations. I’ll assign the Animator component, the main MenuButtonController script and make sure that its index corresponds to its position on the list. I’ll count from the top starting from 0. In case of an example shown below the correct index of an element is 2. In addition, I’ll assign a separate panel I want to show when the player selects this specific option from the list (Menu Panel To Open).

Menu item position configuration for scrollable menu in Unity

That’s it! If you have followed all the steps up till now you should have a solid foundation for scrollable menus inside your Unity games! This is an example of how it works:

Scrollable menu in Unity in action!

4. The dynamic population of the scrollable menu in Unity (optional)

At this point you may be wondering “Ok, but what if I have a content that changes dynamically during gameplay? How to update my scrollable lists so that the recent changes are accommodated?” The answer to that question lies in the OnEnable() method. When a new scrollable menu is enabled, we need to access structures holding our data, instantiate menu button prefabs with it and attach it to the wrapper. To make it more practical I’ll create a new script that inherits from our MenuButtonController and overrides its OnEnable() method. To that end, let’s first add a virtual method to the base class.

public class MenuButtonController : MonoBehaviour
{
    ...

    public virtual void OnEnable() 
    {
        // This method is supposed to be overridden
        // in child classes that inherit from this class 
    }
}

After that I’ll write a new script that inherits from the base class with it’s own OnEnable() method implementation (please note lines 1 and 7 below). In my example I extract the items information from my global data structure, count them and put in the dictionary. The dictionary is a perfect data structure for our purpose since it is a key-value pair collection. This allows me to couple the items with their corresponding amounts inside it.

public class MenuButtonItemMenuController : MenuButtonController
{
    [SerializeField] GameObject menuItemPrefab;

    public override void OnEnable()
    {
        base.OnEnable();
        Dictionary<Item, int> inventory = new Dictionary<Item, int>();

        foreach (Item item in Inventory.instance.items)
        {
            if (inventory.ContainsKey(item))
            {
                inventory[item] += 1;
            }
            else
            {
                inventory.Add(item, 1);
            }
        }
    }
}

I will now iterate through all entries in the newly created dictionary and create single menu button position instances. Remember to set all the necessary fields of the newly created instances with their correct values!

public class MenuButtonItemMenuController : MenuButtonController
{
    [SerializeField] GameObject menuItemPrefab;

    public override void OnEnable()
    {
        base.OnEnable();
        Dictionary<Item, int> inventory = new Dictionary<Item, int>();

        foreach (Item item in Inventory.instance.items)
        {
            if (inventory.ContainsKey(item))
            {
                inventory[item] += 1;
            }
            else
            {
                inventory.Add(item, 1);
            }
        }
    }

    maxIndex = inventory.Count - 1;

    int index = 0;
    foreach (KeyValuePair<Item, int> entry in inventory)
    {
        if(menuItemPrefab != null)
        {
            GameObject gameObject = Instantiate(menuItemPrefab, Vector3.zero, Quaternion.identity, this.transform) as GameObject;
            gameObject.GetComponent<MenuButton>().menuButtonController = this;
            gameObject.GetComponent<MenuButton>().thisIndex = index;
            gameObject.GetComponent<MenuButton>().animator = gameObject.GetComponent<Animator>();
gameObject.transform.GetChild(1).GetComponent<TextMeshProUGUI>().text = entry.Key.name;
gameObject.transform.GetChild(3).GetComponent<TextMeshProUGUI>().text = entry.Value.ToString();
            index++;
        }
    }
}

Note that on lines 34-35 I’m referencing the children inside the menu button prefab that represent text elements. Their positions will depend on how you have structured your individual position prefab.

The result can be seen in the animation below:

Dynamic population of a scrollable menu in Unity!

Congratulations! You now not only have a strong foundation for building your own scrollable menus in Unity, but also know how to populate it dynamically with data at runtime!

Conclusion

In this tutorial I showed you how you can create a scrollable menu in Unity. This kind of menu is quite popular in most games taking advantage of more advanced user interfaces such as RPGs. Firstly, I created all necessary constructs for laying out the menu inside a game. Secondly, I’ve created animations for different states individual positions can be in at any given time. Thirdly, I wrote scripts controlling the flow and behaviour of selecting different options on the list. I finished by showing the curious reader how to populate the list dynamically on a basis of stored data at runtime.

References

Make A Gorgeous Start Menu (Unity UI Tutorial)! by Thomas Brush