For our approach, starting a new game requires that we respond to a button press. This means setting up a system for handling button interactions.
We want a few aspects of the button to change for different interactions
- Change styles when hovered
- Display different button text based on game
RunState
We can start by introducing a set of new colors just for our button. We’ll do that in src/colors.rs
using a submodule called button
.
This is going to be a module like any other, although it won’t be in a different file because I prefer to have all of the colors in this one file.
pub mod button {
use bevy::prelude::Color;
pub const NORMAL: Color = Color::Lcha {
lightness: 0.15,
chroma: 0.5,
hue: 281.0,
alpha: 1.0,
};
pub const HOVERED: Color = Color::Lcha {
lightness: 0.55,
chroma: 0.5,
hue: 281.0,
alpha: 1.0,
};
pub const PRESSED: Color = Color::Lcha {
lightness: 0.75,
chroma: 0.5,
hue: 281.0,
alpha: 1.0,
};
}
The button that will allow the user to end their game or start a new game will change color on hover to indicate to the user that it is clickable. The color we want to modify is already built in as the BackgroundColor
of the button, which allows us to query for it in the new button_interaction_system
.
For the interaction_query
we'll ask for an Interaction
(which can be Clicked
, Hovered
, or None
) as well as mutable access to the BackgroundColor
used for the button. We'll use this to change the color used to display the button.
In this query we also filter by Changed<Interaction>
and With<Button>
. Filtering by Changed<Interaction>
means Bevy will only give us the entities that have Interaction
components that have changed since the last execution of the button_interaction_system
system. Filtering by With<Button>
means we will only get entities that have a Button
component.
Finally we need access to the RunState
, both the current value and the ability to set the next value.
fn button_interaction_system(
mut interaction_query: Query<
(&Interaction, &mut BackgroundColor),
(Changed<Interaction>, With<Button>),
>,
run_state: Res<State<RunState>>,
mut next_state: ResMut<NextState<RunState>>,
){
This system will apply to any button we ever create we'll use iter_mut
and a for loop, which will be more resilient to zero, one, or many entities even though we only expect one.
for (interaction, mut color) in
interaction_query.iter_mut()
{
Inside of the loop, we'll match
on the interaction
. Interaction
is an enum, so we can match on Clicked
, Hovered
, or None
.
When the button is Clicked
, we want to set the button color to the pressed
color. As we specified in our interaction_query
, the color
is a mutable reference to the BackgroundColor
. We want to set the value of the BackgroundColor
to a new color, rather than changing what the variable contains. To do that we can dereference the variable when setting the value.
You can think of this like the color
variable being a container with a blue ball in it. We don't want to take the blue ball out and put a new red ball in the container, instead we want to reach into the container and paint the existing ball red.
We take this approach for the other two states as well.
match interaction {
Interaction::Clicked => {
*color = colors::button::PRESSED.into();
...
}
Interaction::Hovered => {
*color = colors::button::HOVERED.into();
}
Interaction::None => {
*color = colors::button::NORMAL.into();
}
}
Since we set up our button colors in a public sub-module inside of the colors
module, we can access them using the module path colors::button
.
The final piece of functionality in this system is for setting the new RunState
. run_state
already contains the current State<RunState>
so we can match on that. State
is the state machine that we created when we used add_state
when creating our new Bevy app, and it is defined as a tuple struct that contains our RunState
. This is why we can use .0
to access our RunState
value.
If we're in the Playing
state, we want to set the state to GameOver
, and if we're in GameOver
we want to send the user to the Playing
state.
match run_state.0 {
RunState::Playing => {
next_state.set(RunState::GameOver);
}
RunState::GameOver => {
next_state.set(RunState::Playing);
}
}
To run the system we'll add it to our GameUiPlugin
.
impl Plugin for GameUiPlugin {
fn build(&self, app: &mut App) {
app.add_startup_system(setup_ui).add_systems((
scoreboard,
button_interaction_system,
));
}
}
If we run the game now, we'll see the button color change on hover and we'll also be able to click it to change the RunState
of the game. You can check that this is happening because all of the systems responding to keyboard input will be stopped and the game will not progress.
However, the text on the button doesn't change regardless of what state we're in so the user never knows what the button will do. We can add another system to handle the button text in response to RunState
changes.
The new button_text_system
will query for entity children using the With
filter in the same way we used it in the last system. We also grab all Text
components as mutable references so we can change the button text and we also grab the current RunState
.
Our button_query
will only ever have one result, so we can use `.single().
[children.first()](https://docs.rs/bevy/0.10.0/bevy/prelude/struct.Children.html#method.first) will give us the first entity that is a child of the button entity. In this case, that is the entity with a
Textcomponent.
firstreturns an
Option<&T>so we need to
unwrap() the Option
and dereference the T
, which in this case is an Entity
.
Bevy Query
s can be used as collections, so we can use this Entity
as an argument to text_query.get_mut() and get the
Textcomponent on that
Entityfrom the
Query`.
fn button_text_system(
button_query: Query<&Children, With<Button>>,
mut text_query: Query<&mut Text>,
run_state: Res<State<RunState>>,
) {
let children = button_query.single();
let first_child_entity = children
.first()
.expect("expect button to have a first child");
let mut text =
text_query.get_mut(*first_child_entity).unwrap();
match run_state.0 {
RunState::Playing => {
text.sections[0].value = "End Game".to_string();
}
RunState::GameOver => {
text.sections[0].value = "New Game".to_string();
}
}
}
To finish off this system, we'll do the same match
on run_state.0
and set the appropriate text for each state. Text
components have sections that can be updated and our Text
s only have a single section, so we can set text.sections[0].value
to the string we want it to display.
match run_state.0 {
RunState::Playing => {
text.sections[0].value = "End Game".to_string();
}
RunState::GameOver => {
text.sections[0].value = "New Game".to_string();
}
}
We can add the button_text_system
to our GameUiPlugin
to finish off the button interactivity.
impl Plugin for GameUiPlugin {
fn build(&self, app: &mut App) {
app.add_startup_system(setup_ui).add_systems((
scoreboard,
button_interaction_system,
button_text_system,
));
}
}
Running the game now will result in a button we can use to end the game, restart the game, and shows different text depending on which RunState
the game is in.