Tainted\\Coders

Bevy Commands

Bevy version: 0.16Last updated:

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);
  }
}