Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generalised ECS reactivity with Observers #10839

Merged
merged 136 commits into from
Jun 15, 2024
Merged

Conversation

james-j-obrien
Copy link
Contributor

@james-j-obrien james-j-obrien commented Dec 2, 2023

Objective

  • Provide an expressive way to register dynamic behavior in response to ECS changes that is consistent with existing bevy types and traits as to provide a smooth user experience.
  • Provide a mechanism for immediate changes in response to events during command application in order to facilitate improved query caching on the path to relations.

Solution

  • A new fundamental ECS construct, the Observer; inspired by flec's observers but adapted to better fit bevy's access patterns and rust's type system.

Examples

There are 3 main ways to register observers. The first is a "component observer" that looks like this:

world.observe(|trigger: Trigger<OnAdd, Transform>, query: Query<&Transform>| {
    let transform = query.get(trigger.entity()).unwrap();
});

The above code will spawn a new entity representing the observer that will run it's callback whenever the Transform component is added to an entity. This is a system-like function that supports dependency injection for all the standard bevy types: Query, Res, Commands etc. It also has a Trigger parameter that provides information about the trigger such as the target entity, and the event being triggered. Importantly these systems run during command application which is key for their future use to keep ECS internals up to date. There are similar events for OnInsert and OnRemove, and this will be expanded with things such as ArchetypeCreated, TableEmpty etc. in follow up PRs.

Another way to register an observer is an "entity observer" that looks like this:

world.entity_mut(entity).observe(|trigger: Trigger<Resize>| {
    // ...
});

Entity observers run whenever an event of their type is triggered targeting that specific entity. This type of observer will de-spawn itself if the entity (or entities) it is observing is ever de-spawned so as to not leave dangling observers.

Entity observers can also be spawned from deferred contexts such as other observers, systems, or hooks using commands:

commands.entity(entity).observe(|trigger: Trigger<Resize>| {
    // ...
});

Observers are not limited to in built event types, they can be used with any type that implements Event (which has been extended to implement Component). This means events can also carry data:

#[derive(Event)]
struct Resize { x: u32, y: u32 }

commands.entity(entity).observe(|trigger: Trigger<Resize>, query: Query<&mut Size>| {
    let event = trigger.event();
    // ...
});

// Will trigger the observer when commands are applied.
commands.trigger_targets(Resize { x: 10, y: 10 }, entity);

You can also trigger events that target more than one entity at a time:

commands.trigger_targets(Resize { x: 10, y: 10 }, [e1, e2]);

Additionally, Observers don't need entity targets:

app.observe(|trigger: Trigger<Quit>| {
})

commands.trigger(Quit);

In these cases, trigger.entity() will be a placeholder.

Observers are actually just normal entities with an ObserverState and Observer component! The observe() functions above are just shorthand for:

