← View other assets
asset

Menus - buttons, sliders, and carousels

A struct based menu system with support for various element types.
Mimpy
Lv. 26
· 8 min read · 1478 views

This asset will allow you to create a menu that you can navigate with your arrow keys, and swap between multiple pages. Different menu elements can be added to each page; regular buttons that can be pressed, sliders that can be moved side to side, and carousels that can let you pick from a range of options.

Editor's Note: The carousel element discussed here doesn't spin around like a true carousel menu feature does, but it does let us move our selection around in a set like a carousel menu would. The name is mostly used here to distinguish it from a slider. The term "radio" can also be applied to this type of menu element.

1
The Menu
Create a menu script and add this constructor to it:

function Menu() constructor {
    position = 0;
    elements = [];
    length = 0;

    scroll = function(_direction, _wrap = false) {
        position += _direction;
        if (_wrap) {
            position = position - floor(position / length) * length;
        }
        else {
            position = clamp(position, 0, length - 1);
        }
    }

    changeValue = function(_amount) {
        if (_amount != 0) {
            elements[position].changeValue(_amount);
            elements[position].onValueChange(elements[position].value);
        }
    }

    select = function() {
        elements[position].onSelect();
    }

    add_button = function(_text, _onSelect) {
        array_push(elements, new MenuButton(_text, _onSelect));
        length = array_length(elements);
        return self;
    }

    add_slider = function(_text = "", _minimum = 0, _maximum = 10, _interval = 1, _start = _minimum, _width = 100, _onValueChange) {
        array_push(elements, new MenuSlider(_text, _minimum, _maximum, _interval, _start, _width, _onValueChange));
        length = array_length(elements);
        return self;
    }

    add_carousel = function(_text = "", _options = [], _start = 0, _onValueChange) {
        array_push(elements, new MenuCarousel(_text, _options, _start, _onValueChange));
        length = array_length(elements);
        return self;
    }

    draw = function(_x, _y, _spacing) {
        draw_set_halign(fa_left);
        draw_set_valign(fa_center);
        for (var i = 0; i < length; i++) {
            elements[i].draw(_x, _y + _spacing * i, i == position);
        }
    }
}

There are several methods in this constructor that can be called:

  • add - add a new button to our menu
  • scroll - move our cursor up and down the menu. Add in a true argument to the end to make it wrap around at the start and end
  • select - call the currently selected button's code
  • changeValue - called when using a menu element that can change its value, like a slider
  • add_button - add a standard menu button
  • add_slider - add a slider (a bar with a cursor that can slide from one side to the other)
  • add_carousel add a carousel (a discrete set of values, the user can select one of them)
  • draw - each element type needs to be able to draw itself in a unique way. When the menu draws itself, it tells each element to run its own draw method.
2
Menu Elements
Underneath our menu constructor, we're going to add several other constructors. These constructors will contain information about the different types of elements that can go into a menu, like buttons.

First, the MenuElement constructor, which will serve as a parent for our other menu elements.

function __MenuElement(_onSelect = function() { }, _onValueChange = function() { }) constructor {
    onSelect = _onSelect;

    value = 0;
    changeValue = function() { };
    onValueChange = _onValueChange;
}

Next, MenuButton, which is a standard button on the menu. It requires:

  • text - the name of the button
  • onSelect - what to do when the button is selected
function MenuButton(_text = "", _onSelect) : __MenuElement(_onSelect) constructor {
    text = _text;

    draw = function(_x, _y, _isSelected) {
        draw_set_color(_isSelected ? c_yellow : c_white);
        draw_text(_x, _y, text);
    }
}

A slider stores a value that can slide around within a specified range. It requires:

  • text - the name of the slider
  • minimum - the lower bound of our range
  • maximum - the upper bound of our range
  • interval - how much the slider moves with each input
  • start - what our value should start out as
  • width - how wide the bar should be visually when drawn
  • onValueChange - what to do when the value changes
function MenuSlider(_text = "", _minimum = 0, _maximum = 10, _interval = 1, _start = _minimum, _width = 100, _onValueChange = function() { }) : __MenuElement(undefined, _onValueChange) constructor {
    text = _text + ":";

    minimum = _minimum;
    maximum = _maximum;
    interval = _interval;

    value = _start;

    width = _width;

    changeValue = function(_amount) {
        value = clamp(value + _amount * interval, minimum, maximum);
    }

    onValueChange = _onValueChange;

    draw = function(_x, _y, _isSelected) {
        static spacing = 20;

        draw_set_color(_isSelected ? c_yellow : c_white);
        draw_text(_x, _y, text);

        _x += string_width(text) + spacing;

        draw_line(_x, _y, _x + width, _y);

        var pos = lerp(_x, _x + width, (value - minimum) / (maximum - minimum));
        draw_line(pos, _y - 10, pos, _y + 10);
    }
}

