Click here to Skip to main content
14,934,046 members
Articles / Game Development / Unity
Technical Blog
Posted 27 May 2018

Stats

931 views

Day 70 of 100 Days of VR: How to Implement Menu Scrolling in Unity

Rate me:
Please Sign up or sign in to vote.
0.00/5 (No votes)
27 May 2018CPOL10 min read
How to implement Menu scrolling in Unity

Day 70: Implement Scrolling

Currently, we have a menu. We can select it with our pointer, but what if there was an easier way to interact with it?

Since we’re working with the Daydream, one unique way to interact with the menu is to use our touchpad to scroll through the menu options!

That’s the goal of today! To implement menu scrolling features! There’s a lot involved, so let’s look at some of the key ideas that we have to work with to be able to scroll through menu items before we start implementing it!

Scrolling Through Menu Items Concepts

Unity has no way of knowing that we grouped our buttons together. As a result, there’s no way they can provide us any build in solution to be able to navigate through our list of buttons. We’re going to have to build this ourselves.

Here are a couple of things we’re going to have to do ourselves:

  1. Keep track of the current index we’re currently at.
  2. How to update our UI to be able to look like they’re being highlighted/selected
  3. Figure out how to detect which way we should move from one item to the next and how long we have to wait for us to jump from one item to the next (more on this)
  4. Get rid of our selection system if the user normally selects something

Keeping Track of Our Button Selection

The first thing we need to think about implementing our own button selection system is how we would know which button we select.

This is relatively straightforward, we’ll use an index to keep track of which button we’re on. When we do our next (or previous) option, we would switch the index.

Every time we pass the size of our buttons, we would go back to the starting index.

Now, this starts to get a bit tricky when we want to go backwards, that’s why we’re going to use modular arithmetic to always guarantee us a valid index.

C# has an interesting problem with mod and that it doesn’t deal with negative numbers as it would also give us a negative number. There’s a Stack Overflow question regarding negative mod number. The summary is that we’re going to have to roll our own solution for mod.

Changing Our Button UI to Show Hover and Selection

After we have a system to represent which button in our menu we have selected, we still have to show the user that we’re selecting something.

The best way to do that is to rely on the API that is available on the Button UI.

Specifically, the 3 things we’re going to have to rely on are:

  1. OnSelect() – Shows the UI on the button when we highlight it.
  2. OnDeselect() – Removes the highlight UI we have on our button.
  3. Select() – Calls the onClick function on the button

We’re going to see them later.

Detecting User Input From the Touchpad

We have a system to represent which button we’re currently selecting, and we now know how to select them.

The next thing we need to figure out is how to select the next button based on the input on our touchpad on our controller.

We’ve already seen how we can use the touchpad when moving our character, we’re going to use the same concept to detect if the user is touching the top or bottom of the touchpad to see if the user should scroll up or down.

There are 2 things we need to create:

  1. A sensitivity system to figure out how far we have to touch in the touchpad to be considered a valid scroll.
  2. How long do we want to hold on to the touchpad to count as a legal input? If we don’t have this restriction, we’ll quickly scroll through a new button every frame.

We’ll see more of this in our code.

Detecting When the User Switches to the Pointer

At this point, we would have a working system to select a button, however, the next thing we have to think about is what happens if we use the touchpad to select buttons but then decided to go back to our controller.

This can be solved pretty easily. We just need to add a new event trigger for on pointer enter. Whenever our controller points at a button, we can unselect everything. We’ll see more on this later.

Getting Started

So now we have a summary of everything we need to do, it’s time to actually write the script.

We’re going to go add our changes to our MenuController script. Here’s the complete code:

C#
using UnityEngine;
using UnityEngine.UI;

public class MenuController : MonoBehaviour
{
    public float TimeDelay = 0.5f; // Time to hold touchpad to move to next menu button
    public float DirectionSensitivity = 0.1f; // Only works up to 0.5f

    private Button[] _buttons;     // List of menu buttons
    private int _currentIndex;     // Current button index that we're selecting. 
                                   // -1 if we're not using it.
    private float _currentSelectTime; // Current time we use to keep track 
                                      // how long we've been holding

