Skip to content
Development
Skill

/bevy-ecs

Builds and debugs Bevy 0.20 games in Rust: components, Query and Commands systems, schedules, States, observers and headless tests. Use when Cargo.toml depends on bevy, on a B0001 query-conflict panic, or pre-0.20 code (EventReader, SpriteBundle). Not for engine-neutral AI,

From plugin
awesome-gamedev-agent-skills
8.6k79 skills

Claude Code

$ npx -y skills add gamedev-skills/awesome-gamedev-agent-skills --skill bevy-ecs --agent claude-code

SKILL.md

bevy-ecs.SKILL.md
name: bevy-ecs
description: >
  Builds and debugs Bevy 0.20 games in Rust: components, Query and Commands systems, schedules,
  States, observers and headless tests. Use when Cargo.toml depends on bevy, on a B0001
  query-conflict panic, or pre-0.20 code (EventReader, SpriteBundle). Not for engine-neutral AI,
  generation, physics or shaders (`game-ai`, `procedural-gen`, `physics-tuning`,
  `shader-programming`).

Bevy ECS

Structure and debug a Bevy game around its ECS. New code targets **Bevy 0.20** (`bevy = "0.20"`, which resolves to the latest 0.20.x patch). Bevy breaks APIs in every minor release, so the version the project pins decides which syntax is right.

When to use

  • Wiring an `App`, defining components and resources, writing systems, ordering them,

gating them on state or timers, or passing data with messages and observers.

  • Fixing query-conflict panics, entities that seem missing, order bugs, or code copied from

an older Bevy release.

**When not to use:** rendering pipelines, custom shaders, UI layout and audio are outside this skill; AI and generation algorithms go to `game-ai` / `procedural-gen`.

Workflow

1. **Read the pinned version first.** `Cargo.toml` gives the requirement and `Cargo.lock` the resolved `bevy` version. Keep it; a minor-release migration is a separate task that follows that release's migration guide. Never mix snippets from two minors. 2. **Model the data.** Per-entity data is a `#[derive(Component)]`; one-of-a-kind data (score, settings) is a `#[derive(Resource)]`. Spawn entities as tuples of components; required components fill in the rest (`commands.spawn(Camera2d)` adds `Transform`, `Camera` and friends). 3. **Write systems as plain functions** whose parameters declare access: `Query<D, F>`, `Res`/`ResMut`, `Commands`, `MessageReader`/`MessageWriter`, `Single`. Systems with non-conflicting access run in parallel, in no guaranteed order. 4. **Order only what must be ordered** (`.chain()`, `.before`/`.after`, `SystemSet`s) and gate with `run_if`. Group features into `Plugin`s. Scale motion by `time.delta_secs()` or put it in `FixedUpdate`. 5. **Verify.** Run `cargo check`, then `cargo test` with a headless `App` test (pattern below) for each rule you changed, then `cargo run` and read the first panic: Bevy reports conflicting system parameters when a schedule first runs, with an error code and a link. Report which of these you ran; a clean `cargo check` proves only that it compiles. When the change is visual, ask the user to run the game and report what they see.

Patterns

Spawning and filtering

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

fn setup(mut commands: Commands) {
    commands.spawn(Camera2d);
    commands.spawn((Player, Velocity(Vec2::new(150.0, 0.0)), Transform::default()));
}

fn move_players(time: Res<Time>, mut q: Query<(&Velocity, &mut Transform)>) {
    for (v, mut t) in &mut q {
        t.translation += v.0.extend(0.0) * time.delta_secs();
    }
}

// Two &mut Transform queries in one system must be provably disjoint.
fn separate(
    mut players: Query<&mut Transform, With<Player>>,
    mut others: Query<&mut Transform, Without<Player>>,
) { /* ... */ }

Ordering, run conditions, plugins

