Skip to content

Commit

Permalink
Spawning Snake segments with Custom Commands
Browse files Browse the repository at this point in the history
Spawning a new snake segment in `tick` is something we could copy/paste the code for from the original snake spawn in `spawn_board`.

Instead of that, we’re going to make our own custom Bevy command to make it a bit more clear what’s happening.

The `Commands` we use are how we mutate the `World`, and the trait each of these commands implements is called `Command`. Our goal is to implement `Command` to create our own spawn snake command.

In `board.rs` create a new public struct called `SpawnSnakeSegment` which will be the struct we use to drive our command. Think of this like the arguments to a function.

```rust
pub struct SpawnSnakeSegment {
    pub position: Position,
}
```

Bring command into scope:

```rust
use bevy::{ecs::system::Command, prelude::*};
```

and we can start implementing `Command` for `SpawnSnakeSegment`. The core of the `Command` trait is the `write` function, which gives us `self`, which is our `SpawnSnakeSegment` and a mutable world to modify.

```rust
impl Command for SpawnSnakeSegment {
    fn write(self, world: &mut World) {
        ...
    }
}
```

We are freely available to use this world reference to grab anything we want. We can use it to grab a reference to the `board` by `query`ing the world in the same way our systems do. The biggest difference here is that `world` is a bit lower level than our systems, so the functions that are available to us change a bit.

For example, we can query for `&Board` but we don’t have `.single()` available to us anymore, so we use `.iter`, which also accepts an argument. We know there’s only one board, so we can call `.next()` on the iterator to get an `Option<&Board>` and `.unwrap()` to get the `&Board`.

Also note that we don’t have access to `spawn_bundle` anymore. We need to first `.spawn` an entity and then we can `insert_bundle` on that entity.

Finally also note that we’re using `self` to access the position.

```rust
impl Command for SpawnSnakeSegment {
    fn write(self, world: &mut World) {
        let board = world
            .query::<&Board>()
            .iter(&world)
            .next()
            .unwrap();

        world
            .spawn()
            .insert_bundle(SpriteBundle {
                sprite: Sprite {
                    color: COLORS.snake,
                    custom_size: Some(Vec2::new(
                        TILE_SIZE, TILE_SIZE,
                    )),
                    ..Sprite::default()
                },
                transform: Transform::from_xyz(
                    board.cell_position_to_physical(
                        self.position.x,
                    ),
                    board.cell_position_to_physical(
                        self.position.y,
                    ),
                    2.0,
                ),
                ..Default::default()
            })
            .insert(self.position);
    }
}
```

The core spawning logic here is a copy/paste from the spawn_board logic where we were spawning the snake segments originally.

The code here does have a flaw though and it’s an important one to point out.

```rust
error[E0502]: cannot borrow `*world` as mutable because it is also borrowed as immutable
   --> src/board.rs:119:9
    |
115 |               .iter(&world)
    |                     ------ immutable borrow occurs here
...
119 | /         world
120 | |             .spawn()
    | |____________________^ mutable borrow occurs here
...
130 | /                     board.cell_position_to_physical(
131 | |                         self.position.x,
132 | |                     ),
    | |_____________________- immutable borrow later used here
```

We get a board from the world, which means we’re holding a small piece of the world as a shared reference in the `board` variable.

When we go to use `world.spawn()` we are trying to mutate the world, which requires an exclusive (also known as mutable) reference. This is ok so far because the board hasn’t been used yet.

