Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Spawning Snake segments with Custom Commands
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