Trait queries
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.
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
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.
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.
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).
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)
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]
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.
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.
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.
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.
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.
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/
Any reason why this was closed?
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.