name: bevy-ecs description: Use when the user asks about Bevy's Entity Component System, defining components, writing systems, queries, commands, resources, events, observers, system ordering, system sets, run conditions, or the ECS paradigm in Bevy. Also triggers when the user is confused about the ECS mental model or asks how to structure game logic. version: 1.0.0
Bevy ECS — Entity Component System Fundamentals
Mental Model
Entities are IDs, components are data structs, systems are functions that query components. Composition over inheritance.
- Entity — A unique ID (like a database row). No data or behavior by itself.
- Component — A plain Rust struct attached to an entity. This is your data.
- System — A function that queries entities by component combination. This is your behavior.
OOP: class Player extends Character { hp: i32, speed: f32 }
ECS: entity.insert((Player, Health(100), Speed(3.0), Transform::default()))
Systems run automatically each frame — the scheduler invokes them based on the data they request.
Components
A component is any Rust type with #[derive(Component)]:
#![allow(unused)] fn main() { #[derive(Component)] struct Health(i32); #[derive(Component)] struct Speed(f32); #[derive(Component, Default)] struct Player; #[derive(Component)] struct Enemy { aggro_range: f32, damage: i32, } }
Marker Components
Zero-sized types used purely for filtering queries:
#![allow(unused)] fn main() { #[derive(Component, Default)] struct Player; #[derive(Component)] struct Poisoned; #[derive(Component)] struct Grounded; }
Required Components (Bevy 0.15+)
Use #[require(...)] to auto-insert dependencies when a component is added (uses Default unless overridden at spawn):
#![allow(unused)] fn main() { #[derive(Component, Default)] #[require(Health, Speed)] struct Player; // commands.spawn(Player) automatically inserts Health::default() and Speed::default() // commands.spawn((Player, Speed(10.0))) overrides the Speed default }
Common Derive Macros
Component— required for all componentsDebug,Clone,PartialEq— commonly combined withComponentDefault— needed for#[require]andinit_resourceReflect+#[reflect(Component)]— enables runtime inspection (editor tooling, serialization)
Systems
Systems are plain Rust functions. Their parameters declare what data they need, and Bevy injects the data automatically:
#![allow(unused)] fn main() { fn move_entities(mut query: Query<(&mut Transform, &Velocity)>, time: Res<Time>) { for (mut transform, velocity) in &mut query { transform.translation += velocity.0 * time.delta_secs(); } } }
Register systems when building your app:
fn main() { App::new() .add_plugins(DefaultPlugins) .add_systems(Startup, setup) .add_systems(Update, (move_entities, check_health, handle_input)) .run(); }
System Parameter Types
Key types: Query, Res/ResMut, Commands, EventReader/EventWriter, Local, Single, ParamSet, Option<Res<T>>.
See the system-params-cheatsheet reference for the complete table with examples and notes.
Queries
Queries are how systems access entity data. The type signature determines what data is fetched and how it is filtered.
Basic Queries
#![allow(unused)] fn main() { // Read one component fn system(query: Query<&Transform>) { for transform in &query { info!("Position: {}", transform.translation); } } // Read multiple components fn system(query: Query<(&Transform, &Health, &Name)>) { for (transform, health, name) in &query { info!("{} at {} with {} hp", name, transform.translation, health.0); } } // Write to components fn system(mut query: Query<(&mut Transform, &Velocity)>) { for (mut transform, velocity) in &mut query { transform.translation += velocity.0; } } }
Query Filters
Filters go in the second type parameter of Query:
#![allow(unused)] fn main() { // Only entities that have the Player component fn system(query: Query<&Transform, With<Player>>) { } // Entities with Health but NOT the Invincible component fn system(query: Query<&mut Health, Without<Invincible>>) { } // Entities whose Transform changed since last system run fn system(query: Query<&Transform, Changed<Transform>>) { } // Entities that just had the Poisoned component added fn system(query: Query<Entity, Added<Poisoned>>) { } // Combine multiple filters with tuples fn system(query: Query<&mut Health, (With<Enemy>, Without<Shield>)>) { } }
Optional Components
Use Option<&T> to query entities that may or may not have a component:
#![allow(unused)] fn main() { fn system(query: Query<(&Transform, Option<&Velocity>)>) { for (transform, maybe_velocity) in &query { if let Some(velocity) = maybe_velocity { // Entity has velocity } else { // Entity is stationary } } } }
Single-Entity Queries
When you expect exactly one matching entity, use Single<> (Bevy 0.15+):
#![allow(unused)] fn main() { fn camera_follow( player: Single<&Transform, With<Player>>, mut camera: Single<&mut Transform, With<Camera>>, ) { camera.translation = player.translation; } }
If zero or more than one entity matches, the system panics. Use this for unique entities like "the player", "the main camera", or "the UI root".
Querying by Entity ID
#![allow(unused)] fn main() { fn system(query: Query<&Health>, specific_entity: Res<TrackedEntity>) { if let Ok(health) = query.get(specific_entity.0) { info!("Health: {}", health.0); } } }
Commands
Commands perform deferred world mutations. They do not take effect immediately — they are applied at the end of the current stage (between system sets). This avoids borrow conflicts.
Spawning Entities
#![allow(unused)] fn main() { fn spawn_enemies(mut commands: Commands) { // Spawn with a bundle of components let entity = commands.spawn(( Enemy { aggro_range: 10.0, damage: 5 }, Health(50), Transform::default(), Visibility::default(), )).id(); // Spawn and then add more components commands.spawn((Player, Health(100))) .insert(Speed(5.0)) .insert(Name::new("Hero")); } }
Inserting and Removing Components
#![allow(unused)] fn main() { fn poison_system( mut commands: Commands, query: Query<Entity, (With<Enemy>, Without<Poisoned>)>, ) { for entity in &query { commands.entity(entity).insert(Poisoned); } } fn cure_system( mut commands: Commands, query: Query<Entity, With<Poisoned>>, ) { for entity in &query { commands.entity(entity).remove::<Poisoned>(); } } }
Despawning Entities
#![allow(unused)] fn main() { fn cleanup_dead( mut commands: Commands, query: Query<Entity, With<Dead>>, ) { for entity in &query { // Despawn the entity and all its children commands.entity(entity).despawn(); } } }
Spawning Children (Hierarchies)
#![allow(unused)] fn main() { fn spawn_ui(mut commands: Commands) { commands.spawn(Node { width: Val::Percent(100.0), height: Val::Percent(100.0), ..default() }).with_children(|parent| { parent.spawn(( Text::new("Hello, Bevy!"), TextFont { font_size: 40.0, ..default() }, )); }); } }
Resources
Resources are global singletons — data that exists once, not per-entity. Use them for game-wide state.
#![allow(unused)] fn main() { #[derive(Resource)] struct Score(u32); #[derive(Resource, Default)] struct GameSettings { difficulty: Difficulty, volume: f32, } }
Inserting Resources
fn main() { App::new() .add_plugins(DefaultPlugins) // Insert with an explicit value .insert_resource(Score(0)) // Insert using Default::default() .init_resource::<GameSettings>() .run(); }
insert_resource(value)— provide a concrete instance.init_resource::<T>()— requiresT: Default(orT: FromWorld). Creates the resource from its default.
Accessing Resources in Systems
#![allow(unused)] fn main() { fn display_score(score: Res<Score>) { info!("Current score: {}", score.0); } fn increment_score(mut score: ResMut<Score>) { score.0 += 10; } }
Optional Resources
If a resource might not exist:
#![allow(unused)] fn main() { fn system(score: Option<Res<Score>>) { if let Some(score) = score { info!("Score: {}", score.0); } } }
Events and Observers
Events
Events are the primary way to communicate between systems without tight coupling.
#![allow(unused)] fn main() { #[derive(Event)] struct DamageEvent { entity: Entity, amount: i32, } #[derive(Event)] struct GameOverEvent; }
Register events and use EventWriter / EventReader:
fn main() { App::new() .add_plugins(DefaultPlugins) .add_event::<DamageEvent>() .add_event::<GameOverEvent>() .add_systems(Update, (deal_damage, apply_damage).chain()) .run(); } fn deal_damage( mut writer: EventWriter<DamageEvent>, query: Query<(Entity, &ContactInfo), With<Hazard>>, ) { for (entity, contact) in &query { writer.send(DamageEvent { entity: contact.other_entity, amount: 10, }); } } fn apply_damage( mut reader: EventReader<DamageEvent>, mut query: Query<&mut Health>, ) { for event in reader.read() { if let Ok(mut health) = query.get_mut(event.entity) { health.0 -= event.amount; } } }
Events last for two frames by default, then are dropped. Always read events every frame to avoid missing them.
Observers (Bevy 0.15+)
Observers are reactive — they run immediately when a specific event is triggered, without waiting for the schedule. They are ideal for structural changes.
#![allow(unused)] fn main() { #[derive(Event)] struct OnDeath; fn setup(mut commands: Commands) { commands.spawn(( Enemy { aggro_range: 10.0, damage: 5 }, Health(50), )).observe(on_death); } fn on_death(trigger: Trigger<OnDeath>, mut commands: Commands) { // `trigger.target()` is the entity that the event was triggered on let entity = trigger.target(); commands.entity(entity).despawn(); info!("Entity {:?} died", entity); } }
Trigger an observer:
#![allow(unused)] fn main() { fn check_health( mut commands: Commands, query: Query<(Entity, &Health), Changed<Health>>, ) { for (entity, health) in &query { if health.0 <= 0 { commands.trigger_targets(OnDeath, entity); } } } }
Global observers (not tied to a specific entity):
fn main() { App::new() .add_plugins(DefaultPlugins) .add_observer(on_any_death) .run(); } fn on_any_death(trigger: Trigger<OnDeath>, mut score: ResMut<Score>) { score.0 += 100; }
One-Shot Systems
Run a system once on demand using Commands:
#![allow(unused)] fn main() { fn trigger_explosion(mut commands: Commands) { commands.run_system(explosion_effect); } fn explosion_effect(mut query: Query<&mut Health, With<Enemy>>) { for mut health in &mut query { health.0 -= 50; } } }
Scheduling
Register systems into schedules: Startup (once), Update (every frame), FixedUpdate (fixed timestep, default 64 Hz). Systems in the same schedule run in parallel by default.
Enforce ordering with .before(), .after(), or .chain():
#![allow(unused)] fn main() { App::new() .add_systems(Startup, setup) .add_systems(Update, ( (read_input, move_player, check_collisions).chain(), game_logic.run_if(in_state(AppState::InGame)), )) }
Use system sets to group systems with shared ordering and run conditions. Use states (States, SubStates) to control which systems run based on app phase, with OnEnter/OnExit schedules for setup and cleanup.
See the scheduling-guide reference for all built-in schedules, ordering primitives, run conditions, and state patterns.
Writing Custom Plugins
Plugins are the standard way to organize related systems, resources, and events into reusable modules:
#![allow(unused)] fn main() { pub struct CombatPlugin; impl Plugin for CombatPlugin { fn build(&self, app: &mut App) { app .add_event::<DamageEvent>() .add_event::<DeathEvent>() .init_resource::<CombatStats>() .add_systems(Update, ( deal_damage, apply_damage, check_death, ).chain().in_set(GameSet::Combat)); } } }
Use plugins in your app:
fn main() { App::new() .add_plugins(DefaultPlugins) .add_plugins(( CombatPlugin, InventoryPlugin, AudioPlugin, )) .run(); }
Plugin Groups
Group multiple plugins together:
pub struct GamePlugins; impl PluginGroup for GamePlugins { fn build(self) -> PluginGroupBuilder { PluginGroupBuilder::start::<Self>() .add(CombatPlugin) .add(InventoryPlugin) .add(MovementPlugin) .add(UIPlugin) } } // Use like DefaultPlugins: fn main() { App::new() .add_plugins(DefaultPlugins) .add_plugins(GamePlugins) .run(); }
Configurable Plugins
Accept configuration by storing it in the plugin struct:
#![allow(unused)] fn main() { pub struct PhysicsPlugin { pub gravity: f32, pub substeps: u32, } impl Default for PhysicsPlugin { fn default() -> Self { Self { gravity: -9.81, substeps: 4, } } } impl Plugin for PhysicsPlugin { fn build(&self, app: &mut App) { app.insert_resource(PhysicsConfig { gravity: self.gravity, substeps: self.substeps, }); app.add_systems(FixedUpdate, ( apply_gravity, resolve_collisions, ).chain()); } } }