Skip to content

Intro

The idea of this section is to showcase how to create relatively complex menus with gooey, having an excuse to show up many of the features of the library.

We will build an Options menu that is reachable from the Pause menu, and we will include tabs for Game, Video, and Audio options, as well as Credits.

Coding the pause functionality

The first thing to be done is actually code the pause state for the game. Fortunately, we already have this coded - all objects exit their step events if the game is in the Paused state. We just have to trigger it (and back) whenever appropriate, and pause/resume the time_source_game time source parent1:

Game / Step
if (self.fsm.get_current_state_name() == "Playing")     self.pause();
else                                                    self.resume();

and we can create the pause and resume methods in the Create event, and make them to simply set the Game state and pause/resume the time sources:

Game / Create
self.pause = function()     {
    self.fsm.trigger("Paused");
    time_source_pause(time_source_game);
};

self.resume = function() {
    self.fsm.trigger("Playing");
    time_source_resume(time_source_game);
};

Now, we can test the pause functionality by using ESC or Start in the controller. We will use this state to show the pause/settings menu.

Creating the pause menu

The pause menu will be very simple: a menu with Resume and Options buttons inside a panel. The resume button will call the resume method, and the options button will hide the pause panel and show an options panel that we will create. We set the panels to invisible at first, since we only want them to show when we pause the game (or click Options from the pause menu, respectively):

Game / Draw GUI
if (!ui_exists("Panel_Pause")) {
    var _panel = new UIPanel("Panel_Pause", 0, 0, 350, 250, green_panel, UI_RELATIVE_TO.MIDDLE_CENTER);
    _panel.setResizable(false).setMovable(false).setTitle("Game Paused").setTitleFormat("[fnt_UI][fa_top][fa_center][scale,3]");
    _panel.setVisible(false);

    var _grid = new UIGrid("Grid_Pause", 2, 1);
    _grid.setSpacingVertical(20).setMargins(20).setMarginTop(80).setShowGridOverlay(true);
    _panel.add(_grid);

    var _button = new UIButton("Button_Pause_Resume", 0, 0, 0, 0, "Resume", lt_box_9slice_c, UI_RELATIVE_TO.MIDDLE_CENTER);
    _button.setSpriteMouseover(dt_box_9slice_c_selected).setSpriteClick(dt_box_9slice_c_selected).setTextFormat("[fnt_UI][fa_center][fa_middle][scale,2]", true);
    _button.setInheritWidth(true).setInheritHeight(true);
    _button.setCallback(UI_EVENT.LEFT_RELEASE, self.resume);
    _grid.addToCell(_button, 0, 0);

    var _button = new UIButton("Button_Pause_Options", 0, 0, 0, 0, "Options", lt_box_9slice_c, UI_RELATIVE_TO.MIDDLE_CENTER);
    _button.setSpriteMouseover(dt_box_9slice_c_selected).setSpriteClick(dt_box_9slice_c_selected).setTextFormat("[fnt_UI][fa_center][fa_middle][scale,2]", true);
    _button.setInheritWidth(true).setInheritHeight(true);
    _button.setCallback(UI_EVENT.LEFT_RELEASE, function() {
        ui_get("Panel_Pause").setVisible(false);
        ui_get("Panel_Options").setVisible(true);
    });
    _grid.addToCell(_button, 1, 0);
}

if (!ui_exists("Panel_Options")) {
    var _panel = new UIPanel("Panel_Options", 0, 0, 450, 500, green_panel, UI_RELATIVE_TO.MIDDLE_CENTER);
    _panel.setResizable(false).setMovable(false).setTitle("Options").setTitleFormat("[fnt_UI][fa_top][fa_center][scale,3]");
    // more to come
}

Now, to show the panel, we can simply add the visibility set statement to our pause and resume methods, like so:

Game / Create
self.pause = function()     {
    self.fsm.trigger("Paused");
    time_source_pause(time_source_game);
    ui_get("Panel_Pause").setVisible(true);
};

self.resume = function() {
    self.fsm.trigger("Playing");
    time_source_resume(time_source_game);
    ui_get("Panel_Pause").setVisible(false);
};

