In Bevy, each Command
represents some mutation we want to perform on our
world. Bevy takes these commands and will apply them to our
world.
Because these commands need a mutable reference to the World
they are
not executed immediately. Instead, they get added to a CommandQueue
which
schedules them to run together in a single system in the order they were added.
Scheduling commands
To execute a command we schedule them in our systems with the Command
system
parameter:
fn spawn_an_entity(
mut commands: Commands,
) {
commands.spawn_empty();
}
This is the most simple command we can write. It will simply spawn an empty
Entity
without any components.
Its important to note that the command did not actually execute here. Instead it
was queued to run the next time all your commands run together. This will happen
once per tick during the apply_deferred
system that was added in the
DefaultPlugins
.
Spawning components and bundles
The most commonly used commands will be for spawning entities with components:
#[derive(Component)]
struct Player;
fn spawn_player(
mut commands: Commands
) {
commands
.spawn_empty()
.insert(Player)
.insert(Transform::default());
}
After we spawn_empty
we actually get back an EntityCommands
for the
particular entity we spawned.
EntityCommands
allow us to insert
components onto the new entity. Insert
itself will return EntityCommands
which lets us chain these calls together.
Spawning an entity with some kind of component is so common that there is a shorter version of this:
fn spawn_player_shorter(
mut commands: Commands
) {
commands
.spawn(Player)
.insert(Transform::default());
}
Here instead of spawn_empty
we use spawn
which takes a component to add to
the newly spawned entity. This also passes back EntityCommands
which we can
use to further insert components.
In fact, spawn
can take a Bundle
of components:
#[derive(Bundle)]
struct PlayerBundle {
player: Player,
transform: Transform,
}
fn spawn_with_bundle(
mut commands: Commands
) {
commands.spawn(PlayerBundle {
player: Player,
transform: Transform::default()
});
}
And because tuples of components are already Bundle
types this can be
shortened even further to:
fn spawn_player_shortest(
mut commands: Commands
) {
commands.spawn((
Player,
Transform::default()
));
}
Commands are delayed until next stage
When you use the system parameter Commands
you are enqueuing your commands to
run when we transition to the next stage.
If we have only one stage even if we have systems that run before or after each other, the actual commands will not be applied to the world until the next frame.
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_systems(
Update,
place_enemy_capital.after(build_board)
)
.run();
}
Lets say we have a system place_enemy_capital
that runs after another system
build_board
. You would think that we would have access to the board components
that spawned. But this is not the case, because when place_enemy_capital
runs
our commands have only been enqueued and not executed.
Since each command requires exclusive access to the World
(we are mutating
state), all queued commands are automatically applied in sequence when the
builtin apply_deferred
system runs.
The solution here would be to ensure that we run our apply_deferred
after we have enqueued our commands but before we run place_enemy_capital
:
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_systems(
Update,
(apply_deferred.after(build_board), place_enemy_capital).chain()
)
.run();
}
The chain
call here just ensures that the first system
apply_deferred.after(build_board)
will run and then
place_enemy_capital
will run right after.
Custom commands
We can implement our own commands to make our logic more understandable.
Imagine we have have a game that spawns a big planet in the middle of the screen. We might have a system that looks like:
#[derive(Component)]
struct Planet {
radius: f32
}
#[derive(Bundle)]
struct PlanetBundle {
planet: Planet,
transform: Transform,
}
impl PlanetBundle {
fn new(radius: f32) -> Self {
Self {
planet: Planet{ radius },
transform: Transform::default()
}
}
}
// Spawns a blue planet in the middle of the screen
fn spawn_planet_manually(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<ColorMaterial>>
) {
let shape = Circle::new(100.0);
let color_material = ColorMaterial::from(Color::rgb(0.0, 0.0, 1.0));
commands.spawn((
PlanetBundle::new(100.0),
ColorMesh2dBundle {
mesh: meshes.add(Mesh::from(shape)).into(),
material: materials.add(color_material),
..Default::default()
}
));
}
However for now for every system that spawns a planet we will need our
meshes
and materials
resources. Its also not entirely clear without our
comment and system name what exactly is happening here.
To shorten our parameter lists and encapsulate our logic we can create our own
custom commands by implementing Command
on a struct holding our parameters:
// Our custom command
struct SpawnPlanet {
radius: f32,
position: Vec2
}
use bevy::sprite::MaterialMesh2dBundle;
impl Command for SpawnPlanet {
fn apply(self, world: &mut World) {
// Resource scope works by removing the resource from the world
// and then running our closure. This lets us mutably borrow
// the world safely multiple times
let mesh_handle = world.resource_scope(|_world, mut meshes: Mut<Assets<Mesh>>| {
let shape = Circle::new(self.radius);
meshes.add(Mesh::from(shape))
});
let material_handle = world.resource_scope(|_world, mut materials: Mut<Assets<ColorMaterial>>| {
let material = ColorMaterial::from(Color::rgb(0.0, 0.0, 1.0));
materials.add(material)
});
world.spawn((
PlanetBundle::new(self.radius),
MaterialMesh2dBundle {
mesh: mesh_handle.into(),
material: material_handle,
transform: Transform::from_translation(self.position.extend(0.)),
..default()
}
));
}
}
fn spawn_planet(mut commands: Commands) {
commands.add(SpawnPlanet {
radius: 100.,
position: Vec2::new(0., 0.)
});
}
Commands::add
will schedule our command to have apply
called with a mutable
reference to a world. This execution happens with all the other commands in the
CommandQueue
during the builtin system apply_deferred
.
Extending the commands API
Sometimes it would be even nicer if we could extend the API of our commands.
Something like commands.spawn_planet(Vec2::Zero, 100.0)
might work even
better. To do so we can use a trait extension.
Trait extensions allow you to add methods to an existing type via a new implementation. We can see an example in bevy_hierarchy:
// https://github.com/bevyengine/bevy/blob/13d46a528ac6e8c2e08e8b9ba436abb9baaefefc/crates/bevy_hierarchy/src/hierarchy.rs#L84
// Trait that holds functions for despawning recursively down the transform hierarchy
pub trait DespawnRecursiveExt {
// Despawns the provided entity alongside all descendants.
fn despawn_recursive(self);
// Despawns all descendants of the given entity.
fn despawn_descendants(&mut self);
}
impl<'w, 's, 'a> DespawnRecursiveExt for EntityCommands<'w, 's, 'a> {
// Despawns the provided entity and its children.
fn despawn_recursive(mut self) {
let entity = self.id();
self.commands().add(DespawnRecursive { entity });
}
fn despawn_descendants(&mut self) {
let entity = self.id();
self.commands().add(DespawnChildrenRecursive { entity });
}
}
Testing commands
We can write tests for our custom commands and manually push
them to
a CommandQueue
and finally trigger an apply
:
use bevy::ecs::system::Command;
use bevy::prelude::*;
struct MyCommand;
impl Command for MyCommand {
fn apply(self, world: &mut World) {
info!("Hello, world!");
}
}
#[cfg(test)]
mod tests {
use bevy::ecs::system::CommandQueue;
#[test]
fn test_my_command() {
let mut world = World::default();
let mut command_queue = CommandQueue::default();
// We could manually add our commands to the queue
command_queue.push(MyCommand);
// We can apply the commands to a given world:
command_queue.apply(&mut world);
// Assert something about the world here
}
}