Unfortunately for us, we use the board *after* the `world.spawn()` to determine the cell positions. This means we’re trying to hold onto the shared reference to the board (which is a piece of `world` while also using a mutable reference to `world`.

We can not hold both a shared reference *and* an exclusive reference at the same time.

Luckily for us, we don’t actually need to use the board that late in the program, we can move our `.cell_position_to_physical` calls up above the `world.spawn()` call which means that we acquire and stop using the `board` before `world.spawn` happens, allowing us to drop the shared reference and take the exclusive reference.

```rust
impl Command for SpawnSnakeSegment {
    fn write(self, world: &mut World) {

        let board = world
            .query::<&Board>()
            .iter(&world)
            .next()
            .unwrap();
        let x = board
            .cell_position_to_physical(self.position.x);
        let y = board
            .cell_position_to_physical(self.position.y);

        world
            .spawn()
            .insert_bundle(SpriteBundle {
                sprite: Sprite {
                    color: COLORS.snake,
                    custom_size: Some(Vec2::new(
                        TILE_SIZE, TILE_SIZE,
                    )),
                    ..Sprite::default()
                },
                transform: Transform::from_xyz(x, y, 2.0),
                ..Default::default()
            })
            .insert(self.position);
    }
}
```

Back up in `spawn_board` we can now use our custom command by calling `commands.add` with out `SpawnSnakeSegment` that implements `Command`. The segment can be dereferenced, which will use the `Copy` implementation on `Position`, as I’m doing here or cloned with `.clone` if that is how you want to write it. It amounts to the same thing.

```rust
for segment in snake.segments.iter() {
    commands.add({
        SpawnSnakeSegment { position: *segment }
    });
}
```

Don’t forget to remove the extra `Board` we built in `spawn_board`.

```rust
let board = Board::new(20);
```

Back in `lib.rs`, right after `push_front`, add the same `SpawnSnakeSegment` command using `next_position` to insert the new snake head.

```rust
snake.segments.push_front(next_position);
commands.add({
    SpawnSnakeSegment {
        position: next_position,
    }
});
```

After running `cargo run` this will result in the snake running away off the right side of the screen.
  • Loading branch information
ChristopherBiscardi committed Apr 22, 2022
1 parent c3979c7 commit f4e9e0f
Show file tree
Hide file tree
Showing 2 changed files with 35 additions and 16 deletions.
43 changes: 28 additions & 15 deletions src/board.rs
@@ -1,4 +1,4 @@
use bevy::prelude::*;
use bevy::{ecs::system::Command, prelude::*};
use itertools::Itertools;

use crate::{colors::COLORS, snake::Snake};
Expand Down Expand Up @@ -84,29 +84,42 @@ pub fn spawn_board(
})
.insert(board);

let board = Board::new(20);

for segment in snake.segments.iter() {
commands
.spawn_bundle(SpriteBundle {
commands.add({
SpawnSnakeSegment { position: *segment }
});
}
}

pub struct SpawnSnakeSegment {
pub position: Position,
}

impl Command for SpawnSnakeSegment {
fn write(self, world: &mut World) {
let board = world
.query::<&Board>()
.iter(&world)
.next()
.unwrap();
let x = board
.cell_position_to_physical(self.position.x);
let y = board
.cell_position_to_physical(self.position.y);

world
.spawn()
.insert_bundle(SpriteBundle {
sprite: Sprite {
color: COLORS.snake,
custom_size: Some(Vec2::new(
TILE_SIZE, TILE_SIZE,
)),
..Sprite::default()
},
transform: Transform::from_xyz(
board.cell_position_to_physical(
segment.x,
),
board.cell_position_to_physical(
segment.y,
),
2.0,
),
transform: Transform::from_xyz(x, y, 2.0),
..Default::default()
})
.insert(segment.clone());
.insert(self.position);
}
}
8 changes: 7 additions & 1 deletion src/lib.rs
@@ -1,5 +1,5 @@
use bevy::prelude::*;
use board::Position;
use board::{Position, SpawnSnakeSegment};
use snake::Snake;

pub mod board;
Expand All @@ -14,6 +14,12 @@ pub fn tick(
let mut next_position = snake.segments[0].clone();
next_position.x += 1;
snake.segments.push_front(next_position);
commands.add({
SpawnSnakeSegment {
position: next_position,
}
});

let old_tail = snake.segments.pop_back().unwrap();
if let Some((entity, _)) =
positions.iter().find(|(_, pos)| pos == &&old_tail)
Expand Down

0 comments on commit f4e9e0f

Please sign in to comment.