In snake the snake only moves one square per tick. In Bevy we can emulate this with FixedTimesteps.
Add iyes_loopless to our game.
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 and RenderStage. 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
.
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.
use snake::{board::spawn_board, snake::Snake, tick};
In lib.rs
write a public tick
function.
pub fn tick() {
dbg!("tick!");
}
Running cargo run
will now execute our system repeatedly on the delay we’ve specified.
Moving the Snake
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.
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:
- Spawn a new sprite for the new head position
- Despawn the old tail
Despawning 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.
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.