Retour aux skills
gamedev-skills/awesome-gamedev-agent-skillsContrôle réussi

SKILL DETAIL

bevy-ecs

gamedev-skills/awesome-gamedev-agent-skills/bevy-ecs

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.

Installations · 239Voir la source

Installation

npx skills add https://github.com/gamedev-skills/awesome-gamedev-agent-skills --skill bevy-ecs

Fichiers du skill

SKILL.md

Dernière synchronisation · 25 oct. 2026

references/queries-and-scheduling.md›
# Bevy queries & scheduling detail (0.20)

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.

### Many-entity access (0.20)

`iter_many`, `iter_many_mut`, and their `unique`/`par_` variants take a list of
entities and, since 0.20, yield `Result<Item, QueryEntityError>`: an entity that was
despawned or doesn't match the query is an `Err`, not silently skipped.

```rust
#[derive(Component)]
struct Health(f32);

#[derive(Resource)]
struct Targets(Vec<Entity>);

fn check_targets(targets: Res<Targets>, q: Query<&Health>) {
    for result in q.iter_many(targets.0.iter()) {
        match result {
            Ok(health) => { /* ... */ }
            Err(err) => warn!("stale target: {err}"),
        }
    }
}

// .matched() keeps the pre-0.20 behaviour of skipping missing entities.
fn heal_targets(targets: Res<Targets>, mut q: Query<&mut Health>) {
    let mut iter = q.iter_many_mut(targets.0.iter()).matched();
    while let Some(mut health) = iter.fetch_next() {
        health.0 += 1.0;
    }
}
```

## 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() { /* ... */ }
}
```

3. **Resource entities** — since 0.19 each resource is stored on an entity tagged
   `IsResource`, so broad queries (`Query<Entity>`, `Query<EntityRef>`,
   `Query<EntityMut>`) match those entities too. `Query<EntityMut>` next to any
   `Res<T>` panics at startup (`error[B0002]` when the `Res` comes after the query,
   `error[B0001]` when it comes first), and despawning every entity a `Query<Entity>`
   returns panics. Query a marker component instead, or exclude resources:

```rust
use bevy::ecs::resource::IsResource; // not in the prelude

fn inspect(entities: Query<EntityMut, Without<IsResource>>, score: Res<Score>) { /* ... */ }
```

## 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));
```

### Weak ordering (0.20)

`.chain_weak()`, `.before_weak()`, and `.after_weak()` keep an ordering only between
systems whose data access actually conflicts; systems that touch disjoint data may run
in either order or in parallel. An earlier system that queues `Commands`, and any
exclusive system, always keeps its place.

```rust
app.add_systems(Update, (tick_ai, tick_audio, tick_particles).chain_weak());
```

The scheduler can't see dependencies carried by interior mutability, channels, atomics,
or global/`NonSend` state, so keep `.chain()` for those. 0.20 also orders several
built-in sets weakly (`RenderSystems`, the `Core2d`/`Core3d` passes, and the UI
`UiSystems` in `PostUpdate`): a custom system in one of those sets that relied on an
earlier set finishing first, without a data dependency, needs an explicit `.after(...)`.

### 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
use bevy::time::common_conditions::on_timer; // not in the prelude
use std::time::Duration;

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

### Testing for hidden order dependencies (0.20)

Systems with no ordering constraint run in an order the scheduler picks
deterministically but arbitrarily, so code can work only by accident until an unrelated
change reshuffles it. With Bevy's `debug` feature, shuffle that order in tests and try
several seeds; explicit `.before`/`.after`/`.chain` constraints are kept.

```rust
// Cargo.toml: bevy = { version = "0.20", features = ["debug"] }
use bevy::ecs::schedule::ScheduleBuildSettings;

app.edit_schedule(Update, |schedule| {
    schedule.set_build_settings(ScheduleBuildSettings {
        shuffle_seed: Some(seed), // log the seed so a failing order can be replayed
        ..default()
    });
});
```

## 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, Paused }

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

// set() always runs OnExit/OnEnter, even when the target is the current state.
// set_if_different() skips same-state transitions (0.20 name; was set_if_neq).
fn resume(mut next: ResMut<NextState<AppState>>) { next.set_if_different(AppState::Playing); }

