Graphical menu
Macroquad has a built-in system to display a graphical user interface where the look can easily be changed using PNG images. We will use this to create a graphical main menu for our game. There will be quite a lot of code to define the look of the UI. However, once that is done, it is very easy to use it.
The menu will have a window centered on the screen with the text “Main menu” in
the title bar. Inside the window there will be two buttons, one for “Play” and
one for “Quit”. The UI will be built using different kinds of widgets such as
label
, button
, editbox
, and combobox
.
Implementation
To begin with we need to import what we need from the ui
module.
use macroquad::ui::{hash, root_ui, Skin};
Load resources
After loading the sounds we’ll load the font and images used for the UI.
There is an image to create the window, window_background.png
, one image for
the buttons, button_background.png
, and finally an image for when the button
is pressed, button_clicked_background.png
. The images are loaded with the
function load_image()
and binary files with the function load_file()
. Both
images and files are loaded asynchronously and may return errors. This means
we will have to call await
and unwrap()
to get the files. If we can’t load
the files needed to display the main menu, we can just exit the program
immediately.
let window_background = load_image("window_background.png").await.unwrap();
let button_background = load_image("button_background.png").await.unwrap();
let button_clicked_background = load_image("button_clicked_background.png").await.unwrap();
let font = load_file("atari_games.ttf").await.unwrap();
Create a skin
Before the game loop we need to define how our UI should look. We will build
Style
structs for the window, buttons and texts. After that we will use the
styles to create a Skin
.
We use the function root_ui()
that will draw widgets last in every frame
using a default camera and the coordinate system
(0..screen_width(), 0..screen_height())
.
Window look
To build a style we use a StyleBuilder
that has helper methods to define all
parts of the style. Vi get access to it by using the method style_builder()
on root_ui()
. The values that aren’t set will use the same values as the
default look.
We will use the method background()
to set the image used to draw the
window. After that we can use background_margin()
to define which parts of
the image that shouldn’t change proportion when the window changes size. This
is used to ensure that the edges of the window will look good.
The method margin()
is used to set margins for the content. These values can
be negative to draw content to the borders of the window.
let window_style = root_ui()
.style_builder()
.background(window_background)
.background_margin(RectOffset::new(32.0, 76.0, 44.0, 20.0))
.margin(RectOffset::new(0.0, -40.0, 0.0, 0.0))
.build();
There are many more methods to define styles, these are described in the
documentation for Macroquad’s
StyleBuilder
Button look
In the definition for buttons we’ll use two images. Using background()
we
set the default image for the button, and background_clicked()
is used to
set the image to be displayed while the button is clicked on.
We need to set both background_margin()
and margin()
to be able to stretch
the image to cover the text inside the button. The look of the text is defined
using the methods font()
, text_color()
, and font_size()
.
let button_style = root_ui()
.style_builder()
.background(button_background)
.background_clicked(button_clicked_background)
.background_margin(RectOffset::new(16.0, 16.0, 16.0, 16.0))
.margin(RectOffset::new(16.0, 0.0, -8.0, -8.0))
.font(&font)
.unwrap()
.text_color(WHITE)
.font_size(64)
.build();
Text look
Normal text displayed in the interface uses label_style
. We will use the
same font as for the buttons, but in a slightly smaller font size.
let label_style = root_ui()
.style_builder()
.font(&font)
.unwrap()
.text_color(WHITE)
.font_size(28)
.build();
Define a Skin
We can now create a Skin
using window_style
, button_style
, and
label_style
. We won’t define any other styles for the skin as we won’t be
using them.
We use push_skin()
to define the current skin that is to be applied. We will
only use one skin, but to change between different looks between windows, it’s
possible to use push_skin()
and pop_skin()
.
We will also set the variable window_size
to define the size of the window.
let ui_skin = Skin {
window_style,
button_style,
label_style,
..root_ui().default_skin()
};
root_ui().push_skin(&ui_skin);
let window_size = vec2(370.0, 320.0);
It’s possible to change the look of more parts of the UI. More information on how to do this can be found in the documentation of the struct Skin.
Build the menu
We can now build a menu by drawing a window with two buttons and a heading.
The content of the GameState::MainMenu
matching arm can be replaced with the
code at the end of this chapter.
Start by creating a window using root_ui().window()
. The function takes an
argument that is generated with the macro hash!
, a position that we’ll
calculate based on the window size and the screen dimensions, and finally a
Vec2
for the size of the window. Finally it takes a function that is used to
draw the content of the window.
Window title
In the window function we start by setting a title for the window with the
widget Label
that we can create using ui.label()
. The method takes two
arguments, a Vec2
for the position of the label and a string with the text
to be displayed. It’s possible to set None
as position, in which case the
placement will be relative to the previous widget. We will use a negative y
position to place the text within the title bar of the window.
It’s also possible to create widgets by instantiating a struct and using builder methods.
widgets::Button::new("Play").position(vec2(45.0, 25.0)).ui(ui);
Buttons
After the label we’ll add a button to begin playing the game. The method
ui.button()
returns true
when the button is clicked. We will use this to
set the GameState::Playing
to start a new game.
Then we can create a button with the text “Quit” to exit the game.
GameState::MainMenu => {
root_ui().window(
hash!(),
vec2(
screen_width() / 2.0 - window_size.x / 2.0,
screen_height() / 2.0 - window_size.y / 2.0,
),
window_size,
|ui| {
ui.label(vec2(80.0, -34.0), "Main Menu");
if ui.button(vec2(65.0, 25.0), "Play") {
squares.clear();
bullets.clear();
explosions.clear();
circle.x = screen_width() / 2.0;
circle.y = screen_height() / 2.0;
score = 0;
game_state = GameState::Playing;
}
if ui.button(vec2(65.0, 125.0), "Quit") {
std::process::exit(0);
}
},
);
}
There are many different widgets that can be used to create interfaces.
The list of available widgets can be found in the documentation of the
struct Ui
.
Try the game
When starting the game, a graphical menu will be shown where the player can choose to start a game or quit the program.