Preparing the game audio and options

Currently, there's no options data structure set up. Let's go ahead and do that, considering settings for the game itself, video and audio (I've added the basics):

Game / Create
self.player_hair_options = ["bowlhair", "curlyhair", "longhair", "mophair", "shorthair", "spikeyhair"];
self.options = {
    game: {
        player_hair_index: 2,
    },
    video: {
        fullscreen: false,
    },
    audio: {
        music_enabled: true,
        sounds_enabled: true,
        music_volume: 0.4,
        sounds_volume: 1.0,
    },
};

I made sure to toggle the fullscreen option on the F key press:

Game / Step
if (InputPressed(INPUT_VERB.FULLSCREEN))        {
    self.options.video.fullscreen = !self.options.video.fullscreen;
    Camera.toggle_fullscreen();
}

I also went ahead and downloaded and added audio (music and sound effects on most actions) to the game, these are the credits for the sounds used:

Audio
Sound URL Notes/attribution
Music Link "Make it Good" by Ketsa
Footsteps Link
Axe slash Link
Axe chop Link
Cow Link
Sheep Link
Chicken Link
Pig Link
Water Link
Digging Link
Inventory open Link
Inventory close Link
Pickup Link
Hover Link
Click Link

Creating the options menu

Adding the base menu

Let's create our panel to host the options. We want a tabbed panel with four tabs, so we'll add 3 (by default, all panels have one tab) and configure the titles and sprites for the buttons appropriately. We'll also add a button to the common widgets part as described in the tabbed panels tutorial:

Game / Draw GUI
if (!ui_exists("Panel_Options")) {
    var _panel = new UIPanel("Panel_Options", 0, 0, 550, 550, green_panel, UI_RELATIVE_TO.MIDDLE_CENTER);
    _panel.setResizable(false).setMovable(false).setTitle("Options").setTitleFormat("[fnt_UI][fa_top][fa_center][scale,3]");

    _panel.setTabControlVisible(true).setTabMargin(20);
    _panel.addTab(3).setTabText(0, "Game").setTabText(1, "Video").setTabText(2, "Audio").setTabText(3, "Credits");
    _panel.setTabsTextFormat("[fnt_UI][scale,2][#eeeeee]").setTabsTextFormatMouseover("[fnt_UI][scale,2]").setTabsTextFormatSelected("[fnt_UI][scale,2]");
    _panel.setTabSprites(green_button00).setTabSpritesMouseover(green_button01).setTabSpritesSelected(green_button01);
    _panel.setTabOffset({x:0, y: 70});
    _panel.setVisible(false);

    var _button = new UIButton("Button_Options_Return", 0, -20, 400, 50, "Return", green_button00, UI_RELATIVE_TO.BOTTOM_CENTER);
    _button.setSpriteMouseover(green_button01).setSpriteClick(green_button01).setTextFormat("[fnt_UI][fa_center][fa_middle][scale,2]", true);
    _button.setCallback(UI_EVENT.LEFT_RELEASE, function() {
        ui_get("Panel_Pause").setVisible(true);
        ui_get("Panel_Options").setVisible(false);
    });
    _panel.add(_button, -1);
    // more to come
}

After we have added our basic panel, let's work on each tab at a time:

  • For game options, we want to be able to change the player's hairstyle. We'll show a text label, a spinner to choose between the options and a canvas to preview how the player will look.
  • For video options, we will include a checkbox to toggle full screen.
  • For audio options, we will include checkboxes to toggle music and sound effects on/off separately, and we will also include sliders to control the volume for each one.
  • For credits, we will show a wrapped text and implement scrolling to view the credits of the game.

Adding the Game tab

Let's start with the game tab:

  • We want to control hairstyle and outfit. The logic for dynamically changing this in-game is already coded in obj_Player (the hairstyle is actually a second, layered sprite drawn on top of the base player, and the outfit is being drawn using PixelatedPope's Retro Palette Swapper and a palette sprite, spr_PalSwap, that has four different palettes).
  • I'd like controls to the left and a preview of how the player will look with the chosen hairstyle and outfit, to the right. To achieve this we will use text labels and spinners on the left, and a UICanvas to show a surface with the preview of the character to the right.
  • We'll bind the spinners to the hairstyle index and the player outfit index.
  • Finally, we'll greate a surface and render the current player hairstyle and outfit to the surface.

This is the code (that goes inside the previous if (!ui_exists("Panel_Options")) { codeblock):

Game / Draw GUI
    // Game Tab
    var _base_grid = new UIGrid("Grid_Options_Game", 1, 2);
    _base_grid.setSpacingHorizontal(10).setMargins(30).setMarginTop(140).setMarginBottom(90).setColumnProportions([0.6,0.4]).setShowGridOverlay(false);
    _panel.add(_base_grid, 0);

    var _left_grid = new UIGrid("Grid_Options_Game_Left", 4, 1);
    _left_grid.setShowGridOverlay(false);
    _base_grid.addToCell(_left_grid, 0, 0);

    var _txt = new UIText("Text_Options_Game_PlayerHair", 0, 0, "Player Hairstyle", UI_RELATIVE_TO.MIDDLE_LEFT);
    _txt.setTextFormat(_fmt, true);
    _left_grid.addToCell(_txt, 0, 0);

    var _spinner = new UISpinner("Spinner_Options_Game_PlayerHair", 0, 0, Game.player_hair_options, green_button03, green_sliderLeft, green_sliderRight, 250, 50, Game.options.game.player_hair_index);
    _spinner.setBinding(Game.options.game, "player_hair_index");
    _spinner.getButtonText().setTextFormat("[fnt_UI][scale,2][fa_center][fa_middle]", true);
    _spinner.setCallback(UI_EVENT.VALUE_CHANGED, function() {
        obj_Player.hairstyle = Game.player_hair_options[Game.options.game.player_hair_index];
        obj_Player.setup_hair(obj_Player.hairstyle);
        self.update_surf();
    });
    _left_grid.addToCell(_spinner, 1, 0);

    var _txt = new UIText("Text_Options_Game_PlayerOutfit", 0, 0, "Player Outfit", UI_RELATIVE_TO.MIDDLE_LEFT);
    _txt.setTextFormat(_fmt, true);
    _left_grid.addToCell(_txt, 2, 0);

    var _spinner = new UISpinner("Spinner_Options_Game_PlayerOutfit", 0, 0, Game.player_outfit_options, green_button03, green_sliderLeft, green_sliderRight, 250, 50, Game.options.game.player_outfit_index);
    _spinner.setBinding(Game.options.game, "player_outfit_index");
    _spinner.getButtonText().setTextFormat("[fnt_UI][scale,2][fa_center][fa_middle]", true);
    _spinner.setCallback(UI_EVENT.VALUE_CHANGED, function() {
        obj_Player.outfit = Game.options.game.player_outfit_index;
        self.update_surf();
    });
    _left_grid.addToCell(_spinner, 3, 0);

    var _canvas = new UICanvas("Canvas_Options_Game_Skin", 0, 0, 250, 250, self.surf, UI_RELATIVE_TO.MIDDLE_CENTER);
    _base_grid.addToCell(_canvas, 0, 1);

Also, as mentioned, to achieve the preview we need to create the self.surf surface and we need to add the update_surf method to the Create event. Since surfaces are volatile, we also need to check if it still exists on every step and recreate it if needed - we'll do this at the beginning of our Draw GUI event:

Game / Create
self.surf = undefined;
self.update_surf = function() {
    surface_set_target(self.surf);
    draw_clear_alpha(c_black, 0);
    pal_swap_set(spr_PalSwap, obj_Player.outfit, false);
    draw_sprite_ext(asset_get_index(string($"base_idle")), 0, 125, 200, 6, 6, 0, c_white, 1);       
    pal_swap_reset();
    draw_sprite_ext(asset_get_index(string($"{obj_Player.hairstyle}_idle")), 0, 125, 200, 6, 6, 0, c_white, 1);         
    surface_reset_target();
}

and

Game / Draw GUI
2
3
4
5
6
if (!surface_exists(self.surf)) {
    self.surf = surface_create(250, 250);
    self.update_surf();
    if (ui_exists("Canvas_Options_Game_Skin")) ui_get("Canvas_Options_Game_Skin").setSurface(self.surf);
}

This gives us a fantastic-looking player customization screen:

Game options tab
The Game options tab, after implementation.

Adding the Video tab

In the video tab we only need a checkbox for toggling full screen. We bind it to the corresponding options struct key and we make it so that it calls the Camera.toggle_fullscreen method whenever the value changes:

Game / Draw GUI
    // Video Tab
    var _grid = new UIGrid("Grid_Options_Video", 2, 2);
    _grid.setSpacingVertical(20).setMargins(30).setMarginTop(150).setMarginBottom(100).setShowGridOverlay(false);
    _panel.add(_grid, 1);

    var _chk = new UICheckbox("Checkbox_Options_Video_Fullscreen", 0, 0, "Full screen", green_checkmark, undefined,, UI_RELATIVE_TO.MIDDLE_LEFT);
    _chk.setSpriteBase(checkbox_off).setInnerSpritesOffset({x: 9, y: 9});
    _chk.setBinding(self.options.video, "fullscreen");
    _chk.setCallback(UI_EVENT.VALUE_CHANGED, Camera.toggle_fullscreen);
    _chk.setTextFormatTrue(_fmt).setTextFormatFalse(_fmt).setTextFormatMouseoverTrue(_fmt).setTextFormatMouseoverFalse(_fmt);
    _grid.addToCell(_chk, 0, 0);

Adding the Audio tab

The audio tab is similarly set up by creating two checkboxes to allow enabling/disabling music or sounds, and two sliders to control volume. We bind all those to the corresponding options and use the same technique as above to use the VALUE_CHANGED event and reduce the gain level of the music so it matches the value:

Game / Draw GUI
    // Audio Tab
    var _grid = new UIGrid("Grid_Options_Audio", 2, 2);
    _grid.setSpacingVertical(20).setMargins(30).setMarginTop(150).setMarginBottom(100).setShowGridOverlay(false);
    _panel.add(_grid, 2);

    var _chk = new UICheckbox("Checkbox_Options_Audio_Music", 0, 0, "Music", green_checkmark, undefined,, UI_RELATIVE_TO.MIDDLE_LEFT);
    _chk.setSpriteBase(checkbox_off).setInnerSpritesOffset({x: 9, y: 9});
    _chk.setBinding(self.options.audio, "music_enabled");
    _chk.setCallback(UI_EVENT.VALUE_CHANGED, function() {
        if (Game.options.audio.music_enabled)   audio_resume_sound(snd_Music);
        else                                    audio_pause_sound(snd_Music);
    });
    _chk.setTextFormatTrue(_fmt).setTextFormatFalse(_fmt).setTextFormatMouseoverTrue(_fmt).setTextFormatMouseoverFalse(_fmt);
    _grid.addToCell(_chk, 0, 0);

    var _chk = new UICheckbox("Checkbox_Options_Audio_Sounds", 0, 0, "Sounds", green_checkmark, undefined,, UI_RELATIVE_TO.MIDDLE_LEFT);
    _chk.setSpriteBase(checkbox_off).setInnerSpritesOffset({x: 9, y: 9});
    _chk.setBinding(self.options.audio, "sounds_enabled");
    _chk.setTextFormatTrue(_fmt).setTextFormatFalse(_fmt).setTextFormatMouseoverTrue(_fmt).setTextFormatMouseoverFalse(_fmt);
    _grid.addToCell(_chk, 1, 0);

    var _slider = new UISlider("Slider_Options_Audio_Music", 0,0 , 200, grey_sliderHorizontal, green_sliderDown, Game.options.audio.music_volume, 0, 1,,UI_RELATIVE_TO.MIDDLE_CENTER);
    _slider.setShowHandleText(false).setShowMinMaxText(false).setHandleOffset({x:0, y:-15}).setClickToSet(true).setScrollChange(0.1).setDragChange(0.1);
    _slider.setBinding(Game.options.audio, "music_volume");
    _slider.setCallback(UI_EVENT.VALUE_CHANGED, function() {
        audio_sound_gain(snd_Music, Game.options.audio.music_volume);
    });
    _grid.addToCell(_slider, 0, 1);

    var _slider = new UISlider("Slider_Options_Audio_Sounds", 0,0 , 200, grey_sliderHorizontal, green_sliderDown, Game.options.audio.sounds_volume, 0, 1,,UI_RELATIVE_TO.MIDDLE_CENTER);
    _slider.setShowHandleText(false).setShowMinMaxText(false).setHandleOffset({x:0, y:-15}).setClickToSet(true).setScrollChange(0.1).setDragChange(0.1);
    _slider.setBinding(Game.options.audio, "sounds_volume");
    _grid.addToCell(_slider, 1, 1);

Adding the Credits tab: Scrolling widgets

Lastly, we add a Credits tab which shows text and allows it to be scrolled. We use the scroll method whenever the mouse wheel scrolls up or down, and we control it so it cannot scroll more than the top of the text or more than the bottom (plus a buffer). In order to get this right:

  • First of all, we add the text inside a UIGroup and we set setClipsContent to true so the UIGroup hides the rest of the text.
  • We use the getChildrenBoundingBoxAbsolute method to get the bounding box of all children of a widget. This returns a struct with x, y, width and height values that we can use to determine the height of the content.
  • We check the current scroll offset with getScrollOffset
  • With this, we determine whether the current offset is within the required range. If so, we call scroll, which handles the vertical (or horizontal) scrolling logic.

The code looks like this:

Game / Draw GUI
    // Credits tab
    var _grp = new UIGroup("Group_Options_Credits", 30, _panel.getDragBarHeight()+_panel.getTabControl().getDimensions().height+50, _panel.getDimensions().width-60, 330, glass_panel);
    _panel.add(_grp, 3);

    var _txt = new UIText("Text_Options_Credits", 0, 20,  "[rainbow]CREDITS[/rainbow]\n[scale,1](mouse wheel to scroll)[scale,2]\n\ngooey library - manta ray\n\nScribbleDX - JujuAdams\nUI Assets - Kenney\n\nDemo game art - Daniel Diggle\nMusic - 'Make it Good' by Ketsa\nSounds - Artists in freesounds.org\n\n[wave]Thanks for scrolling![/wave]\n[spr_deco_glint_01]", UI_RELATIVE_TO.TOP_CENTER);
    _fmt = "[fnt_UI][scale,2][fa_top][fa_center]";
    _txt.setMaxWidth(_grp.getDimensions().width-40).setTextFormat(_fmt, true);
    _grp.setClipsContent(true);
    _grp.setCallback(UI_EVENT.MOUSE_WHEEL_DOWN, function() {
        var _offset_buffer = 60;
        var _height = ui_get("Group_Options_Credits").getChildrenBoundingBoxAbsolute().height;
        var _offset = ui_get("Group_Options_Credits").getScrollOffset(UI_ORIENTATION.VERTICAL);
        var _parent_height = ui_get("Group_Options_Credits").getDimensions().height;
        if (_offset >= -(_height-_parent_height + _offset_buffer))      ui_get("Group_Options_Credits").scroll(UI_ORIENTATION.VERTICAL, -1, 15);
    });
    _grp.setCallback(UI_EVENT.MOUSE_WHEEL_UP, function() {
        var _height = ui_get("Group_Options_Credits").getChildrenBoundingBoxAbsolute().height;
        var _offset = ui_get("Group_Options_Credits").getScrollOffset(UI_ORIENTATION.VERTICAL);
        var _parent_height = ui_get("Group_Options_Credits").getDimensions().height;
        if (_offset < 0)    ui_get("Group_Options_Credits").scroll(UI_ORIENTATION.VERTICAL, 1, 15);
    });
    _grp.add(_txt);

The final result

The complete menu implementation looks like this. With, frankly, few lines of code, we have created a fairly complex menu logic!

Complete options/pause menu
Our final implementation of the options/pause menu with gooey.

  1. If you are following the tutorial with the official repo from the Github page, you'll see that the player and the NPC perform their idle animation even when paused - this is because of a small error in the FPS setting of the base_idle sprite. Change it to 0 and you're good to go.