Skip to content

Commit

Permalink
Executing Systems on a FixedTimestep
Browse files Browse the repository at this point in the history
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
ChristopherBiscardi committed Apr 22, 2022
1 parent 6f962e4 commit c3979c7
Show file tree
Hide file tree
Showing 4 changed files with 47 additions and 1 deletion.
13 changes: 13 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Expand Up @@ -8,5 +8,6 @@ edition = "2021"
[dependencies]
bevy = "0.7.0"
itertools = "0.10.3"
iyes_loopless = "0.4.0"

[features]
20 changes: 20 additions & 0 deletions src/lib.rs
@@ -1,3 +1,23 @@
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();
}
}
14 changes: 13 additions & 1 deletion src/main.rs
@@ -1,5 +1,7 @@
use bevy::prelude::*;
use snake::{board::spawn_board, snake::Snake};
use iyes_loopless::prelude::*;
use snake::{board::spawn_board, snake::Snake, tick};
use std::time::Duration;

fn main() {
App::new()
Expand All @@ -14,6 +16,16 @@ fn main() {
.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();
}

Expand Down

0 comments on commit c3979c7

Please sign in to comment.