For our approach, starting a new game requires that we respond to a button press. This means setting up a system to handling button interactions.
We want to accomplish a few things for our reset game button:
- Change styles when hovered
- Display different button text based on game
RunState
We can start by introducing a set of new materials just for our button, including the FromWorld implementation for the default values.
struct ButtonMaterials {
normal: Handle<ColorMaterial>,
hovered: Handle<ColorMaterial>,
pressed: Handle<ColorMaterial>,
}
impl FromWorld for ButtonMaterials {
fn from_world(world: &mut World) -> Self {
let mut materials = world
.get_resource_mut::<Assets<ColorMaterial>>()
.unwrap();
ButtonMaterials {
normal: materials
.add(Color::rgb(0.75, 0.75, 0.9).into()),
hovered: materials
.add(Color::rgb(0.7, 0.7, 0.9).into()),
pressed: materials
.add(Color::rgb(0.6, 0.6, 1.0).into()),
}
}
}
We will also have to initialize the new materials in our GameUIPlugin.
impl Plugin for GameUiPlugin {
fn build(&self, app: &mut AppBuilder) {
app.init_resource::<ButtonMaterials>()
.add_startup_system(setup_ui.system())
.add_system(scoreboard.system());
}
}
All of the techniques used to initialize the new materials for the button have been explored earlier in this course.
The button that will allow the user to end their game or start a new game will change materials on hover to indicate to the user that it is clickable. We've already added the materials to our app builder, which allows us to query them 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 a mutable handle to the ColorMaterial used for the button. We'll use this to change the material 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 mutable access to the RunState state machine.
fn button_interaction_system(
button_materials: Res<ButtonMaterials>,
mut interaction_query: Query<
(&Interaction, &mut Handle<ColorMaterial>),
(Changed<Interaction>, With<Button>),
>,
mut run_state: ResMut<State<RunState>>,
) {
Because we're filtering, we can't use single_mut (which is the mutable version of single that we used for the board query elsewhere). This is because single_mut will error if there are no entities or many entities; Basically unless there is always a single entity, it's an error. Instead 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 material) 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 material to the pressed material. This is very similar to the way we set materials on all of the other places we do that. The big difference is that material is a variable and we don't want to set the value of that variable to something new, we want to set the value of the material handle that variable contains, to a new material. To do that we can dereference the variable when setting the value.
You can think of this like the material 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 => {
*material = button_materials.pressed.clone();
...
}
Interaction::Hovered => {
*material = button_materials.hovered.clone();
}
Interaction::None => {
*material = button_materials.normal.clone();
}
}
The final piece of functionality in this system is for setting the new RunState. We can use .current() to get the current RunState. 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.
.set can fail for a few reasons, for example if we're already in the state we're trying to transition to, so we'll .unwrap it because we expect none of the failure cases to happen this time.
match run_state.current() {
RunState::Playing => {
run_state
.set(RunState::GameOver)
.unwrap();
}
RunState::GameOver => {
run_state
.set(RunState::Playing)
.unwrap();
}
}
To run the system we'll add it to our GameUiPlugin.
impl Plugin for GameUiPlugin {
fn build(&self, app: &mut AppBuilder) {
app.init_resource::<ButtonMaterials>()
.add_startup_system(setup_ui.system())
.add_system(scoreboard.system())
.add_system(button_interaction_system.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 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() will give us the first entity that is a child of the button entity. In this case, that is the entity with a Text component. text_query accepts the first entity as an argument and gives us back the Text component that is a child of the button.
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()
.expect("expected only one button");
let mut text = text_query
.get_mut(*children.first().expect(
"expect button to have a first child",
))
.unwrap();
match run_state.current() {
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.current() 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.current() {
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 AppBuilder) {
app.init_resource::<ButtonMaterials>()
.add_startup_system(setup_ui.system())
.add_system(scoreboard.system())
.add_system(button_interaction_system.system())
.add_system(button_text_system.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.
