bevy_query_observer

Crates.iobevy_query_observer
lib.rsbevy_query_observer
version0.2.1
created_at2025-12-14 01:36:38.813846+00
updated_at2026-01-03 10:25:32.879959+00
descriptionMore sophisticated observers for Bevy
homepage
repositoryhttps://github.com/corvusprudens/bevy_query_observer
max_upload_size
id1983626
size141,996
Corvus (CorvusPrudens)

documentation

https://docs.rs/bevy_query_observer

README

crates.io docs.rs

bevy_query_observer provides observers that trigger when an entity starts or stops matching a query. Query observers also trigger when a matching entity's data changes according to lifecycle events.

#[derive(Component)]
struct Red;

#[derive(Component)]
struct Blue;

fn red_and_blue(
    data: Start<Entity, (With<Red>, With<Blue>)>,
    mut commands: Commands,
) {
    warn!("{:?} is both red and blue", *data);
}

The design is based on Lifecycle event observers for queries.

Getting started

Since bevy_query_observer is implemented entirely in terms of lifecycle events, all you need to get started is to add the dependency.

[dependencies]
bevy = "0.17.3"
bevy_query_observer = "0.2.0"

Query observers are added like normal observers.

fn plugin(app: &mut App) {
    app.add_start_observer(red_and_blue);
}

Like Observer, you can spawn QueryObserver directly for greater control over what it watches and where it's spawned.

fn target_entity(mut commands: Commands) {
    let target = commands.spawn_empty().id();

    let query_observer = QueryObserver::start(red_and_blue).with_entity(target);
    commands.spawn_query_observer(query_observer);
}

Maintaining non-trivial invariants

Query observers make it easier to maintain non-trivial invariants. This example, taken from the initial design discussion, illustrates how you can maintain an entity index with excluding marker components.

#[derive(Component, Eq, PartialEq, Hash, Copy, Clone)]
#[component(immutable)]
struct Value(usize);

#[derive(Component)]
struct ExcludeFromIndex;

#[derive(Resource)]
struct Index(HashMap<Value, EntityHashSet>);

fn add_to_index(
    event: Start<(Entity, &Value), Without<ExcludeFromIndex>>,
    mut index: ResMut<Index>,
) {
    let (entity, value) = event.into_inner();
    index.0.entry(*value).or_default().insert(entity);
}

fn remove_from_index(
    event: Stop<(Entity, &Value), Without<ExcludeFromIndex>>,
    mut index: ResMut<Index>,
) {
    let (entity, value) = event.into_inner();
    index.0.entry(*value).or_default().remove(&entity);
}

Start and Stop handle default query filters automatically, meaning the above implicitly accounts for Disabled and any other default filter.

Expressing the above in terms of normal observers requires at least four hand-written observers. Each disabling component requires two additional observers.

Limitations

Because bevy_query_observer is implemented in user space and bevy_ecs isn't yet expressive enough, the crate comes with a few limitations.

  1. Certain observers that require evaluation before an archetype change are faked.

    For example, Stop<(), Without<MyFilter>> must run just before MyFilter is added to fulfill the semantics of Stop. However, Bevy's lifecycle events can't express this, so the query observer ignores Without<MyFilter> in this scenario when fetching the data. In practice this is usually fine, but it may deny simultaneous mutable access or break subtle invariants.

  2. Only a subset of Bevy's built-in QueryData and QueryFilter types are supported.

    While bevy_query_observer could spawn observers in terms of a Query's access, Bevy's access types are private. As a result, Start and Stop depend on QueryObserverAccess, meaning any custom query data or filters require an implementation.

  3. Performance isn't great

    The overhead of evaluating a query observer is around three times greater than a normal observer. Spawning query observers is nearly an order of magnitude slower.

  4. Query observers frequently early return

    Since each unique component in Start and Stop needs a dedicated observer, lifecycle events will frequently trigger query observer evaluations that do nothing. Default filters are included, so inserting or removing a disabling component may trigger many short-circuiting observers.

    In principle, first party query observers could reduce unnecessary evaluations using archetype information.

Bevy version compatibility

bevy bevy_query_observer
0.17 0.1, 0.2

License

Licensed under either of Apache License, Version 2.0 or MIT license at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this crate by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
Commit count: 0

cargo fmt