    enum MenuScrollState { None, Down, Up } // Enum of button press states 
                                            // that we could be in
    private MenuScrollState _currentState;  // The current state that we're in

    void Start ()
    {
        _buttons = GetComponentsInChildren<Button>();
        _currentIndex = -1;
        _currentSelectTime = 0f;
        _currentState = MenuScrollState.None;
    }
    
    void Update ()
    {
        if (GvrControllerInput.ClickButtonUp)
        {
            // If we click our main button, we select our current menu item.
            SelectButton();
        }
        else if (GvrControllerInput.IsTouching)
        {
            // If we're touching the touch pad we should figure out if we should move 
            // our current index selection.
            SelectTouchPad();
        }
    }

    // Uses the user's position on the touchpad to figure out 
    // when to move to select the next button
    private void SelectTouchPad()
    {
        float y = GvrControllerInput.TouchPos.y - 0.5f; // Get the y position after 
                                                        // subtracting the center point
        // Note: the top left corner of our touch pad is position 0,0, so if y == 0, 
        // we're touching the top
        // if y == 1, we're touching the bottom.
        switch (_currentState)
        {
            case MenuScrollState.None:
                if (y > DirectionSensitivity)
                {
                    // If we're currently in the None state and we're holding far enough down
                    // on the touchpad, we transition to the down state
                    SetState(MenuScrollState.Down, TimeDelay);
                }
                else if (y < -DirectionSensitivity) { // If we're currently in the None state 
                                                      // and we're holding far enough up 
                                                      // on the touchpad, we transition to 
                                                      // the down state 
                SetState(MenuScrollState.Up, TimeDelay); 
                } 
                break; 
            case MenuScrollState.Down: if (y > DirectionSensitivity)
                {
                    // If we're in the down state and 
                    // we're still still holding far enough down
                    // on the touchpad, we should move to the next button 
                    // if we meet the condition.
                    ShouldSelectNextButton(1);
                }
                else if (y < -DirectionSensitivity)
                {
                    // If we're in the down state and we're holding far enough up
                    // on the touchpad, we should transition to the up state.
                    SetState(MenuScrollState.Up, TimeDelay);
                }
                else
                {
                    // If we're not holding far enough in either direction on the touchpad,
                    // go back to the None state
                    SetState(MenuScrollState.None, 0f);
                }
                break;
            case MenuScrollState.Up:
                if (y < -DirectionSensitivity) { // If we're in the up state and 
                                                 // we're still still holding far enough up 
                                                 // on the touchpad, we should go to the 
                                                 // previous button in our menu. 
                    ShouldSelectNextButton(-1); 
                } 
                else if (y > DirectionSensitivity)
                {
                    // If we're in the up state and we're holding far enough down
                    // on the touchpad, we should transition to the down state.
                    SetState(MenuScrollState.Down, TimeDelay);
                }
                else
                {
                    // If we're in the Up state and we're not holding far enough in any
                    // direction on the touchpad we go back to the none state.
                    SetState(MenuScrollState.None, 0f);
                }
                break;
        }
    }

    // Handle the logic to switch to the next button in our menu.
    // Takes in an int that we'll use for direction. 1 for next, -1 for prev.
    private void ShouldSelectNextButton(int direction)
    {
        _currentSelectTime += Time.deltaTime;
        // If we hold onto the button long enough to reach our time delay
        // we can move our current index to the next/prev menu button.
        if (_currentSelectTime >= TimeDelay)
        {
            SelectNextButton(direction);
            _currentSelectTime = 0f;
        }
    }

    // Moves us to the next index based off of the direction that we were given.
    // Direction should either be 1 to move to the next button and -1 to move to the
    // previous button.
    private void SelectNextButton(int direction)
    {
        if (_currentIndex != -1)
        {
            // If this isn't the first time we select a menu, there
            // must have already selected a button, deselect it.
            _buttons[_currentIndex].OnDeselect(null);
        }
        else if (_currentIndex == -1 && direction == -1)
        {
            _currentIndex = 0; // Edge case for going backwards at the beginning
        }
        _currentIndex = Mod((_currentIndex + direction), 
                     _buttons.Length);          // Get next button index to use
        _buttons[_currentIndex].OnSelect(null); // Select the new menu button to highlight
    }