// Despawned automatically when Playing exits; no cleanup system needed.
fn spawn_enemy(mut commands: Commands) {
    commands.spawn((Enemy, DespawnOnExit(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.

A component can opt in to a per-table summary tick (0.20). It only pays off for code
that walks the component with `Query::contiguous_iter()` over `Ref<T>` and checks the
chunk's `summary_tick_is_changed()` to skip a whole table at once; an ordinary
`Changed<T>` filter still checks every entity's tick. Every write gets slightly more
expensive, so opt in only for components that are scanned often and written rarely:

```rust
#[derive(Component)]
#[component(summary_tick)]
struct Inventory(Vec<u32>);
```

## 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).
- `commands.despawn_all::<With<Enemy>>()` (0.20) despawns every matching entity in one
  batched command, faster than despawning them one at a time.

```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. Since 0.20 `&mut World` is an ordinary system parameter and no longer has
to come first: `fn snapshot(mut runs: Local<u32>, world: &mut World)` is valid.
Parameters that need their own world access still conflict with it.

## Messages and observers (0.20)

Two ways for systems to communicate:

- **Messages** are buffered and pulled: writers queue them, and each reader drains the
  new ones on its next run. Use them for per-frame streams (damage numbers, input
  actions).
- **Events + observers** are pushed: triggering an event runs every matching observer
  right away (`commands.trigger` when the commands are applied, `world.trigger`
  immediately). Use them for one-off reactions.

```rust
#[derive(Message)]
struct ScoreChanged(u32);

app.add_message::<ScoreChanged>();

fn send(mut writer: MessageWriter<ScoreChanged>) { writer.write(ScoreChanged(10)); }
fn show(mut reader: MessageReader<ScoreChanged>) {
    for msg in reader.read() { info!("+{}", msg.0); }
}
```

```rust
#[derive(Event)]
struct LevelCleared { level: u32 }

app.add_observer(|cleared: On<LevelCleared>| info!("level {} cleared", cleared.level));
fn finish(mut commands: Commands) { commands.trigger(LevelCleared { level: 3 }); }

// Entity-targeted event, observed on one entity.
#[derive(EntityEvent)]
struct Hit { entity: Entity, damage: f32 }

fn spawn_player(mut commands: Commands) {
    commands.spawn((Player, Health(10.0))).observe(|hit: On<Hit>, mut q: Query<&mut Health>| {
        if let Ok(mut health) = q.get_mut(hit.entity) { health.0 -= hit.damage; }
    });
}
fn hit(mut commands: Commands, player: Single<Entity, With<Player>>) {
    commands.trigger(Hit { entity: *player, damage: 4.0 });
}

// Component lifecycle: in 0.20 the component is the event's type parameter.
app.add_observer(|add: On<Add<Player>>| info!("player {} spawned", add.entity));
// Also Insert<T>, Discard<T>, Remove<T>, Despawn<T>.
// 0.17-0.19 wrote On<Add, Player>; 0.16 wrote Trigger<OnAdd, Player>.
```

`EventReader`, `EventWriter`, and `add_event` no longer exist. These APIs have moved in
almost every recent release, so look up the exact message/observer API for the pinned
version rather than copying an example from another one.
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.20**. 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.20"`; 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). Since 0.19 `Resource` extends `Component` and each resource is
   stored on its own entity, 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.20"
```

```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 `error[B0001]`: "... accesses component(s) Transform in a way that conflicts
  with a previous system parameter"** → 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.
  To find order dependencies that only work by accident, shuffle the schedule in tests
  with `ScheduleBuildSettings::shuffle_seed` (0.20, `debug` feature; see the reference).
- **"conflicting implementations of trait `Component`" after deriving both `Resource`
  and `Component`** → since 0.19 `Resource` extends `Component`; derive `Resource` alone.
- **"Despawn everything" panics, or `Query<EntityMut>` next to a `Res<T>` panics with
  `error[B0002]` (or `B0001`, depending on parameter order)** → since 0.19 every
  resource lives on an entity tagged `IsResource`,
  so `Query<Entity>`/`EntityRef`/`EntityMut` match those entities too. Query your own
  marker component, or add `Without<IsResource>` (`use bevy::ecs::resource::IsResource`).
- **`On<Add, Player>` fails with "struct takes 1 generic argument but 2 generic
  arguments were supplied"** → 0.20 moved the component into the lifecycle event:
  write `On<Add<Player>>` (likewise `Insert<T>`, `Discard<T>`, `Remove<T>`, `Despawn<T>`).
- **`Pointer<Click>` / `Pointer<Press>` not found in a picking observer** → 0.20
  flattened pointer events: observe `On<PointerClick>` / `On<PointerPress>`, and read
  `click.pointer.position` (a `Vec2`) instead of the old `pointer_location`.
- **`OnExit`/`OnEnter` run again when "switching" to the current state** →
  `NextState::set` always runs the transition schedules. Use `set_if_different` to skip
  same-state transitions (renamed from `set_if_neq` in 0.20; calling `set_if_neq` on a
  `ResMut<NextState<_>>` picks up the unrelated change-detection method and fails with
  "can't compare `NextState`").
- **"`text` does not live long enough" from `Name::new(text.as_str())`** → since 0.20
  `Name` takes a `&'static str` literal or an owned `String`, not a borrowed `&str`;
  pass `Name::new(text.clone())` or `Name::new(format!("Enemy {id}"))`.
- **Overlapping sprites swap draw order after upgrading to 0.20** → sprites now render
  through the `Mesh2d` backend, and draw order at equal `z` was never guaranteed. Give
  sprites that can overlap distinct `Transform` z values.
- **Copy-pasting older Bevy snippets** → APIs shift between minor versions. The
  buffered event system became the message system: `EventReader`/`EventWriter`/
  `add_event` no longer exist, so use `#[derive(Message)]`, `MessageReader`/
  `MessageWriter`, and `app.add_message::<T>()`. Verify against the docs and migration
  guide for *your* pinned version; don't mix versions.

## References

- For schedules and `SystemSet` ordering (including 0.20 weak ordering and schedule
  shuffling), `States`/`OnEnter`/`OnExit` and state-scoped despawning, change
  detection, `Commands` lifecycle and sync points, `ParamSet` and resource-entity
  conflicts, `iter_many` results, and messages/observers in 0.20 syntax, 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.