struct GameplayPlugin;
impl Plugin for GameplayPlugin {
    fn build(&self, app: &mut App) {
        app.insert_resource(Score(0))
            .add_systems(Startup, setup)
            // damage must resolve before deaths are checked
            .add_systems(Update, (apply_damage, check_deaths).chain())
            .add_systems(Update, spawn_wave.run_if(in_state(AppState::Playing)));
    }
}

Headless test (the check to run)

`MinimalPlugins` gives time and a schedule runner with no window or GPU, so this runs under plain `cargo test`. Drive time by hand to make movement deterministic.

#[cfg(test)]
mod tests {
    use super::*;
    use bevy::time::TimeUpdateStrategy;
    use std::time::Duration;

    #[test]
    fn player_moves_at_its_velocity() {
        let mut app = App::new();
        app.add_plugins(MinimalPlugins)
            .insert_resource(TimeUpdateStrategy::ManualDuration(Duration::from_millis(100)))
            .add_systems(Update, move_players);
        let id = app.world_mut().spawn((Velocity(Vec2::X * 10.0), Transform::default())).id();
        for _ in 0..5 {
            app.update();
        }
        // The first update has a zero delta, so 5 updates advance 4 x 0.1 s.
        let x = app.world().get::<Transform>(id).unwrap().translation.x;
        assert!((x - 4.0).abs() < 1e-4, "x = {x}");
    }
}

For whole-world assertions use `app.world_mut().query_filtered::<&Health, With<Player>>()` and `.single(world)`. States need `bevy::state::app::StatesPlugin` next to `MinimalPlugins`.

Gotchas

  • **Panic `error[B0001]: Query<…> in system … accesses component(s) Transform in a way

that conflicts with a previous system parameter`** → two queries in one system can both reach the same component mutably. Make them disjoint with `With`/`Without`, or use `ParamSet`.

  • **`Query<EntityMut>` (or a broad `Query<Entity>` despawn loop) next to a `Res<T>`

panics** → since 0.19 every resource lives on an entity tagged `IsResource`, so broad queries match resources too. The error is `B0002` when the `Res` comes after the query and `B0001` when it comes first. Query your own marker, or add `Without<IsResource>` (`use bevy::ecs::resource::IsResource`).

  • **"conflicting implementations of trait `Component`"** after deriving `Resource` and

`Component` on one type → since 0.19 `Resource` implies `Component`; derive `Resource` alone.

  • **A spawned entity is missing from a query in the same system** → `Commands` are

deferred to the next sync point. Read it in a later system, or order the reader after the spawner.

  • **Something works only by luck of system order** → unordered syst
Read more
@gamedev-skills
Ships withawesome-gamedev-agent-skills

<img src="docs/assets/awesome-gamedev-agent-skills-banner.png" width="820" alt="awesome-gamedev-agent-skills — game-dev skills for AI coding agents.

Stars on GitHub

Momentum

stars in total
8.6k
Star history of gamedev-skills/awesome-gamedev-agent-skills on GitHub, a chart by star-history.com

More skills from other plugins.

brainstorming
Auto-invoked

brainstorming

You MUST use this before any creative work - creating features, building components, adding functionality, modifying behavior, or…

@obra@obraInstall Skill
ask-matt

ask-matt

Ask which skill or flow fits your situation. A router over the skills in this repo.

skills2w309k
@mattpocock@mattpocockInstall Skill
accessibility
Auto-invoked

accessibility

Design, implement, and audit accessible UI to WCAG 2.2 Level AA across Web, iOS, and Android — semantic ARIA roles and labels,…

ecc2w293k
@affaan-m@affaan-mInstall Skill
compiler-commit

compiler-commit

Use when you want to verify compiler changes and commit with the correct convention. Runs tests, lint, and format, then commits…

react5d252k
@facebook@facebookInstall Skill

Questions about bevy-ecs.

Which coding agents can use bevy-ecs?
bevy-ecs installs in Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, OpenCode and Windsurf with the skills CLI, using each agent's flag shown on this page. Flowy's auto-invocation, which fires a skill when your prompt matches, runs in Claude Code only.