    // Select the button that we're currently highlighting.
    private void SelectButton()
    {
        if (_currentIndex != -1)
        {
            _buttons[_currentIndex].Select();
            // Reset ourselves back to the starting state.
            _currentIndex = -1;
            SetState(MenuScrollState.None, 0f);
        }
    }

    // Helper to change the state we're at and set the time we should be using.
    // When we first just switch to up or down, we don't want to wait or Delay Time,
    // so we immediately set our time to be the delay time.
    private void SetState(MenuScrollState newState, float newTime)
    {
        _currentState = newState;
        _currentSelectTime = newTime;
    }

    // Resets our state if the user decides to point at the menu using the pointer
    // instead of using the touchpad.
    public void PointerEnterObject()
    {
        if (_currentIndex != -1)
        {
            _buttons[_currentIndex].OnDeselect(null);
            _currentIndex = -1;
            SetState(MenuScrollState.None, 0f);
        }
    }

    // Mod function courtesy of 
    // https://stackoverflow.com/questions/1082917/mod-of-negative-number-is-melting-my-brain
    // to help us find a positive value when looking for remainders in division.
    private static int Mod(int a, int b)
    {
        return (a % b + b) % b;
    }

    // Does an action based off of what menu button that was clicked on.
    // We don't have any specific behavior, but we could add them if we needed to.
    public void MenuButtonClick(int index)
    {
        switch (index)
        {
            case 0:
                print("Clicked button " + index);
                break;
            case 1:
                print("Clicked button " + index);
                break;
            case 2:
                print("Clicked button " + index);
                break;
            case 3:
                print("Clicked button " + index);
                break;
            case 4:
                print("Clicked button " + index);
                break;
            case 5:
                print("Clicked button " + index);
                break;
            case 6:
                print("Clicked button " + index);
                break;
            default:
                break;
        }
    }
}

This is a lot of code, so let’s go over it.

Looking at the Variables

This is quite a hefty script, but here’s what we’re working with:

  • public float TimeDelay – This will be used as a limit for how long we have to hold onto our touchpad for before we can move to the next menu button.
  • public float DirectionSensitivity – This setting will be used to adjust how far the user has to hold their finger on the touchpad before we count it as a valid input.
  • private Button[] _buttons; – Array of menu buttons that we’ll be cycling through
  • private int _currentIndex;Int that we use to track of which menu button we’re currently cycling through
  • private float _currentSelectTime;Float used to keep track of how long the user has held onto the touchpad in a certain direction. Once we hit the TimeDelay, we would move to the next menu button.
  • private MenuScrollState _currentState; – The state that we’re in. Either we’re holding the bottom half of the touchpad, the top half of the touchpad, or we’re not holding onto the touchpad/not far enough to count as a valid input.

Hopefully, now that we understand the variable, we can go back and understand more about how the script works.

Walking Through the Code

