Making Games in Rust - Part 9 - Main Menu Screen
Tags: rust, bevy, tutorial, programming
Published: 2022-01-10 12:58:07

Today, we are going to learn about UIs in Bevy. Our main goal is to end up with a main menu that contains 2 buttons, one to launch our game, and another to exit the application. This article will explain how to render UI elements to the screen, and how to listen to button events.

## Nodes and Styles

We could easily compare Bevy's UI to HTML: you create a hierarchy of elements / nodes, apply some styles to them, and Bevy's engine takes care of rendering what you asked for. A few "bundles" are exposed by Bevy to help you build your UI:

- NodeBundle: a basic node

```rs
fn root(materials: &Res<MenuMaterials>) -> NodeBundle {
    NodeBundle {
        style: Style {
            size: Size::new(Val::Percent(100.0), Val::Percent(100.0)),
            justify_content: JustifyContent::Center,
            align_items: AlignItems::Center,
            ..Default::default()
        },
        material: materials.root.clone(),
        ..Default::default()
    }
}
```

- ButtonBundle: to render a button

```rs
fn button(materials: &Res<MenuMaterials>) -> ButtonBundle {
    ButtonBundle {
        style: Style {
            size: Size::new(Val::Percent(100.0), Val::Percent(100.0)),
            justify_content: JustifyContent::Center,
            align_items: AlignItems::Center,
            ..Default::default()
        },
        material: materials.button.clone(),
        ..Default::default()
    }
}
```

- TextBundle: to render text

```rs
fn button_text(asset_server: &Res<AssetServer>, materials: &Res<MenuMaterials>, label: &str) -> TextBundle {
    return TextBundle {
        style: Style {
            margin: Rect::all(Val::Px(10.0)),
            ..Default::default()
        },
        text: Text::with_section(
            label,
            TextStyle {
                font: asset_server.load("fonts/FiraSans-Bold.ttf"),
                font_size: 30.0,
                color: materials.button_text.clone(),
            },
            Default::default(),
        ),
        ..Default::default()
    };
}
```

Each of these element have a `style` property used to express how to render the node.

This `style` property is a Rust struct that contains a subset of the usual properties found in `CSS`. Among others, you should find:

