Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Executing Systems on a FixedTimestep
In snake the snake only moves one square per tick. In Bevy we can emulate this with FixedTimesteps. <aside>⚠️ A big note before we begin moving our snake. In this workshop we’ll be using States and FixedTimesteps together. The current versions of these concepts in Bevy don’t allow them to be used together, but there is a [popular RFC](bevyengine/rfcs#45) called “Stageless” that Bevy is moving toward that allows States and FixedTimesteps to be used together. A third party crate called [iyes_loopless](https://lib.rs/crates/iyes_loopless) is an implementation of this RFC that we’ll be using in this workshop. </aside> Add iyes_loopless to our game. ```rust cargo add iyes_loopless ``` The way Bevy executes our systems is in a series of `Stage`s (If you’re interested these are at least the ones listed in [CoreStage](https://docs.rs/bevy/0.7.0/bevy/app/enum.CoreStage.html) and [RenderStage](https://docs.rs/bevy/0.7.0/bevy/render/enum.RenderStage.html). Because of how modular Bevy is, we can also add our own `Stage`s. Bring the `iyes_loopless::prelude` into scope, as well as `std::time::Duration`. We’ll add our new stage before the `CoreStage::Update` stage. I’ve labelled our new stage `snake_tick` but the name doesn’t really matter as we won’t be referencing it again, and finally we add our new stage. We’ll create a new `FixedTimestepStage` with a step duration. I’ve chosen 100ms as my step timing but you can choose whatever you want here. This will be how fast the snake moves. The from iyes_loopless is actually a container for as many stages as we want to add although we’re only adding one now. That’s why we’re calling `.with_stage` on it and creating a new `SystemStage`. The `SystemStage` will execute our systems in parallel and we can add our systems to the stage with `with_system`. I’ve named the system that will execute on the tick, `tick`. ```rust use bevy::prelude::*; use iyes_loopless::prelude::*; use snake::{board::spawn_board, snake::Snake}; use std::time::Duration; fn main() { App::new() .insert_resource(WindowDescriptor { title: "Snake!".to_string(), ..Default::default() }) .add_plugins(DefaultPlugins) .insert_resource(ClearColor(Color::rgb( 0.52, 0.73, 0.17, ))) .init_resource::<Snake>() .add_startup_system(setup) .add_startup_system(spawn_board) .add_stage_before( CoreStage::Update, "snake_tick", FixedTimestepStage::new(Duration::from_millis( 100, )) .with_stage( SystemStage::parallel().with_system(tick), ), ) .run(); } fn setup(mut commands: Commands) { commands .spawn_bundle(OrthographicCameraBundle::new_2d()); } ``` We’ll write the `tick` system in `lib.rs` so we can bring it into scope in `main.rs` right now. ```rust use snake::{board::spawn_board, snake::Snake, tick}; ``` In `lib.rs` write a public `tick` function. ```rust pub fn tick() { dbg!("tick!"); } ``` Running `cargo run` will now execute our system repeatedly on the delay we’ve specified. Our snake is on the board, and we’ve got a system executing on each tick. It’s time to make the snake move. In `lib.rs`, bring the bevy prelude and `Snake` into scope. ```rust use bevy::prelude::*; use snake::Snake; pub mod board; pub mod colors; pub mod snake; pub fn tick( mut snake: ResMut<Snake>, ) { let mut next_position = snake.segments[0].clone(); next_position.x += 1; snake.segments.push_front(next_position); snake.segments.pop_back(); dbg!(snake); } ``` Then modify the `tick` system to accept a mutable Snake resource. We’ll take the first snake segment (the head of the snake), and clone it to function as our new head position. We’ll just move rightward, so add one to the x position on the new head. Then we take advantage of the `VecDeque` functions to push the new position onto the `Snake` and pop the old tail off. The `dbg` item is especially relevant at the moment. If we `cargo run` we can see the snake move rightward infinitely... but that’s not happening on our board! Our sprites aren’t updating yet even though the resource is. We need to make sure two actions happen: 1. Spawn a new sprite for the new head position 2. Despawn the old tail We can despawn the old tail easy enough. Add `commands` to the arguments for `tick`. We also want to query for all `Position`s on the board. We do this for the purpose of getting the position’s entity id, so the `positions` query is getting all `Position`s that also have `Entity` ids and giving us both values. ```rust use bevy::prelude::*; use board::Position; use snake::Snake; pub mod board; pub mod colors; pub mod snake; pub fn tick( mut commands: Commands, mut snake: ResMut<Snake>, positions: Query<(Entity, &Position)>, ) { let mut next_position = snake.segments[0].clone(); next_position.x += 1; snake.segments.push_front(next_position); let old_tail = snake.segments.pop_back().unwrap(); if let Some((entity, _)) = positions.iter().find(|(_, pos)| pos == &&old_tail) { commands.entity(entity).despawn_recursive(); } } ``` `pop_back` gives us an `Option<Position>`. It should always exist, so we can `.unwrap()` it here, making `old_tail` a `Position`. Our next step is to find the `old_tail` position in the bag of `positions` we queried. By iterating over the `positions` we can `find` the item we want by grabing the `Position` from each item and comparing it to the `old_tail`. `old_tail` needs to be double-referenced because `pos` is an `&&Position`. That’s because `iter` returns shared references to the items it is iterating over *and* `.find` operates on shared references to those references. We use `if let` syntax to destructure the `entity` out of the tuple if we found a match (we should always find a match). The entity id is then used to despawn the entity itself as well as all children. This will remove all of the tails as our snake moves forward, which if we `cargo run` now, will result in nothing on the screen after two passes.
- Loading branch information