Setting up the UI to display the score is going to make use of everything we know about building UI and binding data, but it’s nothing we haven’t seen before in this workshop.
All of our work in this lesson will be in ui.rs
.
The layout we’re going with is a three panel layout. The center panel is going to be the size of the game board, so that we don’t put any in-game UI over it.
The center panel will also contain our menu systems, which only show when the game isn’t being played. The fact that we can wrap our existing UI elements in this center panel without changing anything is a lucky coincidence.
The left and right panels are structured the same. Each has a title, a score and a time.
The times are Duration
s measured in seconds.
<Element styles={Some(row_styles)}>
<Element styles={Some(left_styles)}>
<Text
size={50.0}
content={"Current Run".to_string()}
/>
<Text
size={50.0}
content={score.to_string()}
/>
<Text
size={25.0}
content={format!("{} seconds",current_run_time.as_secs().to_string())}
/>
</Element>
<Element styles={Some(gameboard_spacer_styles)}>
<If condition={show_menus}>
...
</If>
</Element>
<Element styles={Some(right_styles)}>
<Text
size={50.0}
content={"High Score".to_string()}
/>
<Text
size={50.0}
content={high_score.score.to_string()}
/>
<Text
size={25.0}
content={format!("{} seconds",high_score.time.as_secs().to_string())}
/>
</Element>
</Element>
The styles we use are styles we’ve seen before. We’re mostly concerned with making sure the left panel contents are pushed up against the right hand side.
Any width
or height
here is the game board size (which you can check by debugging out board.physical_size
). We could have chosen to automatically calculate these from the board size for a more responsive UI, but we don’t let the user change the board size currently anyway so I hardcoded it.
let gameboard_spacer_styles = Style {
bottom: StyleProp::Value(Units::Stretch(1.0)),
layout_type: StyleProp::Value(LayoutType::Column),
top: StyleProp::Value(Units::Stretch(1.0)),
width: StyleProp::Value(Units::Pixels(600.0)),
..Default::default()
};
let row_styles = Style {
layout_type: StyleProp::Value(LayoutType::Row),
padding_top: StyleProp::Value(Units::Stretch(1.0)),
padding_bottom: StyleProp::Value(Units::Stretch(
1.0,
)),
..Default::default()
};
let left_styles = Style {
padding_left: StyleProp::Value(Units::Stretch(1.0)),
height: StyleProp::Value(Units::Pixels(600.0)),
border: StyleProp::Value(Edge::all(25.0)),
..Default::default()
};
let right_styles = Style {
height: StyleProp::Value(Units::Pixels(600.0)),
border: StyleProp::Value(Edge::all(25.0)),
..Default::default()
};
We then have to deal with binding the data we care about to Kayak’s UI.
We add three new binding systems: bind_score
, bind_high_score
, and bind_timer
.
Only the logic for bind_timer
is new. We convert the Timer
to a Duration
for the binding.
bind_timer
matches on the state of the Timer
.
If the Timer
has a runtime
then the game must be over and we should use that Duration
value.
Otherwise, if the timer has a start
but no runtime
we must be in the middle of a game, so we get the elapsed
time and store that Duration
in the binding.
and Finally, if both Timer
fields are None
, which happens when the game first opens, then we set the value to a Duration
of 0.
impl Plugin for UiPlugin {
fn build(&self, app: &mut bevy::prelude::App) {
app.add_plugin(BevyKayakUIPlugin)
.insert_resource(bind(STARTING_GAME_STATE))
.add_startup_system(game_ui)
.add_system(bind_gamestate)
.add_system(bind_game_settings)
.add_system(bind_score)
.add_system(bind_high_score)
.add_system(bind_timer);
}
}
...
pub fn bind_score(
state: Res<Score>,
binding: Res<Binding<Score>>,
) {
if state.is_changed() {
binding.set(state.clone());
}
}
pub fn bind_high_score(
state: Res<HighScore>,
binding: Res<Binding<HighScore>>,
) {
if state.is_changed() {
binding.set(state.clone());
}
}
pub fn bind_timer(
state: Res<Timer>,
binding: Res<Binding<Duration>>,
) {
match *state {
Timer {
start: _,
runtime: Some(duration),
} => {
binding.set(duration);
}
Timer {
start: Some(instant),
runtime: None,
} => {
binding.set(instant.elapsed());
}
_ => {
binding.set(Duration::from_secs(0));
}
};
}
In game_ui
we need to initialize the bindings. We do this mostly by grabbing the current value of each resource and using that for the binding.
pub fn game_ui(
...
score: Res<Score>,
high_score: Res<HighScore>,
) {
...
commands.insert_resource(bind(score.clone()));
commands.insert_resource(bind(high_score.clone()));
commands.insert_resource(bind(Duration::from_secs(0)));
With the bindings in place, we can go back into the GameMenu
widget and query for each of the bindings, binding them to the GameMenu
widget so that the UI updates when these values change.
let score = {
let score = context
.query_world::<Res<Binding<Score>>, _, _>(
|state| state.clone(),
);
context.bind(&score);
score.get().score
};
let high_score = {
let score = context
.query_world::<Res<Binding<HighScore>>, _, _>(
|state| state.clone(),
);
context.bind(&score);
score.get()
};
let current_run_time = {
let score = context
.query_world::<Res<Binding<Duration>>, _, _>(
|state| state.clone(),
);
context.bind(&score);
score.get()
};
and now we can cargo run
.
Congrats you now have a fully functioning game of Snake with high scores, character selection, and more!