references/queries-and-scheduling.md
# Bevy queries & scheduling detail (0.19)
Depth behind the ECS skill: query filters and access, schedules and ordering,
states, change detection, and the `Commands` lifecycle. Verify any borderline API
against the docs for your pinned Bevy version — minor releases move things.
## Query anatomy
A `Query<D, F>` has a **data** part `D` (what you read/write) and an optional
**filter** part `F` (which entities, without fetching their data).
```rust
// Data: read Name, write Transform. Filter: must have Player, must NOT have Frozen.
Query<(&Name, &mut Transform), (With<Player>, Without<Frozen>)>
```
Common filters:
- `With<T>` / `Without<T>` — entity has / lacks a component (no data fetched).
- `Added<T>` — `T` was added since this system last ran.
- `Changed<T>` — `T` was added or mutably accessed since last run (change detection).
- `Or<(...)>` — combine filters disjunctively.
Optional and entity access in the data part:
```rust
Query<(Entity, &Transform, Option<&Velocity>)> // Entity id; Velocity may be absent
```
### Single-entity access
For a query you expect to match exactly one entity (e.g. the player), `single()`
and `single_mut()` return a `Result` in 0.16+ (they replaced the older
`get_single`/panicking `single`):
```rust
fn read_player(q: Query<&Transform, With<Player>>) {
if let Ok(transform) = q.single() {
// exactly one Player matched
}
}
```
Iterating with `for x in &query` is always valid and the safest default.
## Avoiding conflicting access
Two systems can run in parallel only if their data accesses don't conflict. Two
queries **within one system** that both mutably touch the same component will panic
at startup. Fixes:
1. **Disjoint with filters** — `With<Player>` vs `Without<Player>` guarantees the
sets never overlap, so both can be `&mut`.
2. **`ParamSet`** — when sets *can* overlap, access them one at a time:
```rust
fn swap(mut set: ParamSet<(
Query<&mut Transform, With<A>>,
Query<&mut Transform, With<B>>,
)>) {
for mut t in &mut set.p0() { /* ... */ }
for mut t in &mut set.p1() { /* ... */ }
}
```
## Schedules and ordering
Built-in schedules you'll use most:
- `Startup` — once, before the first `Update`.
- `Update` — every frame.
- `FixedUpdate` — fixed timestep; use it for physics/gameplay that needs
determinism (read `time.delta_secs()` here too; it's the fixed step).
- `PreUpdate` / `PostUpdate` — around `Update` for setup/teardown ordering.
Ordering within a schedule:
```rust
// Explicit pairwise order.
app.add_systems(Update, (input, movement, collision).chain());
// Named constraints.
app.add_systems(Update, movement.before(collision));
app.add_systems(Update, camera_follow.after(movement));
```
### System sets
Group systems into a `SystemSet` to order whole phases and attach shared run
conditions:
```rust
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
enum GameSet { Input, Logic, Render }
app.configure_sets(Update, (GameSet::Input, GameSet::Logic, GameSet::Render).chain());
app.add_systems(Update, read_input.in_set(GameSet::Input));
app.add_systems(Update, (move_units, resolve).in_set(GameSet::Logic));
```
### Run conditions
```rust
app.add_systems(Update, pause_menu.run_if(in_state(AppState::Paused)));
app.add_systems(Update, autosave.run_if(on_timer(Duration::from_secs(30))));
```
## States
`States` model app-wide modes (menu, playing, paused). Use `OnEnter`/`OnExit`
schedules for transition logic and `in_state` to gate `Update` systems.
```rust
#[derive(States, Default, Debug, Clone, PartialEq, Eq, Hash)]
enum AppState { #[default] Menu, Playing }
app.init_state::<AppState>()
.add_systems(OnEnter(AppState::Playing), spawn_level)
.add_systems(OnExit(AppState::Playing), cleanup_level)
.add_systems(Update, gameplay.run_if(in_state(AppState::Playing)));
// Transition from a system:
fn start(mut next: ResMut<NextState<AppState>>) { next.set(AppState::Playing); }
```
## Change detection
`Changed<T>` / `Added<T>` filters and the `Ref<T>`/`Mut<T>` wrappers let systems
react only to modified data — cheaper than recomputing every frame. Note: writing
through a `&mut T` marks it changed even if the value is identical; guard with a
value check if that matters.
## Commands lifecycle
`Commands` queue structural changes (spawn, despawn, insert/remove components,
insert resources). They are **deferred** and applied at the next sync point
(end of the schedule stage), so:
- An entity spawned this frame is not in queries until a later system/stage.
- `commands.entity(e).despawn()` removes the entity; in 0.16+ this also removes its
children (the old explicit `despawn_recursive` was folded in).
```rust
fn spawn_bullet(mut commands: Commands) {
let id = commands.spawn((Bullet, Transform::default())).id();
commands.entity(id).insert(Velocity(Vec2::Y * 500.0));
}
```
For immediate, exclusive access to the whole `World` (one-off setup, complex
queries), use an exclusive system `fn(&mut World)` — it can't run in parallel, so
use sparingly.
## Messages / observers — version caution
Bevy's buffered event API evolved into the message API in recent releases, while
observers remain event-oriented. If systems need buffered communication, look up
the exact message/observer API for the pinned release rather than copying an
example from a different version.
SKILL.md
---
name: bevy-ecs
description: >
Structure a Bevy app around its Entity Component System: build the App with
plugins, define Component/Resource types, write systems with Query/Res/Commands,
filter and order systems, and use the Time resource for frame-rate-independent
motion. Use when building or debugging a Bevy game in Rust — when the user
mentions Bevy, ECS, App::new, add_systems, Query, Commands, components/systems,
or a Cargo.toml depending on bevy.
---
# Bevy ECS
Structure a Bevy game in Rust around the Entity Component System: the `App` and
plugins, components and resources, systems with queries, scheduling, and
frame-rate-independent updates. New examples target **Bevy 0.19**. If the project
already pins another release, keep that release and use its matching migration guide.
## When to use
- Use when wiring a Bevy `App`, defining `Component`/`Resource` types, writing
systems that query entities, ordering/filtering systems, or fixing
borrow-conflict panics and frame-dependent movement.
- Use when `Cargo.toml` depends on `bevy` and code calls `App::new()`,
`add_systems`, `Query`, or `Commands`.
**When *not* to use:** this is the ECS core. Deep rendering, custom shaders/
pipelines, UI layout, and audio are separate concerns. For engine-agnostic AI or
procedural algorithms, pair with `game-ai` / `procedural-gen`.
## Core workflow
1. **Detect and pin the version.** Read `Cargo.toml` and `Cargo.lock` first. For a
new project use `bevy = "0.19"`; never silently migrate an existing project
across a Bevy minor release. Treat the matching docs and migration guides as truth.
2. **Build the `App`.** `App::new().add_plugins(DefaultPlugins)` gives windowing,
input, rendering, time, etc. Register systems into schedules: `Startup` (once)
and `Update` (every frame).
3. **Model data as components, globals as resources.** `#[derive(Component)]` for
per-entity data; `#[derive(Resource)]` for one-of-a-kind data (score, settings,
the `Time` clock). In 0.19 `Resource` extends `Component`, so do not derive both.
4. **Write systems as plain functions.** Parameters declare data access: `Query<...>`
for entities, `Res<T>`/`ResMut<T>` for resources, `Commands` for deferred
spawn/despawn. Systems run in parallel when their accesses don't conflict.
5. **Drive motion by `time.delta_secs()`** so speed is frame-rate independent.
6. **Order only what must be ordered** with `.chain()` or explicit constraints;
gate systems with `run_if`. Group related setup into `Plugin`s. Build with
`cargo run` and read the panics — Bevy reports conflicting queries at startup.
## Patterns
### 1. Cargo.toml + minimal App
```toml
# Cargo.toml — pin the version; the API differs across minor releases.
[dependencies]
bevy = "0.19"
```
```rust
// main.rs
use bevy::prelude::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins) // window, input, render, time, ...
.add_systems(Startup, setup) // runs once at startup
.add_systems(Update, move_players) // runs every frame
.run();
}
```
### 2. Components, resources, and spawning
```rust
#[derive(Component)]
struct Player;
#[derive(Component)]
struct Velocity(Vec2);
#[derive(Resource)]
struct Score(u32);
fn setup(mut commands: Commands) {
commands.insert_resource(Score(0));
// Camera2d is a component with required components (bundles removed in 0.16);
// spawning it pulls in Transform, Camera, etc. automatically.
commands.spawn(Camera2d);
// Spawn an entity as a tuple of components.
commands.spawn((
Player,
Velocity(Vec2::new(150.0, 0.0)),
Transform::from_xyz(0.0, 0.0, 0.0),
));
}
```
### 3. A system with a query + the Time resource
```rust
// Iterate every entity that has BOTH Velocity and Transform; mutate Transform.
fn move_players(time: Res<Time>, mut query: Query<(&Velocity, &mut Transform)>) {
for (velocity, mut transform) in &mut query {
// delta_secs() is f32 seconds (renamed from delta_seconds() in 0.16).
transform.translation += velocity.0.extend(0.0) * time.delta_secs();
}
}
```
### 4. Query filters (With / Without / Changed)
```rust
// Only entities tagged Player (the Player component itself isn't read).
fn aim_player(mut q: Query<&mut Transform, With<Player>>) { /* ... */ }
// Disjoint two mutable Transform queries so they don't conflict at runtime.
fn separate(
mut players: Query<&mut Transform, With<Player>>,
mut enemies: Query<&mut Transform, Without<Player>>,
) { /* ... */ }
// React only when Health changed since last run (change detection).
fn on_health_change(q: Query<&Health, Changed<Health>>) {
for health in &q { /* update the HUD, etc. */ }
}
```
### 5. Resources: read and write
```rust
fn add_points(mut score: ResMut<Score>) {
score.0 += 10; // ResMut = write access
}
fn show_score(score: Res<Score>) {
info!("score: {}", score.0); // Res = read access
}
```
### 6. Ordering, run conditions, and plugins
```rust
fn main() {
App::new()
.add_plugins((DefaultPlugins, GameplayPlugin))
// .chain() forces order: damage resolves before death is checked.
.add_systems(Update, (apply_damage, check_deaths).chain())
// run_if gates a system on a condition each frame.
.add_systems(Update, spawn_wave.run_if(wave_timer_finished))
.run();
}
struct GameplayPlugin;
impl Plugin for GameplayPlugin {
fn build(&self, app: &mut App) {
app.insert_resource(Score(0))
.add_systems(Startup, setup)
.add_systems(Update, (move_players, add_points));
}
}
```
## Pitfalls
- **`delta_seconds()` not found** → it was renamed to `time.delta_secs()` (and
`elapsed_secs()`) in 0.16. Using the old name fails to compile.
- **Movement speed scales with frame rate** → multiply per-frame changes by
`time.delta_secs()`. Never assume a fixed frame time.
- **Panic: "conflicting accesses" / "&mut T and &mut T"** → two `Query`s in one
system both write the same component, or one reads while another writes overlapping
entities. Make them disjoint with `With`/`Without`, or use `ParamSet`.
- **`Camera2dBundle`/`SpriteBundle` not found** → bundles were deprecated in 0.15 and
removed in 0.16.
Spawn the components directly (`Camera2d`, `Sprite`, `Transform`); required
components fill in the rest.
- **"trait `Component` is not implemented"** → you forgot `#[derive(Component)]`
(or `#[derive(Resource)]` for a resource).
- **Spawned entity not visible to a later query in the same frame** → `Commands` are
deferred and applied at the next sync point. Read the entity in a subsequent system,
not the one that spawned it.
- **System order assumed but not enforced** → systems run in parallel by default.
If `B` must follow `A`, add `(A, B).chain()` or an explicit ordering constraint.
- **Deriving both `Resource` and `Component` in 0.19** → `Resource` now extends
`Component`; derive `Resource` alone to avoid conflicting implementations.
- **Copy-pasting older Bevy snippets** → APIs shift between minor versions. The
buffered event system became the message system in recent releases. Verify against
the docs and migration guide for *your* pinned version; don't mix versions.
## References
- For schedules and `SystemSet` ordering, `States`/`OnEnter`/`OnExit`, change
detection, `Commands` lifecycle and sync points, `ParamSet` for conflicting
queries, and a version note on the events/observers API, read
`references/queries-and-scheduling.md`.
## Related skills
- `game-ai` — FSMs/behavior trees/steering as portable concepts to implement in ECS.
- `procedural-gen` — noise/RNG/generation algorithms to drive from systems.
- `pygame-core` / `love2d-core` — lighter-weight engines for smaller projects.