gamedev-skills/awesome-gamedev-agent-skillsPrüfung bestanden
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.
Installationen · 239Quelle ansehen
Installation
npx skills add https://github.com/gamedev-skills/awesome-gamedev-agent-skills --skill bevy-ecs
Skill-Dateien
SKILL.md
Zuletzt synchronisiert · 25.10.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.