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 anOption<&T>so we need tounwrap() the Option and dereference the T, which in this case is an Entity.
Bevy Querys can be used as collections, so we can use this Entity as an argument to text_query.get_mut() and get the Textcomponent on thatEntityfrom theQuery`.
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 Texts 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.