- margin and padding: to add space around our UI elements
- position_type: for relative or absolute positioning
- most of the [flexbox](https://www.w3schools.com/css/css3_flexbox.asp) properties: Bevy's rendering engine implements flexbox for positioning

Our menu also includes these two nodes:

```rs
fn border(materials: &Res<MenuMaterials>) -> NodeBundle {
    NodeBundle {
        style: Style {
            size: Size::new(Val::Px(400.0), Val::Auto),
            border: Rect::all(Val::Px(8.0)),
            ..Default::default()
        },
        material: materials.border.clone(),
        ..Default::default()
    }
}

fn menu_background(materials: &Res<MenuMaterials>) -> NodeBundle {
    NodeBundle {
        style: Style {
            size: Size::new(Val::Percent(100.0), Val::Percent(100.0)),
            align_items: AlignItems::Center,
            justify_content: JustifyContent::Center,
            flex_direction: FlexDirection::ColumnReverse,
            padding: Rect::all(Val::Px(5.0)),
            ..Default::default()
        },
        material: materials.menu.clone(),
        ..Default::default()
    }
}
```

## Text Styles

The `TextBundle`, in addition to the `style` property, specifies a property called `text` which also has a `style` property. This one exposes the possibility to modify the text font size, color and font.

If you take a look to the text node above, you should notice the use of a `Res<AssetServer>`. This utility allows us to load non Rust resources such as images or fonts.

To use it, we creates a new folder to the root of our project and named it `assets`. When using the asset server to load a resource, give it the path of your resource relative to this folder. Ex:

```rs
asset_server.load("fonts/FiraSans-Bold.ttf")
```

## Colors

Nodes and Buttons also contain a `material` property. This property works the same as the materials we previously added to our player and maps. To ease the development of our menu, we can centralize the definition of these colors into a resource:

```rs
struct MenuMaterials {
    root: Handle<ColorMaterial>,
    border: Handle<ColorMaterial>,
    menu: Handle<ColorMaterial>,
    button: Handle<ColorMaterial>,
    button_hovered: Handle<ColorMaterial>,
    button_pressed: Handle<ColorMaterial>,
    button_text: Color,
}

impl FromWorld for MenuMaterials {
    fn from_world(world: &mut World) -> Self {
        let mut materials = world.get_resource_mut::<Assets<ColorMaterial>>().unwrap();
        MenuMaterials {
            root: materials.add(Color::NONE.into()),
            border: materials.add(Color::rgb(0.65, 0.65, 0.65).into()),
            menu: materials.add(Color::rgb(0.15, 0.15, 0.15).into()),
            button: materials.add(Color::rgb(0.15, 0.15, 0.15).into()),
            button_hovered: materials.add(Color::rgb(0.25, 0.25, 0.25).into()),
            button_pressed: materials.add(Color::rgb(0.35, 0.75, 0.35).into()),
            button_text: Color::WHITE,
        }
    }
}
```

We still need to tell Bevy to instantiate this resource:

```rs
// on our AppBuilder
.init_resource::<MenuMaterials>()
```

## Defining the Node Hierarchy

We can add UI elements the same way we register other Bevy entities: we call `AppBuilder.spawn_bundle` and register children nodes with the `with_children` method:

```rs
enum MenuButton {
    Play,
    Quit,
}

fn setup(
    mut commands: Commands,
    asset_server: Res<AssetServer>,
    materials: Res<MenuMaterials>,
) {
    commands.spawn_bundle(UiCameraBundle::default());

    commands
        .spawn_bundle(root(&materials))
        .with_children(|parent| {
            // left vertical fill (border)
            parent
                .spawn_bundle(border(&materials))
                .with_children(|parent| {
                    // left vertical fill (content)
                    parent
                        .spawn_bundle(menu_background(&materials))
                        .with_children(|parent| {
                            parent.spawn_bundle(button(&materials))
                                .with_children(|parent| {
                                    parent.spawn_bundle(button_text(&asset_server, &materials, "New Game"));
                                })
                                .insert(MenuButton::Play);
                            parent.spawn_bundle(button(&materials))
                                .with_children(|parent| {
                                    parent.spawn_bundle(button_text(&asset_server, &materials, "Quit"));
                                })
                                .insert(MenuButton::Quit);
                        });
                });
        });
}
```

## Wiring it up

To wire everything up, we created a new Rust module called `main_menu`, which contains a `MainMenuPlugin`.

```rs
pub struct MainMenuPlugin;

impl Plugin for MainMenuPlugin {
    fn build(&self, app: &mut AppBuilder) {
        app.init_resource::<MenuMaterials>()
            .add_system_set(
                SystemSet::on_enter(AppState::MainMenu)
                    .with_system(cleanup.system())
                    .with_system(setup.system()),
            )
            .add_system_set(SystemSet::on_exit(AppState::MainMenu).with_system(cleanup.system()));
    }
}
```

```rs
// main.rs
mod main_menu;
use main_menu::MainMenuPlugin;

// ...

.add_plugin(MainMenuPlugin)
```

Running the game:

![Image of the menu](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tk8e670vqfq3axwdjztn.png)

## Apply Different Styles on Interactions

It is possible to modify the button look with a system:

```rs
// MainMenuPlugin
.add_system(button_system.system())

// system implementation
fn button_system(
    materials: Res<MenuMaterials>,
    mut buttons: Query<
        (&Interaction, &mut Handle<ColorMaterial>),
        (Changed<Interaction>, With<Button>),
    >
) {
    for (interaction, mut material) in buttons.iter_mut() {
        match *interaction {
            Interaction::Clicked => *material = materials.button_pressed.clone(),
            Interaction::Hovered => *material = materials.button_hovered.clone(),
            Interaction::None => *material = materials.button.clone(),
        }
    }
}
```

This way, our buttons should change color when hovered, and turn to green when pressed.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4rk6lxoeybum6mok5omm.png)

## Runnig Actions on Button Click

The same way we changed button styles, we can react to button click with a system triggered on clicks:

```rs
// main menu plugin
.add_system(button_press_system.system())

// System implementation
fn button_press_system(
    buttons: Query<(&Interaction, &MenuButton), (Changed<Interaction>, With<Button>)>,
    mut state: ResMut<State<AppState>>,
    mut exit: EventWriter<AppExit>
) {
    for (interaction, button) in buttons.iter() {
        if *interaction == Interaction::Clicked {
            match button {
                MenuButton::Play => state
                    .set(AppState::InGame)
                    .expect("Couldn't switch state to InGame"),
                MenuButton::Quit => exit.send(AppExit),
            };
        }
    }
}
```

In order for this system to run, we need to ensure that a `MenuButton` component is added to our button. This system will run the action linked to the type of `MenuButton` that was added to our entity.

Here, pressing `Play Game` should start a new game, and pressing `Quit` should exit the game.

## Final Bits of Code

Since we now have a menu that handles starting a new game, we can remove the `main_menu_controls` system that was defined in `main.rs`. Instead, we can add a new `back_to_main_menu_controls` in our `game` module:

```rs
// GamePlugin
.add_system_set(SystemSet::on_update(AppState::InGame).with_system(back_to_main_menu_controls.system()))

// System implementation
fn back_to_main_menu_controls(mut keys: ResMut<Input<KeyCode>>, mut app_state: ResMut<State<AppState>>) {
    if *app_state.current() == AppState::InGame {
        if keys.just_pressed(KeyCode::Escape) {
            app_state.set(AppState::MainMenu).unwrap();
            keys.reset(KeyCode::Escape);
        }
    }
}
```

All the code is available [here](https://github.com/belzile/platformer/tree/part-9). The most recent version of the game can be played [here](https://belzile.github.io/platformer/).