Skip to content

Commit

Permalink
Showing and hiding the UI
Browse files Browse the repository at this point in the history
In the `GameState::Menu` game state, we need to show our wonderful new menu.

In the `GameState::Playing` game state, we need to *hide* our wonderful UI.

Kayak requires that we `bind` to resources we want to expose to the UI before we can use it to show/hide the UI.

We need to add a new resource that is `bind(GameState::Menu)`. This is the binding that will update over time.

Add a new system called `bind_gamestate` that accesses the `CurrentState` and the `Binding` for `GameState`.

We can use `is_changed()` to detect if the `CurrentState` has changed, and set the binding to the current `GameState` value. This will continually update our binding when the `CurrentState` changes.

```rust
impl Plugin for UiPlugin {
    fn build(&self, app: &mut bevy::prelude::App) {
        app.add_plugin(BevyKayakUIPlugin)
            .insert_resource(bind(GameState::Menu))
            .add_startup_system(game_ui)
            .add_system(bind_gamestate);
    }
}

pub fn bind_gamestate(
    state: Res<CurrentState<GameState>>,
    binding: Res<Binding<GameState>>,
) {
    if state.is_changed() {
        binding.set(state.0);
    }
}
```

You might think that we could insert this resource by querying the `CurrentState` in `game_ui` but unfortunately, the iyes_loopless `GameState` isn’t accessible in startup systems (our custom stage only inserts it after the startup systems have run), so our `game_ui` startup system can’t access it.

If we tried to, with code that looks like this:

```rust
pub fn game_ui(
    mut commands: Commands,
    mut font_mapping: ResMut<FontMapping>,
    asset_server: Res<AssetServer>,
    gamestate: Res<CurrentState<GameState>>
) {...}
```

Then we would get this error at runtime: `Resource requested by snake::ui::game_ui does not exist: iyes_loopless::state::CurrentState<snake::GameState>`

```rust
❯ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.08s
     Running `target/debug/snake`
2022-04-23T12:50:49.775340Z  INFO bevy_render::renderer: AdapterInfo { name: "Apple M1 Max", vendor: 0, device: 0, device_type: DiscreteGpu, backend: Metal }
thread 'Compute Task Pool (2)' panicked at 'Resource requested by snake::ui::game_ui does not exist: iyes_loopless::state::CurrentState<snake::GameState>', /Users/chris/.cargo/registry/src/github.com-1ecc6299db9ec823/bevy_ecs-0.7.0/src/system/system_param.rs:319:17
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
thread 'main' panicked at 'task has failed', /Users/chris/.cargo/registry/src/github.com-1ecc6299db9ec823/async-task-4.2.0/src/task.rs:425:45
thread 'main' panicked at 'Task thread panicked while executing.: Any { .. }', /Users/chris/.cargo/registry/src/github.com-1ecc6299db9ec823/bevy_tasks-0.7.0/src/task_pool.rs:77:21
```

We do want to sync up the state that we’re actually starting the game with and the state that we initialize the binding with though.

We can create a public const for this in `lib.rs`.

```rust
pub const STARTING_GAME_STATE: GameState = GameState::Menu;
```

and then use it in our `add_loopless_state` in `main.rs`.

```rust
.add_loopless_state(STARTING_GAME_STATE)
```

as well as our `bind` in `ui.rs`.

```rust
.insert_resource(bind(STARTING_GAME_STATE))
```

In our `GameMenu` widget, we can create a new boolean value called `show_menus`. This will control whether or not our menus will show.

Using `context` we can query the world for the `Binding<GameState>` and clone the binding out for our own use. This is similar to how we sent the app exit event.

The `Binding` type is a struct that holds an id and an `Arc` wrapped value, which means cloning is cheap.

Then we need to bind the relevant value to this widget so that we can react to updates. This is done with `context.bind`.

We can `.get` the value from the binding and check it against `Menu` to determine if the game is in the `Menu` state or not.

```rust
let show_menus = {
    let gamestate = context
        .query_world::<Res<Binding<GameState>>, _, _>(
            |state| state.clone(),
        );

    context.bind(&gamestate);
    gamestate.get() == GameState::Menu
};
```

We can wrap the `If` component around our entire menu and set the `condition` to our `show_menus` boolean.

```rust
rsx! {
    <If condition={show_menus}>
        <Background
            styles={Some(container_styles)}
        >
        ...
        </Background>
    </If>
    }
```

A `STARTING_GAME_STATE` of `GameState::Menu` will now show us the menu.

