Queries, Commands, and Entities
Components and entities
An entity is just an id; components are plain Rust values attached to it. Spawn one with Commands:
fn spawn_entities(mut commands: Commands) -> Option<()> {
commands.spawn((
Name("wanderer"),
Position { x: 0.0, y: 0.0 },
Velocity { dx: 1.0, dy: 2.0 },
));
Some(())
}
app.add_system(SystemStage::PreUpdate, spawn_entities.once());
Any tuple of types works as the component set — no registration step, no marker trait to implement. A component with no data at all (struct Asleep;) is a common, useful pattern purely for tagging entities so a query can filter by “has this,” regardless of what else it carries.
Querying
Query<Q> fetches every entity matching the component set Q. Iterate it with .iter():
fn move_entities(mut query: Query<(&Name, &mut Position, &Velocity)>) {
for (name, pos, vel) in query.iter() {
pos.x += vel.dx;
pos.y += vel.dy;
println!("{} moved to ({:.1}, {:.1})", name.0, pos.x, pos.y);
}
}
Mixing &T (read) and &mut T (write) in the same query is fine — hecs (the ECS crate Pebble builds on) borrow-checks it per-component at runtime, so this only panics if some other system concurrently holds a conflicting borrow, not merely because the query itself asks for both.
Include Entity directly in Q when you need the id back alongside the components — it’s a query term like any other, not a separate mechanism:
fn despawn_far_away(mut commands: Commands, mut query: Query<(Entity, &Name, &Position)>) {
for (entity, name, pos) in query.iter() {
if pos.x.hypot(pos.y) > 10.0 {
commands.despawn(entity);
}
}
}
Two more helpers for the cases where you don’t want the whole result set:
query.get(entity)— look up one known entity directly, without scanning the rest of the query.query.single()/query.get_single()— expect exactly one match (the player, the active camera).singlepanics if that’s not true;get_singlereturnsNoneinstead.
Commands: deferred mutation
despawn_far_away above calls commands.despawn(entity) while a query over the same world is still borrowed — safe only because Commands doesn’t touch the world immediately. Every Commands call (spawn, despawn, insert_resource, …) queues an operation into a command buffer, which is flushed once the current stage finishes running every system in it. This is also why a resource inserted via commands.insert_resource(...) isn’t visible to a system later in the same stage — only from the next stage (or the next tick) onward; use App::add_resource directly (outside of a system) or ResMut/direct mutation inside a system when you need it visible immediately.
Why the split?
Querying and mutating the same world simultaneously is exactly the kind of aliasing Rust’s borrow checker exists to prevent — Commands sidesteps it by not aliasing at all: it just appends “do this later” to a buffer, and the actual mutation happens at a single well-defined point (the end of the stage) where nothing else is borrowed. It’s the same trick most ECS frameworks use, under whatever name they give it (Bevy calls its version Commands too).