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

Trait queries #39

Closed

Conversation

alice-i-cecile
Copy link
Member

@alice-i-cecile alice-i-cecile commented Oct 21, 2021

RENDERED

This RFC introduces trait queries, which use runtime trait information about components (powered by reflection) to enable more direct and extensible methods for abstracting over component types that share a common trait.

Trait queries allow users to query and filter for all (or a single) components that implement a given trait.

Many thanks to @Davier for the initial prototype and idea.

@alice-i-cecile alice-i-cecile changed the title Trait queries and trait-universal systems Trait queries Oct 21, 2021
@alice-i-cecile alice-i-cecile marked this pull request as draft October 21, 2021 05:35
@alice-i-cecile
Copy link
Member Author

Stumbled across this existing alternate implementation of the idea as a stand-alone Rust crate: https://github.com/Diggsey/query_interface/blob/master/src/lib.rs

@B-Reif
Copy link

B-Reif commented Jan 3, 2022

What are the performance characteristics of this? I would expect iterating over a set of heterogenous component data to be slower than (presumably memory-contiguous) homogenous data.

@B-Reif
Copy link

B-Reif commented Jan 3, 2022

From an idiomatic perspective, I tend to think of components and systems as a separation of data and behavior. Since traits implement behaviors, a trait queries seems to un-separate these concerns.

@alice-i-cecile
Copy link
Member Author

What are the performance characteristics of this? I would expect iterating over a set of heterogenous component data to be slower than (presumably memory-contiguous) homogenous data.

This is worth calling out. Like all performance questions, this will ultimately come down to benchmarking. And, this is largely an ergonomic feature: I would expect this to be more important in rich, complex areas of games, rather than compute-heavy inner loops.

However, we would be able to cache which components implement the desired traits, and thus cache which archetypes must be touched. Effectively, a trait query expands out into a Query<(Option<&A>, Option<&B>, Option<&C>)> at system initialization time: the cost is only paid once.

As a result, it should have similar performance characteristics to other queries which need to mutate the same number of archetypes and components (although this may be high, due to the nature of the feature). There may be a tiny amount of additional overhead due to having to construct the per entity iterators for all components that impl the correct trait.

So, not bare-metal-fast, but should be quite good. And probably very competitive with other approaches that could be used to achieve similar levels of flexibility / reusability (e.g. spawning a bajillion generic systems will require overhead on system initialization, sticking this into a Box<Vec<dyn Trait>> will have dispatch overhead and force stack allocation).

@alice-i-cecile
Copy link
Member Author

From an idiomatic perspective, I tend to think of components and systems as a separation of data and behavior. Since traits implement behaviors, a trait queries seems to un-separate these concerns.

My perspective here is actually a bit different, if admittedly slightly unconventional: I tend to think of components as more than just Raw Data. Instead, they enable behavior, toggling on and off different effects and systems on an entity within the context of a specific schedule. This perspective is why the dataless marker component pattern works (to great effect!).

I tend to view trait queries in this light, and in existing Bevy code bases I'm often reaching for generic systems with a trait bound to imitate this functionality. Like marker components, components with a trait enable behavior: the existence of a trait just gives some tighter correctness bounds and enables heterogeneous implementations of how exactly that behavior should be implemented.

Ultimately trait queries serve two main purposes:

  • allow for flexible, expressive behavior like you might see in UI or scripting without a massive proliferation of systems or terrible "component stores a &mut Commands function" patterns
  • collect over multiple components with the same trait in a way that is fundamentally impossible using generic systems (as they can only see the component type that they were added on)

@DrLuke
Copy link

DrLuke commented Apr 2, 2022

I have an usecase where this would be extremely useful for my implementation of an OSC dispatcher. An OSC dispatcher receives an OSC message, which has an address, and forwards it to all receivers within an application that have a matching address:

flowchart LR

r["Server (UDP)"] -->|receive message| d[Dispatcher]

d -- match --> A[Receiver Component A]
d -. no match .-x B[Receiver Component B]
d -- match --> C[Receiver Component C]
Loading