Now, let’s start walking through the code:

  1. Like always, we initialize all of our variables in Start(), we set our state to be None, we set the starting index to be -1, and our current time to be 0.
  2. Everything starts in Update(), there are 2 user inputs that we have to watch out for from the controller, touching the touchpad and clicking the touchpad.
    We’ll go through each case, in Update(), but let’s start with the largest and most important case when we’re touching the touchpad.
  3. If we’re just touching the touchpad, we’re going to call SelectTouchPad()
  4. In SelectTouchPad(), we start off by figuring out the y position of where we are holding onto the touchpad. Remember, (0,0) is the top left of the corner of the touchpad. Specifically, the top of the touchpad is 0 and the bottom is 1. We subtract that value by the center value: 0.5, so that we will know if y > 0, we’re touching the bottom half of the touchpad and if y < 0, we’re touching the top half of the touchpad.
  5. After getting our Y value, we must start considering what “state” we’re in. A state being is we currently doing nothing, are we currently touching the bottom half of the touchpad, or are we currently holding down the top half. We use a switch statement for each state we’re in and depending on what y value we have, do the appropriate action.
  6. The general rule we follow is that if we’re holding the top half of the touchpad while we’re already on the Up state, we’ll call ShouldSelectNextButton(), if we’re not, we’ll switch to the Up state. Otherwise, we’re either touching the bottom half of the touchpad and we’ll switch to the Down state or we’re in an in-between location that we use DirectionSensitivity to create and we’ll switch to the None. The same rule applies if we’re in the Down state and the None state.
  7. In ShouldSelectNextButton(), the next thing we have to figure out is whether we want to move to the next/previous menu button. We use _currentSelectTime and DelayTime to figure this out. We don’t want to immediately jump to the next button, because if we do, we’ll jump every frame that we’re holding on to our touchpad (hint: A LOT), as a result, we need to keep track of the time to see if we’re holding onto the touchpad for a set amount of time. Once we do, we’ll call SelectNextButton()
  8. In SelectNextButton(), we’re given a variable called direction which is also passed into ShouldSelectNextButton(), the direction should either be 1 or -1 and it’ll be used to move our _currentIndex to the next value. We want to increase by 1 if we’re holding the bottom half of the touchpad and if we’re holding up, we want to go decrease our index by 1 to back one menu button.
  9. Continuing on in SelectNextButton(), the core logic is, we add the direction we’re going with the current index and then mod it for the situation where we need to cycle back to index 0 from the last index of vise-versa. We would unselect our previous button, get our new index and then select the next button. Select in this case means hover over the button, but not click it.
  10. The only one case we must worry about is what happens if we’re going backward while starting at -1. We would end up at -2 mod <size of the array>, which would give us the 2nd to the last number, that’s why we must catch this case and set our_currentIndex to be 0.

Phew, that’s it for the case of when we’re using the touchpad. The next of the 2 cases to look at is what happens if we press down on our touchpad:

  1. After checking if GvrControllerInput.ClickButtonUp() is true, we call SelectButton()
  2. In SelectButton(), we make sure we have a valid index and if we do, we would click it, which would call the onClick event trigger that we set which would call MenuButtonClick() with the index that we have pre-set on the button. After we call our button, we would reset everything back to our starting state.

Now that we finished talking about the Update(), we actually still have one last function to look at PointerEnterObject().

What is it for? What would happen if we’re scrolling through our menu with the touchpad, but then decided to use the controller again to select something from the menu? In this case, we would have 2 menu buttons that would be highlighted.

The correct thing to do would be to get rid of the hover effect on the button we cycled through on the UI and let Unity handle it. This is the unselection part of menu button we highlight with our touchpad.

However, the question is how would we call this function? The answer is that we would set up a PointerEnter event trigger that will run PointerEnterObject() on all of our menu buttons.

Every time the user points to a menu button, we would check if we’re currently selecting a menu button via PointerEnterObject() and get rid of it.

Setting Up Our Components

Phew, that was a long script. Hopefully, the explanation helps make the code more understandable.

Now that we have our script, we just need to set up the PointerEnter event trigger to run PointerEnterObject()

  1. In each of the Button in our MenuContainer, for the Event Trigger, create a Pointer Enter event and add MenuContainer as the game object. As for the function to call, select MenuController > PointerEnterObject and that’s it!

With this finished, we can now start scrolling through the menu with our touchpad!

Conclusion

Looking back on what we have accomplished today, we created a system that allows us to scroll through a menu and then connected it with the inputs provided to us by the daydream.

However, if you think we’re done, there’s still more!

Another common feature that you might notice in VR games when it comes to the menu is that if they start to leave your screen, it’ll start to move back to the front of your screen. That’s what we’re going to implement the next day! It’s going to be great!

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

IAmJoshChang
United States United States
Joshua is a passionate software developer working in the Seattle area. He also has experience with developing web and mobile applications, having spent years working with them.

Joshua now finds his spare coding time spent deep in the trenches of VR, working with the newest hardware and technologies. He posts about what he learns on his personal site, where he talks mostly about Unity Development, though he also talks about other programming topic that he finds interesting.

When not working with technology, Joshua also enjoys learning about real estate investment, doing physical activities like running, tennis, and kendo, and having a blast with his buddies playing video games.

Comments and Discussions

 
-- There are no messages in this forum --