Skip to content
rusthacks.dev
Systems

Bevy Systems Queries and Resources Explained Simply

Bevy is an entity component system: entities are ids, components are data, systems are plain functions. See Query, Res, ResMut, Commands and schedules with a small runnable example and the ordering rules.

3 min read
Summarize in AI
Flat illustration for the post “Bevy Systems Queries and Resources Explained Simply”, composed around embedded.

Bevy asks you to stop thinking about objects and start thinking about tables. Data lives in components, behavior lives in functions that query for that data, and the engine does the matching and scheduling. Once the model clicks, the API is small. This post covers the four ideas that make up most Bevy code. It compiles on Rust 1.90 with edition 2024 and Bevy 0.16; Bevy releases often change names, so check the migration guide for your version.

The pieces

ConceptIn codeMeaning
EntityEntityAn id with a set of components
Component#[derive(Component)] struct Health(i32);A piece of data attached to an entity
Systemfn heal(mut q: Query<&mut Health>) {}A function run by the scheduler
Resource#[derive(Resource)] struct Score(u32);Global singleton data
ScheduleStartup, Update, FixedUpdateWhen systems run

A complete example

rust
use bevy::prelude::*;

#[derive(Component)]
struct Position(Vec2);

#[derive(Component)]
struct Velocity(Vec2);

#[derive(Component)]
struct Enemy;

#[derive(Resource, Default)]
struct Score(u32);

fn setup(mut commands: Commands) {
    commands.spawn((Position(Vec2::ZERO), Velocity(Vec2::new(1.0, 0.5))));
    commands.spawn((Position(Vec2::new(5.0, 5.0)), Velocity(Vec2::new(-1.0, 0.0)), Enemy));
}

fn movement(time: Res<Time>, mut query: Query<(&mut Position, &Velocity)>) {
    for (mut pos, vel) in &mut query {
        pos.0 += vel.0 * time.delta_secs();
    }
}

fn count_enemies(enemies: Query<&Position, With<Enemy>>, mut score: ResMut<Score>) {
    score.0 = enemies.iter().count() as u32;
}

fn report(score: Res<Score>) {
    if score.is_changed() {
        println!("enemies: {}", score.0);
    }
}

fn main() {
    App::new()
        .add_plugins(MinimalPlugins)
        .init_resource::<Score>()
        .add_systems(Startup, setup)
        .add_systems(Update, (movement, count_enemies, report).chain())
        .run();
}

The program runs headless with MinimalPlugins (no window or renderer), spawns two entities at startup and runs three systems each frame, in order because of .chain(). It loops forever; stop it with Ctrl-C. Nothing in the code names the entities: movement receives every entity that has Position and Velocity, wherever it came from.

Queries

  • Query<&Position>: read-only access to a component.
  • Query<&mut Position>: write access. Iterate with for mut p in &mut q.
  • Query<(Entity, &Health)>: include the entity id (to despawn or reference it later).
  • With<T> and Without<T>: filters that test presence without reading data.
  • Option<&T>: match entities with or without an optional component.
  • Changed<T> and Added<T>: only entities whose component changed or was just added.

The borrow checker's rules are enforced at the system level. A system with Query<&mut Position> and another Query<&Position> in the same function conflicts, and Bevy panics on startup with a clear message. Use Without filters to prove two queries are disjoint, or ParamSet to access them one at a time.

Resources

Res<T> gives read access and ResMut<T> write access to a global value. Common built-ins: Time, ButtonInput<KeyCode>, Assets<T>. Use Option<Res<T>> when the resource may not exist yet. A resource suits things that exist once; if you find yourself wanting several, it is a component on an entity.

Commands and deferred changes

rust
fn spawn_bullet(mut commands: Commands, keys: Res<ButtonInput<KeyCode>>) {
    if keys.just_pressed(KeyCode::Space) {
        commands.spawn((Position(Vec2::ZERO), Velocity(Vec2::X * 10.0)));
    }
}

fn cleanup(mut commands: Commands, q: Query<(Entity, &Position)>) {
    for (e, pos) in &q {
        if pos.0.x > 100.0 {
            commands.entity(e).despawn();
        }
    }
}

Changes to the world's structure (spawn, despawn, insert or remove a component) go through Commands and take effect at a later sync point, which lets systems run in parallel without invalidating each other's queries. The consequence: do not expect the new entity in the same system.

Ordering and parallelism

Bevy builds a dependency graph from each system's parameters. Two systems that only read the same data run in parallel; one that writes a component conflicts with any other that touches it and is serialized. Add explicit ordering when one system's output feeds another (.after(movement)), put related systems in sets, and use FixedUpdate for physics-style logic that should run at a fixed rate regardless of frame time. For where this design pays off in performance terms, see data oriented design.

Taggedbevy
Share

Keep reading

Get new posts by email

We send one email when a new post is published. No spam.

Unsubscribe in one click. No spam, ever.