Bevy Minesweeper: Generic States

qongzi

Qongzi

Posted on February 21, 2022

Bevy Minesweeper: Generic States

Check the repository

Our board plugin is customizable through BoardOptions, but pour app can't interact with it.
We need to make our BoardPlugin generic, to allow control through states.

Plugin

Lets edit our plugin structure:

// lib.rs
use bevy::ecs::schedule::StateData;

pub struct BoardPlugin<T> {
    pub running_state: T,
}

impl<T: StateData> Plugin for BoardPlugin<T> {
    fn build(&self, app: &mut App) {
        // ..
    }
}

impl<T> BoardPlugin<T> {
    // ..
}
Enter fullscreen mode Exit fullscreen mode

Our plugin cannot know what state the app using it has defined, it needs to be generic.

We can now change our systems structure to take in account this running_state:

// lib.rs
// ..
fn build(&self, app: &mut App) {
    // When the running states comes into the stack we load a board
        app.add_system_set(
            SystemSet::on_enter(self.running_state.clone()).with_system(Self::create_board),
        )
        // We handle input and trigger events only if the state is active
        .add_system_set(
            SystemSet::on_update(self.running_state.clone())
                .with_system(systems::input::input_handling)
                .with_system(systems::uncover::trigger_event_handler),
        )
        // We handle uncovering even if the state is inactive
        .add_system_set(
            SystemSet::on_in_stack_update(self.running_state.clone())
                .with_system(systems::uncover::uncover_tiles),
        )
        .add_event::<TileTriggerEvent>();
}
Enter fullscreen mode Exit fullscreen mode

Bevy's states are in a stack:

  • if a state is at the top of the stack it is considered active
  • if a state is in the stack but not at the top it is considered inactive
  • if a state leaves the stack it is considered exited
  • if a state enters the stack it is considered entered

So what did we do here:

  • Since we now constrain systems with state conditions, everything is a SystemSet
  • Instead of a startup_system we call our setup_board system when our state enters the stack
  • We handle our input, and the trigger events only if the state is active (we allow for paused states)
  • The uncovering system should not be paused, so we run it if the state is in the stack, active or not.

With this configuration the app using the plugin can have menus or other stuff and trigger our board generation with the running_state.
But we need to be able to clean up the board in case the states exits the stack.

For that the Board resource should have a reference to its own Entity to despawn it with all its children:

// board.rs
// ..
#[derive(Debug)]
pub struct Board {
    pub tile_map: TileMap,
    pub bounds: Bounds2,
    pub tile_size: f32,
    pub covered_tiles: HashMap<Coordinates, Entity>,
+    pub entity: Entity,
}
// ..
Enter fullscreen mode Exit fullscreen mode

Let's edit our create_board system to retrieve the entity:

// lib.rs
fn create_board(
    // ..
) {
    // .. 
    let board_entity = commands
    //        .spawn()
    //        .insert(Name::new("Board"))
    // ..
    .id();
    // ..
    commands.insert_resource(Board {
            // ..
            entity: board_entity,
        })

}
Enter fullscreen mode Exit fullscreen mode

Now we can register a cleaning system for our plugin:

// lib.rs

// ..
fn build(&self, app: &mut App) {
    //..
    .add_system_set(
        SystemSet::on_exit(self.running_state.clone())
            .with_system(Self::cleanup_board),
    )
    // ..
}

impl<T> BoardPlugin<T> {
    // ..
    fn cleanup_board(board: Res<Board>, mut commands: Commands) {
        commands.entity(board.entity).despawn_recursive();
        commands.remove_resource::<Board>();
    }
}
Enter fullscreen mode Exit fullscreen mode

What about all the tiles, the texts, the sprites, the covers, etc ?

Since we spawned every board entity as children to our board_entity, using despawn_recursive will also despawn its children:

  • the background
  • the tiles
  • the tile texts
  • the tile sprites
  • the tile covers
  • etc.

App

Let's define some basic states:

// main.rs
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum AppState {
    InGame,
    Out,
}

fn main() {
    // ..
    .add_state(AppState::InGame)
    .add_plugin(BoardPlugin {
        running_state: AppState::InGame,
    })
}
Enter fullscreen mode Exit fullscreen mode

If we run the app now, nothing has changed, but if we edit the states we can completely control our board systems:

// main.rs
use bevy::log;

fn main() {
    // ..
    // State handling
    .add_system(state_handler);
    // ..
}

fn state_handler(mut state: ResMut<State<AppState>>, keys: Res<Input<KeyCode>>) {
    if keys.just_pressed(KeyCode::C) {
        log::debug!("clearing detected");
        if state.current() == &AppState::InGame {
            log::info!("clearing game");
            state.set(AppState::Out).unwrap();
        }
    }
    if keys.just_pressed(KeyCode::G) {
        log::debug!("loading detected");
        if state.current() == &AppState::Out {
            log::info!("loading game");
            state.set(AppState::InGame).unwrap();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Everything should be familiar here,

  • state is wrapped in ResMut<>, because states are handled like any resource but with an additional wrapper: State<>
  • keys is an Input<> argument, allowing to check for keyboard interaction using KeyCode (it can be used with MouseButton for mouse interaction)

Now pressing C should cleanup the board entirely, and pressing G should generate a new board.

Exercise

States can be tricky, so it's good to practice using it.

Implement the following features:

  1. When I press Escape the game pauses and I can't interact with the board, if I press Escape again the game resumes.
  2. When I press G a new board generates, without having to press C first.

Give me your answers on Twitter at @ManevilleF


Previous Chapter -- Next Chapter


Author: Félix de Maneville
Follow me on Twitter

Published by Qongzi

💖 💪 🙅 🚩
qongzi
Qongzi

Posted on February 21, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Bevy Minesweeper: Introduction
rust Bevy Minesweeper: Introduction

February 21, 2022

Bevy Minesweeper: WASM build
rust Bevy Minesweeper: WASM build

February 21, 2022

Bevy Minesweeper: Marking Tiles
rust Bevy Minesweeper: Marking Tiles

February 21, 2022

Bevy Minesweeper: The Board
rust Bevy Minesweeper: The Board

February 21, 2022