Exclusive Systems

Exclusive systems are systems that Bevy will not run in parallel with any other system. They can have full unrestricted access to the whole ECS World, by taking a &mut World parameter.

Inside of an exclusive system, you have full control over all data stored in the ECS. You can do whatever you want.

Some example situations where exclusive systems are useful:

  • Dump various entities and components to a file, to implement things like saving and loading of game save files, or scene export from an editor
  • Directly spawn/despawn entities, or insert/remove resources, immediately with no delay (unlike when using Commands from a regular system)
  • Run arbitrary systems and schedules with your own custom control flow logic

See the direct World access page to learn more about how to do such things.

fn do_crazy_things(world: &mut World) {
    // we can do anything with any data in the Bevy ECS here!
}

You need to add exclusive systems to the App, just like regular systems. All scheduling APIs (ordering, run conditions, sets) are supported and work the same as with regular systems.

app.add_systems(Update,
    do_crazy_things
        .run_if(needs_crazy_things)
        .after(do_regular_things)
        .before(other_things)
);

Exclusive System Parameters

There are a few other things, besides &mut World, that can be used as parameters for exclusive systems:

SystemState can be used to emulate a normal system. You can put regular system parameters inside. This allows you to access the World as you would from a normal system, but you can confine it to a specific scope inside your function body, making it more flexible.

QueryState is the same thing, but for a single query. It is a simpler alternative to SystemState for when you just need to be able to query for some data.

use bevy::ecs::system::SystemState;

fn spawn_particles_for_enemies(
    world: &mut World,
    // behaves sort of like a query in a regular system
    q_enemies: &mut QueryState<&Transform, With<Enemy>>,
    // emulates a regular system with an arbitrary set of parameters
    params: &mut SystemState<(
        ResMut<MyGameSettings>,
        ResMut<MyParticleTracker>,
        Query<&mut Transform, With<Player>>,
        EventReader<MyDamageEvent>,
        // yes, even Commands ;)
        Commands,
    )>,
    // local resource, just like in a regular system
    mut has_run_once: Local<bool>,
) {
    // note: unlike with a regular Query, we need to provide the world as an argument.
    // The world will only be "locked" for the duration of this loop
    for transform in q_enemies.iter(world) {
        // TODO: do something with the transforms
    }

    // create a scope where we can access our things like a regular system
    {
        let (mut settings, mut tracker, mut q_player, mut evr, commands) =
            params.get_mut(world);

        // TODO: do things with our resources, query, events, commands, ...
    }

    // because our SystemState includes Commands,
    // we must apply them when we are done
    params.apply(world);

    // we are now free to directly spawn entities
    // because the World is no longer used by anything
    // (the SystemState and the QueryState are no longer accessing it)

    world.spawn_batch((0..10000) // efficiently spawn 10000 particles
        .map(|_| SpriteBundle {
            // ...
            ..Default::default()
        })
    );

    // and, of course, we can use our Local
    *has_run_once = true;
}

Note: if your SystemState includes Commands, you must call .apply() after you are done! That is when the deferred operations queued via commands will be applied to the World.

Performance Considerations

Exclusive systems, by definition, limit parallelism and multi-threading, as nothing else can access the same ECS World while they run. The whole schedule needs to come to a stop, to accomodate the exclusive system. This can easily introduce a performance bottleneck.

Generally speaking, you should avoid using exclusive systems, unless you need to do something that is only possible with them.

On the other hand, they can have less overhead (by avoiding Commands), and so can be faster for some use cases.

Commands has a lot of overhead, as it needs to store your data in a queue and then process / copy it into the World later. Directly spawning entities into the World is much more efficient.

Some examples for when exclusive systems can be faster:

  • You want to spawn/despawn a ton of entities.
    • Example: Setup/cleanup for your whole game map.
  • You want to do it every frame.
    • Example: Managing hordes of enemies.

You can think of it this way: in order to apply Commands, Bevy effectively needs to run an "exclusive system" internally to do it for you. Normally, that is hidden / automatic. By just writing an exclusive system yourself, you are saving on the additional regular system that you would normally run.

On the other hand, if you have a system that runs every frame, but only needs to manage entities every once in a while, then Commands are likely to be more performant, because, by making your system a normal system, you avoid making it a bottleneck for parallelism.

Some examples for when normal systems can be faster:

  • You need to check some stuff every frame, and maybe spawn/despawn something sometimes.
    • Example: Despawn enemies when they reach 0 HP.
    • Example: Add/remove some UI elements depending on what is happening in-game.