world.spawn(Observer::new(|trigger: Trigger<Resize>| {});

This will spawn the Observer system and use an on_add hook to add the ObserverState component.

Dynamic components and trigger types are also fully supported allowing for runtime defined trigger types.

Possible Follow-ups

  1. Deprecate RemovedComponents, observers should fulfill all use cases while being more flexible and performant.
  2. Queries as entities: Swap queries to entities and begin using observers listening to archetype creation triggers to keep their caches in sync, this allows unification of ObserverState and QueryState as well as unlocking several API improvements for Query and the management of QueryState.
  3. Trigger bubbling: For some UI use cases in particular users are likely to want some form of bubbling for entity observers, this is trivial to implement naively but ideally this includes an acceleration structure to cache hierarchy traversals.
  4. All kinds of other in-built trigger types.
  5. Optimization; in order to not bloat the complexity of the PR I have kept the implementation straightforward, there are several areas where performance can be improved. The focus for this PR is to get the behavior implemented and not incur a performance cost for users who don't use observers.

I am leaving each of these to follow up PR's in order to keep each of them reviewable as this already includes significant changes.

Cargo.toml Outdated Show resolved Hide resolved
Copy link
Contributor

The generated examples/README.md is out of sync with the example metadata in Cargo.toml or the example readme template. Please run cargo run -p build-templated-pages -- update examples to update it, and commit the file change.

@cart cart added this pull request to the merge queue Jun 15, 2024
Merged via the queue into bevyengine:main with commit eb3c813 Jun 15, 2024
31 checks passed
mnmaita pushed a commit to mnmaita/bevy that referenced this pull request Jun 16, 2024
- Provide an expressive way to register dynamic behavior in response to
ECS changes that is consistent with existing bevy types and traits as to
provide a smooth user experience.
- Provide a mechanism for immediate changes in response to events during
command application in order to facilitate improved query caching on the
path to relations.

- A new fundamental ECS construct, the `Observer`; inspired by flec's
observers but adapted to better fit bevy's access patterns and rust's
type system.

---

There are 3 main ways to register observers. The first is a "component
observer" that looks like this:
```rust
world.observe(|trigger: Trigger<OnAdd, Transform>, query: Query<&Transform>| {
    let transform = query.get(trigger.entity()).unwrap();
});
```
The above code will spawn a new entity representing the observer that
will run it's callback whenever the `Transform` component is added to an
entity. This is a system-like function that supports dependency
injection for all the standard bevy types: `Query`, `Res`, `Commands`
etc. It also has a `Trigger` parameter that provides information about
the trigger such as the target entity, and the event being triggered.
Importantly these systems run during command application which is key
for their future use to keep ECS internals up to date. There are similar
events for `OnInsert` and `OnRemove`, and this will be expanded with
things such as `ArchetypeCreated`, `TableEmpty` etc. in follow up PRs.

Another way to register an observer is an "entity observer" that looks
like this:
```rust
world.entity_mut(entity).observe(|trigger: Trigger<Resize>| {
    // ...
});
```
Entity observers run whenever an event of their type is triggered
targeting that specific entity. This type of observer will de-spawn
itself if the entity (or entities) it is observing is ever de-spawned so
as to not leave dangling observers.

Entity observers can also be spawned from deferred contexts such as
other observers, systems, or hooks using commands:
```rust
commands.entity(entity).observe(|trigger: Trigger<Resize>| {
    // ...
});
```

Observers are not limited to in built event types, they can be used with
any type that implements `Event` (which has been extended to implement
Component). This means events can also carry data:

```rust
struct Resize { x: u32, y: u32 }

commands.entity(entity).observe(|trigger: Trigger<Resize>, query: Query<&mut Size>| {
    let event = trigger.event();
    // ...
});

// Will trigger the observer when commands are applied.
commands.trigger_targets(Resize { x: 10, y: 10 }, entity);
```

You can also trigger events that target more than one entity at a time:

```rust
commands.trigger_targets(Resize { x: 10, y: 10 }, [e1, e2]);
```

Additionally, Observers don't _need_ entity targets:

```rust
app.observe(|trigger: Trigger<Quit>| {
})

commands.trigger(Quit);
```

In these cases, `trigger.entity()` will be a placeholder.

Observers are actually just normal entities with an `ObserverState` and
`Observer` component! The `observe()` functions above are just shorthand
for:

```rust
world.spawn(Observer::new(|trigger: Trigger<Resize>| {});
```

This will spawn the `Observer` system and use an `on_add` hook to add
the `ObserverState` component.

Dynamic components and trigger types are also fully supported allowing
for runtime defined trigger types.

1. Deprecate `RemovedComponents`, observers should fulfill all use cases
while being more flexible and performant.
2. Queries as entities: Swap queries to entities and begin using
observers listening to archetype creation triggers to keep their caches
in sync, this allows unification of `ObserverState` and `QueryState` as
well as unlocking several API improvements for `Query` and the
management of `QueryState`.
3. Trigger bubbling: For some UI use cases in particular users are
likely to want some form of bubbling for entity observers, this is
trivial to implement naively but ideally this includes an acceleration
structure to cache hierarchy traversals.
4. All kinds of other in-built trigger types.
5. Optimization; in order to not bloat the complexity of the PR I have
kept the implementation straightforward, there are several areas where
performance can be improved. The focus for this PR is to get the
behavior implemented and not incur a performance cost for users who
don't use observers.

I am leaving each of these to follow up PR's in order to keep each of
them reviewable as this already includes significant changes.

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
Co-authored-by: MiniaczQ <xnetroidpl@gmail.com>
Co-authored-by: Carter Anderson <mcanders1@gmail.com>
alice-i-cecile added a commit that referenced this pull request Jun 16, 2024
# Objective

- Fixes #13825 

## Solution

- Cherry picked and fixed non-trivial conflicts to be able to merge
#10839 into the 0.14 release branch.

Link to PR: #10839

Co-authored-by: James O'Brien <james.obrien@drafly.net>
Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
Co-authored-by: MiniaczQ <xnetroidpl@gmail.com>
Co-authored-by: Carter Anderson <mcanders1@gmail.com>
@alice-i-cecile alice-i-cecile added the D-Complex Quite challenging from either a design or technical perspective. Ask for help! label Jun 19, 2024
github-merge-queue bot pushed a commit that referenced this pull request Jun 27, 2024
# Objective

`StaticSystemParam` should delegate all `SystemParam` methods to the
inner param, but it looks like it was missed when the new `queue()`
method was added in #10839.

## Solution

Implement `StaticSystemParam::queue()` to delegate to the inner param.
mockersf pushed a commit that referenced this pull request Jun 27, 2024
# Objective

`StaticSystemParam` should delegate all `SystemParam` methods to the
inner param, but it looks like it was missed when the new `queue()`
method was added in #10839.

## Solution

Implement `StaticSystemParam::queue()` to delegate to the inner param.
zmbush pushed a commit to zmbush/bevy that referenced this pull request Jul 3, 2024
# Objective

`StaticSystemParam` should delegate all `SystemParam` methods to the
inner param, but it looks like it was missed when the new `queue()`
method was added in bevyengine#10839.

## Solution

Implement `StaticSystemParam::queue()` to delegate to the inner param.
@BD103 BD103 added the C-Breaking-Change A breaking change to Bevy's public API that needs to be noted in a migration guide label Jul 6, 2024
Copy link
Contributor

github-actions bot commented Jul 6, 2024

It looks like your PR is a breaking change, but you didn't provide a migration guide.

Could you add some context on what users should update when this change get released in a new version of Bevy?
It will be used to help writing the migration guide for the version. Putting it after a ## Migration Guide will help it get automatically picked up by our tooling.

@BD103
Copy link
Member

BD103 commented Jul 6, 2024

bevyengine/bevy-website#1526 caught this as a breaking change, since you can no longer annotate #[derive(Event, Component)] on a struct because it results in Component being implemented twice.

I'm making a PR to fix this, so no further action on this side needs to be done. :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-ECS Entities, components, systems, and events C-Breaking-Change A breaking change to Bevy's public API that needs to be noted in a migration guide C-Enhancement A new feature D-Complex Quite challenging from either a design or technical perspective. Ask for help! S-Needs-SME Decision or review from an SME is required X-Controversial There is active debate or serious implications around merging this PR
Projects
Status: Responded
Status: Merged PR
Status: Done
Development

Successfully merging this pull request may close these issues.