![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/59f057e7-477a-44d9-9ed3-904002d3c2a7/Untitled.png)

While a `STARTING_GAME_STATE` of `GameState::Playing` will let us play the game, then transition to showing the menu when we hit a wall.
  • Loading branch information
ChristopherBiscardi committed Apr 23, 2022
1 parent e220814 commit 3385e92
Show file tree
Hide file tree
Showing 3 changed files with 66 additions and 37 deletions.
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ pub mod food;
pub mod snake;
pub mod ui;

pub const STARTING_GAME_STATE: GameState = GameState::Menu;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum GameState {
Menu,
Expand Down
4 changes: 2 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use iyes_loopless::prelude::*;
use snake::{
board::spawn_board, controls::ControlsPlugin,
food::FoodPlugin, snake::Snake, tick, ui::UiPlugin,
GameState,
GameState, STARTING_GAME_STATE,
};
use std::time::Duration;

Expand All @@ -21,7 +21,7 @@ fn main() {
0.52, 0.73, 0.17,
)))
.init_resource::<Snake>()
.add_loopless_state(GameState::Playing)
.add_loopless_state(STARTING_GAME_STATE)
.add_startup_system(setup)
.add_startup_system(spawn_board)
.add_stage_before(
Expand Down
97 changes: 62 additions & 35 deletions src/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,48 @@ use bevy::{
ResMut,
},
};
use iyes_loopless::state::CurrentState;
use kayak_ui::{
bevy::{
BevyContext, BevyKayakUIPlugin, FontMapping,
UICameraBundle,
},
core::{
render, rsx,
bind, render, rsx,
styles::{
Corner, Edge, LayoutType, Style, StyleProp,
Units,
},
widget, Color, EventType, Index, OnEvent,
widget, Binding, Bound, Color, EventType, Index,
MutableBound, OnEvent,
},
widgets::{App, Background, Button, Text},
widgets::{App, Background, Button, If, Text},
};

use crate::{GameState, STARTING_GAME_STATE};

pub struct UiPlugin;

impl Plugin for UiPlugin {
fn build(&self, app: &mut bevy::prelude::App) {
app.add_plugin(BevyKayakUIPlugin)
.add_startup_system(game_ui);
.insert_resource(bind(STARTING_GAME_STATE))
.add_startup_system(game_ui)
.add_system(bind_gamestate);
}
}

pub fn bind_gamestate(
state: Res<CurrentState<GameState>>,
binding: Res<Binding<GameState>>,
) {
if state.is_changed() {
binding.set(state.0);
}
}

// THIS ONLY RUNS ONCE. VERY IMPORTANT FACT.
fn game_ui(
pub fn game_ui(
mut commands: Commands,
mut font_mapping: ResMut<FontMapping>,
asset_server: Res<AssetServer>,
Expand Down Expand Up @@ -82,6 +97,16 @@ fn GameMenu() {
..Default::default()
};

let show_menus = {
let gamestate = context
.query_world::<Res<Binding<GameState>>, _, _>(
|state| state.clone(),
);

context.bind(&gamestate);
gamestate.get() == GameState::Menu
};

let on_click_new_game =
OnEvent::new(|_, event| match event.event_type {
EventType::Click(..) => {
Expand Down Expand Up @@ -112,36 +137,38 @@ fn GameMenu() {
});

rsx! {
<Background
styles={Some(container_styles)}
>
<Button
on_event={Some(on_click_new_game)}
styles={Some(button_styles)}
>
<Text
size={20.0}
content={"New Game".to_string()}
/>
</Button>
<Button
on_event={Some(on_click_settings)}
styles={Some(button_styles)}
>
<Text
size={20.0}
content={"Settings".to_string()}
/>
</Button>
<Button
on_event={Some(on_click_exit)}
styles={Some(button_styles)}
<If condition={show_menus}>
<Background
styles={Some(container_styles)}
>
<Text
size={20.0}
content={"Exit".to_string()}
/>
</Button>
</Background>
<Button
on_event={Some(on_click_new_game)}
styles={Some(button_styles)}
>
<Text
size={20.0}
content={"New Game".to_string()}
/>
</Button>
<Button
on_event={Some(on_click_settings)}
styles={Some(button_styles)}
>
<Text
size={20.0}
content={"Settings".to_string()}
/>
</Button>
<Button
on_event={Some(on_click_exit)}
styles={Some(button_styles)}
>
<Text
size={20.0}
content={"Exit".to_string()}
/>
</Button>
</Background>
</If>
}
}

0 comments on commit 3385e92

Please sign in to comment.