Tainted\\Coders

Custom Commands

Bevy version: 0.19Last updated:

Lets say we have a simple system that spawns something in our game:

#[derive(Resource)]
struct BoidAssets {
  mesh: Handle<Mesh>,
  material: Handle<ColorMaterial>,
}

fn boid(position: Vec2) -> impl Scene {
  bsn! {
    Boid
    Position(position)
  }
}

fn spawn_boid(mut commands: Commands, assets: Res<BoidAssets>) {
  commands.spawn_scene(boid(Vec2::new(0., 0.)));
}

We can extract out our logic into a much more testable and reusable Command by using custom commands:

pub struct SpawnBoid {
  pub position: Vec2,
}

fn boid(position: Vec2) -> impl Scene {
  bsn! {
    Boid
    Position(position)
  }
}

impl SpawnBoid {
  pub fn random() -> Self {
    let mut rng = rand::rng();
    let x = rng.random_range(-200.0..200.0);
    let y = rng.random_range(-200.0..200.0);

    Self {
      position: Vec2 { x, y },
    }
  }
}

impl Command for SpawnBoid {
  type Out = ();

  fn apply(self, world: &mut World) {
    world.spawn_scene(boid(self.position)).unwrap();
  }
}

fn setup(mut commands: Commands) {
  for _ in 0..100 {
    commands.queue(SpawnBoid::random());
  }
}

We define custom commands by implementing Command which expects us to define apply to affect our World when commands are scheduled to run.

Instead of your normal system parameters you have direct mutable access to the World.

impl Command for SpawnBoid {
  fn apply(self, world: &mut World) {
    // ...
  }
}

To call our commands we simply call Commands::queue and pass the struct that implements Command with whatever options we want.

commands.queue(SpawnBoid::random());

This can be a great way to break up complicated logic for creating nested entities, especially with UI nodes. It also makes the logic much easier to test as all we need to do is pass in a World we can create in our tests and observe the changes.

Additionally, we can make custom EntityCommand and do something similar but for a specific entity instead of the whole world:

struct RemoveEntity;

impl EntityCommand for RemoveEntity {
  type Out = ();

  fn apply(self, entity: EntityWorldMut) {
    entity.despawn();
  }
}

fn cleanup_things(mut commands: Commands, query: Query<Entity>) {
  for entity in &query {
    commands.get_entity(entity).unwrap().queue(RemoveEntity);
  }
}