bevy/crates/bevy_ecs
James O'Brien ea42d14344
Dynamic queries and builder API (#9774)
# Objective
Expand the existing `Query` API to support more dynamic use cases i.e.
scripting.

## Prior Art
 - #6390 
 - #8308 
- #10037

## Solution
- Create a `QueryBuilder` with runtime methods to define the set of
component accesses for a built query.
- Create new `WorldQueryData` implementations `FilteredEntityMut` and
`FilteredEntityRef` as variants of `EntityMut` and `EntityRef` that
provide run time checked access to the components included in a given
query.
- Add new methods to `Query` to create "query lens" with a subset of the
access of the initial query.

### Query Builder
The `QueryBuilder` API allows you to define a query at runtime. At it's
most basic use it will simply create a query with the corresponding type
signature:
```rust
let query = QueryBuilder::<Entity, With<A>>::new(&mut world).build();
// is equivalent to
let query = QueryState::<Entity, With<A>>::new(&mut world);
```
Before calling `.build()` you also have the opportunity to add
additional accesses and filters. Here is a simple example where we add
additional filter terms:
```rust
let entity_a = world.spawn((A(0), B(0))).id();
let entity_b = world.spawn((A(0), C(0))).id();

let mut query_a = QueryBuilder::<Entity>::new(&mut world)
    .with::<A>()
    .without::<C>()
    .build();
            
assert_eq!(entity_a, query_a.single(&world));
```
This alone is useful in that allows you to decide which archetypes your
query will match at runtime. However it is also very limited, consider a
case like the following:
```rust
let query_a = QueryBuilder::<&A>::new(&mut world)
// Add an additional access
    .data::<&B>()
    .build();
```
This will grant the query an additional read access to component B
however we have no way of accessing the data while iterating as the type
signature still only includes &A. For an even more concrete example of
this consider dynamic components:
```rust
let query_a = QueryBuilder::<Entity>::new(&mut world)
// Adding a filter is easy since it doesn't need be read later
    .with_id(component_id_a)
// How do I access the data of this component?
    .ref_id(component_id_b)
    .build();
```
With this in mind the `QueryBuilder` API seems somewhat incomplete by
itself, we need some way method of accessing the components dynamically.
So here's one:
### Query Transmutation
If the problem is not having the component in the type signature why not
just add it? This PR also adds transmute methods to `QueryBuilder` and
`QueryState`. Here's a simple example:
```rust
world.spawn(A(0));
world.spawn((A(1), B(0)));
let mut query = QueryBuilder::<()>::new(&mut world)
    .with::<B>()
    .transmute::<&A>()
    .build();

query.iter(&world).for_each(|a| assert_eq!(a.0, 1));
```
The `QueryState` and `QueryBuilder` transmute methods look quite similar
but are different in one respect. Transmuting a builder will always
succeed as it will just add the additional accesses needed for the new
terms if they weren't already included. Transmuting a `QueryState` will
panic in the case that the new type signature would give it access it
didn't already have, for example:
```rust
let query = QueryState::<&A, Option<&B>>::new(&mut world);
/// This is fine, the access for Option<&A> is less restrictive than &A
query.transmute::<Option<&A>>(&world);
/// Oh no, this would allow access to &B on entities that might not have it, so it panics
query.transmute::<&B>(&world);
/// This is right out
query.transmute::<&C>(&world);
```
This is quite an appealing API to also have available on `Query` however
it does pose one additional wrinkle: In order to to change the iterator
we need to create a new `QueryState` to back it. `Query` doesn't own
it's own state though, it just borrows it, so we need a place to borrow
it from. This is why `QueryLens` exists, it is a place to store the new
state so it can be borrowed when you call `.query()` leaving you with an
API like this:
```rust
fn function_that_takes_a_query(query: &Query<&A>) {
    // ...
}

fn system(query: Query<(&A, &B)>) {
    let lens = query.transmute_lens::<&A>();
    let q = lens.query();
    function_that_takes_a_query(&q);
}
```
Now you may be thinking: Hey, wait a second, you introduced the problem
with dynamic components and then described a solution that only works
for static components! Ok, you got me, I guess we need a bit more:
### Filtered Entity References
Currently the only way you can access dynamic components on entities
through a query is with either `EntityMut` or `EntityRef`, however these
can access all components and so conflict with all other accesses. This
PR introduces `FilteredEntityMut` and `FilteredEntityRef` as
alternatives that have additional runtime checking to prevent accessing
components that you shouldn't. This way you can build a query with a
`QueryBuilder` and actually access the components you asked for:
```rust
let mut query = QueryBuilder::<FilteredEntityRef>::new(&mut world)
    .ref_id(component_id_a)
    .with(component_id_b)
    .build();

let entity_ref = query.single(&world);

// Returns Some(Ptr) as we have that component and are allowed to read it
let a = entity_ref.get_by_id(component_id_a);
// Will return None even though the entity does have the component, as we are not allowed to read it
let b = entity_ref.get_by_id(component_id_b);
```
For the most part these new structs have the exact same methods as their
non-filtered equivalents.

Putting all of this together we can do some truly dynamic ECS queries,
check out the `dynamic` example to see it in action:
```
Commands:
    comp, c   Create new components
    spawn, s  Spawn entities
    query, q  Query for entities
Enter a command with no parameters for usage.

> c A, B, C, Data 4  
Component A created with id: 0
Component B created with id: 1
Component C created with id: 2
Component Data created with id: 3

> s A, B, Data 1
Entity spawned with id: 0v0

> s A, C, Data 0
Entity spawned with id: 1v0

> q &Data
0v0: Data: [1, 0, 0, 0]
1v0: Data: [0, 0, 0, 0]

> q B, &mut Data                                                                                     
0v0: Data: [2, 1, 1, 1]

> q B || C, &Data 
0v0: Data: [2, 1, 1, 1]
1v0: Data: [0, 0, 0, 0]
```
## Changelog
 - Add new `transmute_lens` methods to `Query`.
- Add new types `QueryBuilder`, `FilteredEntityMut`, `FilteredEntityRef`
and `QueryLens`
- `update_archetype_component_access` has been removed, archetype
component accesses are now determined by the accesses set in
`update_component_access`
- Added method `set_access` to `WorldQuery`, this is called before
`update_component_access` for queries that have a restricted set of
accesses, such as those built by `QueryBuilder` or `QueryLens`. This is
primarily used by the `FilteredEntity*` variants and has an empty trait
implementation.
- Added method `get_state` to `WorldQuery` as a fallible version of
`init_state` when you don't have `&mut World` access.

## Future Work
Improve performance of `FilteredEntityMut` and `FilteredEntityRef`,
currently they have to determine the accesses a query has in a given
archetype during iteration which is far from ideal, especially since we
already did the work when matching the archetype in the first place. To
avoid making more internal API changes I have left it out of this PR.

---------

Co-authored-by: Mike Hsu <mike.hsu@gmail.com>
2024-01-16 19:16:49 +00:00
..
examples Only run event systems if they have tangible work to do (#7728) 2023-09-24 00:16:33 +00:00
macros Dynamic queries and builder API (#9774) 2024-01-16 19:16:49 +00:00
src Dynamic queries and builder API (#9774) 2024-01-16 19:16:49 +00:00
Cargo.toml Async channel v2 (#10692) 2024-01-15 19:23:00 +00:00
README.md delete methods deprecated in 0.12 (#10693) 2023-11-24 16:15:47 +00:00

Bevy ECS

License Crates.io Downloads Docs Discord

What is Bevy ECS?

Bevy ECS is an Entity Component System custom-built for the Bevy game engine. It aims to be simple to use, ergonomic, fast, massively parallel, opinionated, and featureful. It was created specifically for Bevy's needs, but it can easily be used as a standalone crate in other projects.

ECS

All app logic in Bevy uses the Entity Component System paradigm, which is often shortened to ECS. ECS is a software pattern that involves breaking your program up into Entities, Components, and Systems. Entities are unique "things" that are assigned groups of Components, which are then processed using Systems.

For example, one entity might have a Position and Velocity component, whereas another entity might have a Position and UI component. You might have a movement system that runs on all entities with a Position and Velocity component.

The ECS pattern encourages clean, decoupled designs by forcing you to break up your app data and logic into its core components. It also helps make your code faster by optimizing memory access patterns and making parallelism easier.

Concepts

Bevy ECS is Bevy's implementation of the ECS pattern. Unlike other Rust ECS implementations, which often require complex lifetimes, traits, builder patterns, or macros, Bevy ECS uses normal Rust data types for all of these concepts:

Components

Components are normal Rust structs. They are data stored in a World and specific instances of Components correlate to Entities.

use bevy_ecs::prelude::*;

#[derive(Component)]
struct Position { x: f32, y: f32 }

Worlds

Entities, Components, and Resources are stored in a World. Worlds, much like Rust std collections like HashSet and Vec, expose operations to insert, read, write, and remove the data they store.

use bevy_ecs::world::World;

let world = World::default();

Entities

Entities are unique identifiers that correlate to zero or more Components.

use bevy_ecs::prelude::*;

#[derive(Component)]
struct Position { x: f32, y: f32 }
#[derive(Component)]
struct Velocity { x: f32, y: f32 }

let mut world = World::new();

let entity = world
    .spawn((Position { x: 0.0, y: 0.0 }, Velocity { x: 1.0, y: 0.0 }))
    .id();

let entity_ref = world.entity(entity);
let position = entity_ref.get::<Position>().unwrap();
let velocity = entity_ref.get::<Velocity>().unwrap();

Systems

Systems are normal Rust functions. Thanks to the Rust type system, Bevy ECS can use function parameter types to determine what data needs to be sent to the system. It also uses this "data access" information to determine what Systems can run in parallel with each other.

use bevy_ecs::prelude::*;

#[derive(Component)]
struct Position { x: f32, y: f32 }

fn print_position(query: Query<(Entity, &Position)>) {
    for (entity, position) in &query {
        println!("Entity {:?} is at position: x {}, y {}", entity, position.x, position.y);
    }
}

Resources

Apps often require unique resources, such as asset collections, renderers, audio servers, time, etc. Bevy ECS makes this pattern a first class citizen. Resource is a special kind of component that does not belong to any entity. Instead, it is identified uniquely by its type:

use bevy_ecs::prelude::*;

#[derive(Resource, Default)]
struct Time {
    seconds: f32,
}

let mut world = World::new();

world.insert_resource(Time::default());

let time = world.get_resource::<Time>().unwrap();

// You can also access resources from Systems
fn print_time(time: Res<Time>) {
    println!("{}", time.seconds);
}

The resources.rs example illustrates how to read and write a Counter resource from Systems.

Schedules

Schedules run a set of Systems according to some execution strategy. Systems can be added to any number of System Sets, which are used to control their scheduling metadata.

The built in "parallel executor" considers dependencies between systems and (by default) run as many of them in parallel as possible. This maximizes performance, while keeping the system execution safe. To control the system ordering, define explicit dependencies between systems and their sets.

Using Bevy ECS

Bevy ECS should feel very natural for those familiar with Rust syntax:

use bevy_ecs::prelude::*;

#[derive(Component)]
struct Position { x: f32, y: f32 }
#[derive(Component)]
struct Velocity { x: f32, y: f32 }

// This system moves each entity with a Position and Velocity component
fn movement(mut query: Query<(&mut Position, &Velocity)>) {
    for (mut position, velocity) in &mut query {
        position.x += velocity.x;
        position.y += velocity.y;
    }
}

fn main() {
    // Create a new empty World to hold our Entities and Components
    let mut world = World::new();

    // Spawn an entity with Position and Velocity components
    world.spawn((
        Position { x: 0.0, y: 0.0 },
        Velocity { x: 1.0, y: 0.0 },
    ));

    // Create a new Schedule, which defines an execution strategy for Systems
    let mut schedule = Schedule::default();

    // Add our system to the schedule
    schedule.add_systems(movement);

    // Run the schedule once. If your app has a "loop", you would run this once per loop
    schedule.run(&mut world);
}

Features

Query Filters

use bevy_ecs::prelude::*;

#[derive(Component)]
struct Position { x: f32, y: f32 }
#[derive(Component)]
struct Player;
#[derive(Component)]
struct Alive;

// Gets the Position component of all Entities with Player component and without the Alive
// component. 
fn system(query: Query<&Position, (With<Player>, Without<Alive>)>) {
    for position in &query {
    }
}

Change Detection

Bevy ECS tracks all changes to Components and Resources.

Queries can filter for changed Components:

use bevy_ecs::prelude::*;

#[derive(Component)]
struct Position { x: f32, y: f32 }
#[derive(Component)]
struct Velocity { x: f32, y: f32 }

// Gets the Position component of all Entities whose Velocity has changed since the last run of the System
fn system_changed(query: Query<&Position, Changed<Velocity>>) {
    for position in &query {
    }
}

// Gets the Position component of all Entities that had a Velocity component added since the last run of the System
fn system_added(query: Query<&Position, Added<Velocity>>) {
    for position in &query {
    }
}

Resources also expose change state:

use bevy_ecs::prelude::*;

#[derive(Resource)]
struct Time(f32);

// Prints "time changed!" if the Time resource has changed since the last run of the System
fn system(time: Res<Time>) {
    if time.is_changed() {
        println!("time changed!");
    }
}

The change_detection.rs example shows how to query only for updated entities and react on changes in resources.

Component Storage

Bevy ECS supports multiple component storage types.

Components can be stored in:

  • Tables: Fast and cache friendly iteration, but slower adding and removing of components. This is the default storage type.
  • Sparse Sets: Fast adding and removing of components, but slower iteration.

Component storage types are configurable, and they default to table storage if the storage is not manually defined.

use bevy_ecs::prelude::*;

#[derive(Component)]
struct TableStoredComponent;

#[derive(Component)]
#[component(storage = "SparseSet")]
struct SparseStoredComponent;

Component Bundles

Define sets of Components that should be added together.

use bevy_ecs::prelude::*;

#[derive(Default, Component)]
struct Player;
#[derive(Default, Component)]
struct Position { x: f32, y: f32 }
#[derive(Default, Component)]
struct Velocity { x: f32, y: f32 }

#[derive(Bundle, Default)]
struct PlayerBundle {
    player: Player,
    position: Position,
    velocity: Velocity,
}

let mut world = World::new();

// Spawn a new entity and insert the default PlayerBundle
world.spawn(PlayerBundle::default());

// Bundles play well with Rust's struct update syntax
world.spawn(PlayerBundle {
    position: Position { x: 1.0, y: 1.0 },
    ..Default::default()
});

Events

Events offer a communication channel between one or more systems. Events can be sent using the system parameter EventWriter and received with EventReader.

use bevy_ecs::prelude::*;

#[derive(Event)]
struct MyEvent {
    message: String,
}

fn writer(mut writer: EventWriter<MyEvent>) {
    writer.send(MyEvent {
        message: "hello!".to_string(),
    });
}

fn reader(mut reader: EventReader<MyEvent>) {
    for event in reader.read() {
    }
}

A minimal set up using events can be seen in events.rs.