Bevy Commands
Commands are what we use to change the state of our World
in a way that is more performant than letting each system mutate the world directly.
Each Command
represents a mutation we want to apply. Commands are all scheduled together and execute inside a single function that receives exclusive &mut World
reference.
Because each of your commands needs this mutable reference to the world, it cannot run while other systems might be running in parallel.
So, Bevy schedules all your commands to all run together inside a single system in the same order they are added to the CommandQueue
. We add to this queue through the Commands
system parameter.
Scheduling commands
To execute a command, first we have to schedule them.
Bevy comes with a Main
schedule which is where your Startup
and Update
systems are scheduled to run.
We don't manually add them to a schedule via a CommandQueue
, instead, we use a system parameter: Commands
, which is the public API to the queue.
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 when it runs together with your other commands.
To despawn that same entity we would also use a command:
fn despawn_an_entity(
mut commands: Commands,
query: Query<Entity>
) {
for entity in query.iter() {
commands.entity(entity).despawn();
}
}
Commands
is the system parameter holding all of the methods to schedule changes, including custom commands you can write yourself.
Its important to note: neither command actually executes here. Instead they were queued to run the next time all your commands run together. This will happen any time we transition to the next schedule during the ApplyDeferred
system that was added by the DefaultPlugins
.
Spawning components
We spawn components and entities through commands. To do so we use the Commands
system parameter.
#[derive(Component)]
struct Player;
fn spawn_player(mut commands: Commands) {
// Here we are `Commands`
commands
.spawn_empty()
// We are now an `EntityCommands`
// for the entity we just spawned
.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 on the particular entity we spawned.
Tuples of components are already Bundle
types this can be shortened even further to:
fn spawn_player_shortest(mut commands: Commands) {
let bundle = (Player, Transform::from_xyz(1., 1., 1.));
commands.spawn(bundle);
}
Bundles used to be the preferred way to group up components, however since 0.15
Bevy has moved to using required components. The same bundle from above could be written instead like:
#[derive(Component)]
#[require(Transform)]
struct Player;
fn spawn_with_bundle(mut commands: Commands) {
commands.spawn(Player);
}
These required components will have their defaults added if we do not override them by providing our own. This is much less boiler plate and much easier to keep in our heads when trying to think how to spawn components that depend on each other.
Commands are delayed until the next schedule
When you use the system parameter Commands
you are enqueuing your commands to run when we transition to the next Schedule
.
A Schedule
is one part of the lifecycle of a frame. These schedules are collections of systems and instructions on exactly how to run them. We identify them by their ScheduleLabel
.
For example, Startup
and Update
are both common schedule labels you have likely seen before.
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, take_the_castle.after(raising_the_gates))
.run();
}
Lets say we have a system take_the_castle
that runs after another system raise_the_gates
.
If you're smart, you'd think that by the time you go to take_the_castle
you have already raised the gates.
This however, is not the case, because when take_the_castle
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 applied in sequence when the builtin ApplyDeferred
system runs.
Custom commands
We can implement our own custom 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)]
#[require(Transform)]
struct Planet {
radius: f32,
}
impl Planet {
fn new(radius: f32) -> Self {
Self { radius }
}
}
// 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 size = 100.0;
let shape = Circle::new(size);
let color = Color::srgb(0.0, 0.0, 1.0);
commands.spawn((
Planet::new(size),
Mesh2d(meshes.add(shape)),
MeshMaterial2d(materials.add(color)),
));
}
However, 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,
}
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 circle = Circle::new(self.radius);
meshes.add(circle)
});
let color_material = world.resource_scope(
|_world, mut materials: Mut<Assets<ColorMaterial>>| {
let blue = Color::srgb(0.0, 0.0, 1.0);
materials.add(blue)
},
);
world.spawn((
Planet::new(self.radius),
Mesh2d(mesh_handle.clone()),
MeshMaterial2d(color_material.clone()),
Transform::from_translation(self.position.extend(0.)),
));
}
}
That's a lot more code, but now our ability to spawn planets has become much more simplified:
fn spawn_planet(mut commands: Commands) {
commands.queue(SpawnPlanet {
radius: 100.,
position: Vec2::new(0., 0.),
});
}
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 Pathfinder {
fn find_path(&self, start: Vec2, end: Vec2) -> Vec<Vec2>;
}
impl<'w, 's> Pathfinder for Commands<'w, 's> {
fn find_path(&self, start: Vec2, end: Vec2) -> Vec<Vec2> {
info!("Finding path...");
// Somehow find a path from start to end
Vec::new()
}
}
We could then use our custom API directly on the Commands
system parameter:
fn find_a_path(commands: &mut Commands, start: Vec2, end: Vec2) {
let path = commands.find_path(start, end);
info!("Found path: {:?}", path);
}
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::world::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);
}
}