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
- 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.
- Build the
App. App::new().add_plugins(DefaultPlugins) gives windowing,
input, rendering, time, etc. Register systems into schedules: Startup (once)
and Update (every frame).
- 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.
- 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.
- Drive motion by
time.delta_secs() so speed is frame-rate independent.
- Order only what must be ordered with
.chain() or explicit constraints;
gate systems with run_if. Group related setup into Plugins. Build with
cargo run and read the panics — Bevy reports conflicting queries at startup.
Patterns
1. Cargo.toml + minimal App
# Cargo.toml — pin the version; the API differs across minor releases.
[dependencies]
bevy = "0.19"
// 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
#[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
// 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)
// 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
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
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
Querys 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.
1---2name: bevy-ecs3description: 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.4---5
6# Bevy ECS
7
8Structure a Bevy game in Rust around the Entity Component System: the `App` and
9plugins, components and resources, systems with queries, scheduling, and
10frame-rate-independent updates. New examples target **Bevy 0.19**. If the project
11already pins another release, keep that release and use its matching migration guide.
12
13## When to use
14
15- Use when wiring a Bevy `App`, defining `Component`/`Resource` types, writing
16 systems that query entities, ordering/filtering systems, or fixing
17 borrow-conflict panics and frame-dependent movement.
18- Use when `Cargo.toml` depends on `bevy` and code calls `App::new()`,
19 `add_systems`, `Query`, or `Commands`.
20
21**When *not* to use:** this is the ECS core. Deep rendering, custom shaders/
22pipelines, UI layout, and audio are separate concerns. For engine-agnostic AI or
23procedural algorithms, pair with `game-ai` / `procedural-gen`.
24
25## Core workflow
26
271. **Detect and pin the version.** Read `Cargo.toml` and `Cargo.lock` first. For a
28 new project use `bevy = "0.19"`; never silently migrate an existing project
29 across a Bevy minor release. Treat the matching docs and migration guides as truth.
302. **Build the `App`.** `App::new().add_plugins(DefaultPlugins)` gives windowing,
31 input, rendering, time, etc. Register systems into schedules: `Startup` (once)
32 and `Update` (every frame).
333. **Model data as components, globals as resources.** `#[derive(Component)]` for
34 per-entity data; `#[derive(Resource)]` for one-of-a-kind data (score, settings,
35 the `Time` clock). In 0.19 `Resource` extends `Component`, so do not derive both.
364. **Write systems as plain functions.** Parameters declare data access: `Query<...>`
37 for entities, `Res<T>`/`ResMut<T>` for resources, `Commands` for deferred
38 spawn/despawn. Systems run in parallel when their accesses don't conflict.
395. **Drive motion by `time.delta_secs()`** so speed is frame-rate independent.
406. **Order only what must be ordered** with `.chain()` or explicit constraints;
41 gate systems with `run_if`. Group related setup into `Plugin`s. Build with
42 `cargo run` and read the panics — Bevy reports conflicting queries at startup.
43
44## Patterns
45
46### 1. Cargo.toml + minimal App
47
48```toml
49# Cargo.toml — pin the version; the API differs across minor releases.
50[dependencies]
51bevy = "0.19"
52```
53
54```rust
55// main.rs
56use bevy::prelude::*;
57
58fn main() {
59 App::new()
60 .add_plugins(DefaultPlugins) // window, input, render, time, ...
61 .add_systems(Startup, setup) // runs once at startup
62 .add_systems(Update, move_players) // runs every frame
63 .run();
64}
65```
66
67### 2. Components, resources, and spawning
68
69```rust
70#[derive(Component)]
71struct Player;
72
73#[derive(Component)]
74struct Velocity(Vec2);
75
76#[derive(Resource)]
77struct Score(u32);
78
79fn setup(mut commands: Commands) {
80 commands.insert_resource(Score(0));
81
82 // Camera2d is a component with required components (bundles removed in 0.16);
83 // spawning it pulls in Transform, Camera, etc. automatically.
84 commands.spawn(Camera2d);
85
86 // Spawn an entity as a tuple of components.
87 commands.spawn((
88 Player,
89 Velocity(Vec2::new(150.0, 0.0)),
90 Transform::from_xyz(0.0, 0.0, 0.0),
91 ));
92}
93```
94
95### 3. A system with a query + the Time resource
96
97```rust
98// Iterate every entity that has BOTH Velocity and Transform; mutate Transform.
99fn move_players(time: Res<Time>, mut query: Query<(&Velocity, &mut Transform)>) {
100 for (velocity, mut transform) in &mut query {
101 // delta_secs() is f32 seconds (renamed from delta_seconds() in 0.16).
102 transform.translation += velocity.0.extend(0.0) * time.delta_secs();
103 }
104}
105```
106
107### 4. Query filters (With / Without / Changed)
108
109```rust
110// Only entities tagged Player (the Player component itself isn't read).
111fn aim_player(mut q: Query<&mut Transform, With<Player>>) { /* ... */ }
112
113// Disjoint two mutable Transform queries so they don't conflict at runtime.
114fn separate(
115 mut players: Query<&mut Transform, With<Player>>,
116 mut enemies: Query<&mut Transform, Without<Player>>,
117) { /* ... */ }
118
119// React only when Health changed since last run (change detection).
120fn on_health_change(q: Query<&Health, Changed<Health>>) {
121 for health in &q { /* update the HUD, etc. */ }
122}
123```
124
125### 5. Resources: read and write
126
127```rust
128fn add_points(mut score: ResMut<Score>) {
129 score.0 += 10; // ResMut = write access
130}
131
132fn show_score(score: Res<Score>) {
133 info!("score: {}", score.0); // Res = read access
134}
135```
136
137### 6. Ordering, run conditions, and plugins
138
139```rust
140fn main() {
141 App::new()
142 .add_plugins((DefaultPlugins, GameplayPlugin))
143 // .chain() forces order: damage resolves before death is checked.
144 .add_systems(Update, (apply_damage, check_deaths).chain())
145 // run_if gates a system on a condition each frame.
146 .add_systems(Update, spawn_wave.run_if(wave_timer_finished))
147 .run();
148}
149
150struct GameplayPlugin;
151impl Plugin for GameplayPlugin {
152 fn build(&self, app: &mut App) {
153 app.insert_resource(Score(0))
154 .add_systems(Startup, setup)
155 .add_systems(Update, (move_players, add_points));
156 }
157}
158```
159
160## Pitfalls
161
162- **`delta_seconds()` not found** → it was renamed to `time.delta_secs()` (and
163 `elapsed_secs()`) in 0.16. Using the old name fails to compile.
164- **Movement speed scales with frame rate** → multiply per-frame changes by
165 `time.delta_secs()`. Never assume a fixed frame time.
166- **Panic: "conflicting accesses" / "&mut T and &mut T"** → two `Query`s in one
167 system both write the same component, or one reads while another writes overlapping
168 entities. Make them disjoint with `With`/`Without`, or use `ParamSet`.
169- **`Camera2dBundle`/`SpriteBundle` not found** → bundles were deprecated in 0.15 and
170 removed in 0.16.
171 Spawn the components directly (`Camera2d`, `Sprite`, `Transform`); required
172 components fill in the rest.
173- **"trait `Component` is not implemented"** → you forgot `#[derive(Component)]`
174 (or `#[derive(Resource)]` for a resource).
175- **Spawned entity not visible to a later query in the same frame** → `Commands` are
176 deferred and applied at the next sync point. Read the entity in a subsequent system,
177 not the one that spawned it.
178- **System order assumed but not enforced** → systems run in parallel by default.
179 If `B` must follow `A`, add `(A, B).chain()` or an explicit ordering constraint.
180- **Deriving both `Resource` and `Component` in 0.19** → `Resource` now extends
181 `Component`; derive `Resource` alone to avoid conflicting implementations.
182- **Copy-pasting older Bevy snippets** → APIs shift between minor versions. The
183 buffered event system became the message system in recent releases. Verify against
184 the docs and migration guide for *your* pinned version; don't mix versions.
185
186## References
187
188- For schedules and `SystemSet` ordering, `States`/`OnEnter`/`OnExit`, change
189 detection, `Commands` lifecycle and sync points, `ParamSet` for conflicting
190 queries, and a version note on the events/observers API, read
191 `references/queries-and-scheduling.md`.
192
193## Related skills
194
195- `game-ai` — FSMs/behavior trees/steering as portable concepts to implement in ECS.
196- `procedural-gen` — noise/RNG/generation algorithms to drive from systems.
197- `pygame-core` / `love2d-core` — lighter-weight engines for smaller projects.