A carousel stores a set of distinct, mutually exclusive options, for example a setting being On or Off. It requires:

  • text - the name of the carousel
  • options - an array of options
  • start - which option starts out selected (use its position in the array!)
  • onValueChange - what to do when the selected option changes
function MenuCarousel(_text = "", _options = [], _start = 0, _onValueChange) : __MenuElement(undefined, _onValueChange) constructor {
    text = _text + ":";
    options = _options;
    value = _start;

    changeValue = function(_amount) {
        value = clamp(value + _amount, 0, array_length(options) - 1);
    }

    onValueChange = _onValueChange;

    draw = function(_x, _y, _isSelected) {
        static spacing = 20;

        draw_set_color(_isSelected ? c_yellow : c_white);
        draw_text(_x, _y, text);

        _x += string_width(text) + spacing;

        for (var i = 0; i < array_length(options); i++) {
            var str = options[i];
            draw_text(_x, _y, str);

            if (i == value)
                draw_text(_x - 10, _y, ">");
            _x += string_width(str) + spacing;
        }
    }
}

IMPORTANT NOTE: See how each Menu Element has its own draw function? A button has to draw itself differently from a slider, so each one has a unique method of drawing. The draw methods shown here are extremely simplified, but you can edit them to change the way your elements appear on screen however you want!

3
Using Menus
An object that uses this menu constructor needs to create the buttons we can press, accept the input to interact with the menu, and tell the menu to draw itself.
Let's create some example menus to see the asset in action. We'll make a main menu and an options menu.

The main menu has three buttons:

  • Start - changes the room to rm_game
  • Options - go to the options menu
  • Quit - end the game

The options menu has several elements:

  • Volume - this slider will set global.volume to a value between 0 and 1, in increments of 0.1
  • Fullscreen - this carousel will let us turn fullscreen on and off
  • Back - go back to the main menu
/// Create Event
main = new Menu()
    .add_button("Start", function() {
        room_goto(rm_game);
    })
    .add_button("Options", function() {
        menu = options;
        menu.position = 0;
    })
    .add_button("Quit", function() {
        game_end();
    });

options = new Menu()
    .add_slider("Volume", 0, 1, 0.1, global.volume, 100, function(_value) {
        global.volume = _value;
    })
    .add_carousel("Fullscreen", ["Off", "On"], window_get_fullscreen(), function(value) {
        window_set_fullscreen(value);
    })
    .add_button("Back", function() {
        menu = main;
    });

menu = main;

IMPORTANT NOTES:

  • We are chaining add_ calls together, and so we don't need a semicolon after new Menu(), nor do we need any semicolons after our chained methods besides the last one.
  • We declare our menu variable last, after we declare our individual menus.
  • Check out how the Volume slider's start argument is set to global.volume / 10, and the Fullscreen carousel's start argument is window_get_fullscreen().
    • These options will start out as the values they have been set to previously, so that they don't become mismatched if we close and reopen the menu later!
  • We run menu.position = 0; when switching to the options menu, and we don't when switching to the main menu.
    • This will let us always start at the top of the Options menu when we swap to it, but when we hit back and go to the main menu, we'll be highlighting the Options menu, giving us some nice continuity.


Our step event is just going to run the various functionality the menu has. We'll change the selected element with the up and down arrow keys, we'll change sliders and carousels with the left and right arrow keys, and we'll select buttons with the spacebar.

/// Step Event
menu.scroll(keyboard_check_pressed(vk_down) - keyboard_check_pressed(vk_up));
menu.changeValue(keyboard_check_pressed(vk_right) - keyboard_check_pressed(vk_left));
if (keyboard_check_pressed(vk_space)) {
    menu.select();
}

Finally, we'll tell the menu to draw itself in the draw event. x and y determine the position of the menu, and 20 specifies that each button should be 20 pixels spaced apart.

/// Draw Event
menu.draw(x, y, 20);
Our menu in action!

This should give you a good starting point to create dynamic, flexible menus with various functionalities. If all you need is a set of buttons that you can navigate with the arrow keys, then I suggest the simpler menu option. But if you need sliders, carousels, or even more menu element types that you want to add yourself, then I hope that you find this framework useful!