The dispatcher must have a way to access all Components that can receive OSC messages. I've thus implemented an OscReceiver component to allow me to write a query to find them all. That works super well until you want to receive at more than one address in an entity (very common usecase), at which point you run into trouble.

Instead it would be much easier to implement an OscReceiver trait for each component that can receive messages and to just query for that.

@JoJoJet
Copy link
Member

JoJoJet commented Oct 11, 2022

I threw together a quick and dirty prototype, which uses a different approach than the previous attempts. I'm using it in my game, and it works quite well. Here's a usage example: https://github.com/JoJoJet/bevy-trait-query/blob/main/examples/people.rs

This does not use reflection. To create each trait object, the WorldQuery impl reuses a single function pointer for each archetype, which I think will make this approach cache-friendly.

If more than one component implements the trait for a given entity, it just ignores the extras for now. I have some ideas for making it universal, though.

@JoJoJet
Copy link
Member

JoJoJet commented Oct 12, 2022

I figured out how to do it entirely with pointer arithmetic, which should make it comparable in performance to queries of concrete types. Although this requires ptr_byte_offsets in order to not be UB. It seems like that's stabilizing soonish, though.

@JoJoJet
Copy link
Member

JoJoJet commented Oct 13, 2022

Figured out universal queries. Here's some benchmarks:

Concrete types Trait-existential Trait-universal
1 match 16.100 µs 29.405 µs 58.224 µs
2 match 17.357 µs 30.930 µs 94.012 µs
red 1-2 match - 31.504 µs 72.934 µs

If I apply the nightly-only optimization mentioned earlier...

Concrete types Trait-existential Trait-universal
1 match 16.160 µs 19.382 µs 48.560 µs
2 matches 17.339 µs 22.036 µs 77.447 µs
1-2 matches - 19.893 µs 64.074 µs

I imagine this will rapidly slow down as the number of trait impls and archetypes increase, but that's probably unavoidable. I really like this as a baseline.

@TimJentzsch
Copy link

TimJentzsch commented Oct 13, 2022

I also have a use-case for this: Ergonomic text localization.

I am working on integrating Project Fluent into Bevy, which allows localization that goes beyond simple string replacement.

To use this in code, you have to provide a message ID to identify the text you want to insert. Optionally, you can also have variables which can be strings, numbers, etc. that is replaced in the message and might be used to stuff like pluralization.

Now, because you'll probably have a lot of text in your game, you don't want to manually define a system that updates your Text components whenever the locale or the variables change.
This is why I want to use a component-based system, where you add a component that defines the message ID and then the corresponding Text component is updated automatically.

The problem is now to incorporate the variables, which will be components or resources themselves. The plugin cannot know in advance which concrete components or resources will be needed. I first experimented with using the TypeIds, but to query the component from the World you need to know the concrete type in the end.

I experimented with multiple things, but I think this is simply not possible without trait queries. With trait queries I could probably define an update system that can check if the variables have updated, pulls their values from the World and then updates the Text components.

@JoJoJet
Copy link
Member

JoJoJet commented Oct 17, 2022

I've polished up my implementation, and it's now ready for wider testing. I would highly appreciate feedback from anyone who tries using this in their game. Feel free to reach out if you have any questions.

Crate: https://crates.io/crates/bevy-trait-query
Repo: https://github.com/JoJoJet/bevy-trait-query/

@musjj
Copy link

musjj commented Oct 7, 2023

Any reason why this was closed?

@alice-i-cecile
Copy link
Member Author

I'd still like this, but the 3rd party implementation is quite good. If we adopt this, it won't need to go through an RFC IMO.

@Inspirateur
Copy link

I'd still like this, but the 3rd party implementation is quite good. If we adopt this, it won't need to go through an RFC IMO.

Is there an issue tracking this adoption ? I can't seem to find it

@alice-i-cecile
Copy link
Member Author

@Inspirateur no issue yet: can you please open one?

@Inspirateur
Copy link

@Inspirateur no issue yet: can you please open one?

done here bevyengine/bevy#15970

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants