| name | bevy-ecs-fundamentals |
| description | Core Bevy ECS patterns: components, resources, systems, queries, commands, and App setup. Use when creating new Bevy projects, defining game data types, writing systems, or structuring App builders. |
Bevy ECS Fundamentals
Components
Components are plain Rust data types attached to entities. Derive Component:
use bevy::prelude::*;
#[derive(Component)]
struct Player {
name: String,
}
#[derive(Component)]
struct Health(f32);
#[derive(Component)]
enum PlayerState {
Idle,
Running,
Jumping,
}
#[derive(Component)]
struct Enemy;
#[derive(Component, Deref, DerefMut)]
struct Velocity(Vec2);
#[derive(Component, Default)]
struct Collider;
#[derive(Component)]
#[require(Sprite, Transform, Collider)]
struct Wall;
Immutable Components
Immutable components cannot be mutated once inserted — only replaced or removed:
#[derive(Component)]
#[component(immutable)]
struct ImmutableTag(u32);
Resources
Resources are global singletons. Derive Resource:
#[derive(Resource, Default)]
struct GameState {
score: usize,
level: u32,
}
#[derive(Resource)]
struct GameConfig {
max_players: usize,
difficulty: f32,
}
Systems
Systems are plain functions whose parameters are ECS system params:
fn greet() {
println!("Hello!");
}
fn print_score(state: Res<GameState>) {
info!("Score: {}", state.score);
}
fn increment_score(mut state: ResMut<GameState>) {
state.score += 1;
}
fn move_players(mut query: Query<(&Player, &mut Transform, &Velocity)>) {
for (player, mut transform, velocity) in &mut query {
transform.translation.x += velocity.x;
transform.translation.y += velocity.y;
}
}
fn setup(mut commands: Commands) {
commands.spawn(Camera2d);
}
fn exclusive_system(world: &mut World) {
let count = world.query::<&Player>().iter(world).count();
info!("{count} players");
}
System with Local State
Local<T> provides per-system persistent state:
fn count_frames(mut frame_count: Local<u32>) {
*frame_count += 1;
info!("Frame {}", *frame_count);
}
Fallible Systems
Systems can return Result for error handling:
fn fallible_setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
) -> Result {
let mesh = Sphere::new(1.0).mesh().ico(7)?;
commands.spawn(Mesh3d(meshes.add(mesh)));
Ok(())
}
Queries
Basic Query Patterns
fn read_positions(query: Query<&Transform>) {
for transform in &query {
info!("{:?}", transform.translation);
}
}
fn reset_positions(mut query: Query<&mut Transform>) {
for mut transform in &mut query {
transform.translation = Vec3::ZERO;
}
}
fn inspect(query: Query<(Entity, &Player, &Health)>) {
for (entity, player, health) in &query {
info!("{}: {} HP={}", entity, player.name, health.0);
}
}
fn optional_query(query: Query<(&Player, Option<&Health>)>) {
for (player, maybe_health) in &query {
if let Some(health) = maybe_health {
info!("{} has {} HP", player.name, health.0);
}
}
}
Query Filters
fn enemies_only(query: Query<&Transform, With<Enemy>>) { }
fn non_enemies(query: Query<&Transform, Without<Enemy>>) { }
fn on_health_change(query: Query<(&Player, &Health), Changed<Health>>) {
for (player, health) in &query {
info!("{} health changed to {}", player.name, health.0);
}
}
fn complex_filter(
query: Query<&Transform, (With<Player>, Without<Enemy>, Changed<Transform>)>,
) { }
fn either(
query: Query<Entity, Or<(With<Player>, With<Enemy>)>>,
) { }
Single Entity Query
Use Single when exactly one entity matches:
fn update_camera(
player: Single<&Transform, With<Player>>,
mut camera: Single<&mut Transform, (With<Camera>, Without<Player>)>,
) {
camera.translation = player.translation;
}
Ref for Change Detection Metadata
fn detect_changes(query: Query<Ref<Health>, Changed<Health>>) {
for health in &query {
info!(
"HP={}, added={}, changed={}, changed_by={}",
health.0,
health.is_added(),
health.is_changed(),
health.changed_by()
);
}
}
Parallel Query Iteration
fn parallel_move(mut query: Query<(&Velocity, &mut Transform)>) {
query.par_iter_mut().for_each(|(velocity, mut transform)| {
transform.translation += velocity.extend(0.0);
});
}
Commands
Commands defer mutations to the World:
fn spawn_entities(mut commands: Commands) {
commands.spawn((
Player { name: "Alice".into() },
Health(100.0),
Velocity(Vec2::ZERO),
Transform::default(),
));
commands.spawn_batch(vec![
(Player { name: "Bob".into() }, Health(80.0)),
(Player { name: "Carol".into() }, Health(90.0)),
]);
let entity = commands.spawn(Enemy).id();
commands.entity(entity).insert(Health(50.0));
commands.entity(entity).remove::<Health>();
commands.entity(entity).despawn();
commands.insert_resource(GameConfig {
max_players: 4,
difficulty: 1.0,
});
}
Hierarchy with Commands
fn spawn_hierarchy(mut commands: Commands) {
commands
.spawn((Name::new("Parent"), Transform::default()))
.with_children(|parent| {
parent.spawn((Name::new("Child"), Transform::from_xyz(1.0, 0.0, 0.0)));
});
let parent = commands.spawn(Name::new("Parent")).id();
let child = commands.spawn(Name::new("Child")).id();
commands.entity(parent).add_child(child);
commands.spawn((
Name::new("Parent"),
children![
(Name::new("Child A"),),
(Name::new("Child B"),),
],
));
}
App Builder
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.init_resource::<GameState>()
.insert_resource(GameConfig {
max_players: 4,
difficulty: 1.0,
})
.add_systems(Startup, setup)
.add_systems(Update, (
move_players,
check_collisions,
update_score,
))
.add_systems(FixedUpdate, physics_step)
.insert_resource(Time::<Fixed>::from_seconds(1.0 / 60.0))
.add_systems(Update, (
read_input,
apply_input,
render,
).chain())
.run();
}
SystemSet for Ordering
#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
enum GameSystems {
Input,
Physics,
Render,
}
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.configure_sets(Update, (
GameSystems::Input,
GameSystems::Physics,
GameSystems::Render,
).chain())
.add_systems(Update, read_input.in_set(GameSystems::Input))
.add_systems(Update, apply_physics.in_set(GameSystems::Physics))
.add_systems(Update, draw.in_set(GameSystems::Render))
.run();
}