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:
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):
if(!ui_exists("Panel_Pause")){var_panel=newUIPanel("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=newUIGrid("Grid_Pause",2,1);_grid.setSpacingVertical(20).setMargins(20).setMarginTop(80).setShowGridOverlay(true);_panel.add(_grid);var_button=newUIButton("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=newUIButton("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=newUIPanel("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:
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):
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:
if(!ui_exists("Panel_Options")){var_panel=newUIPanel("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=newUIButton("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 Tabvar_base_grid=newUIGrid("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=newUIGrid("Grid_Options_Game_Left",4,1);_left_grid.setShowGridOverlay(false);_base_grid.addToCell(_left_grid,0,0);var_txt=newUIText("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=newUISpinner("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=newUIText("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=newUISpinner("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=newUICanvas("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:
This gives us a fantastic-looking player customization screen:
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:
// Video Tabvar_grid=newUIGrid("Grid_Options_Video",2,2);_grid.setSpacingVertical(20).setMargins(30).setMarginTop(150).setMarginBottom(100).setShowGridOverlay(false);_panel.add(_grid,1);var_chk=newUICheckbox("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:
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.
// Credits tabvar_grp=newUIGroup("Group_Options_Credits",30,_panel.getDragBarHeight()+_panel.getTabControl().getDimensions().height+50,_panel.getDimensions().width-60,330,glass_panel);_panel.add(_grp,3);var_txt=newUIText("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!
Our final implementation of the options/pause menu with gooey.
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. ↩