-
-
Notifications
You must be signed in to change notification settings - Fork 3.7k
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
[Merged by Bors] - Query::get_unique #1263
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Enough people have asked for this that its definitely worth considering. I think I'm on board, but I'll need a sec to consider alternatives.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Implementation looks good, just a few thoughts about the usage patterns
examples/game/breakout.rs
Outdated
for mut text in query.iter_mut() { | ||
text.value = format!("Score: {}", scoreboard.score); | ||
} | ||
query.get_unique_mut().unwrap().value = format!("Score: {}", scoreboard.score) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would say this is a good use of this api
examples/game/breakout.rs
Outdated
@@ -201,7 +199,7 @@ fn ball_collision_system( | |||
mut ball_query: Query<(&mut Ball, &Transform, &Sprite)>, | |||
collider_query: Query<(Entity, &Collider, &Transform, &Sprite)>, | |||
) { | |||
for (mut ball, ball_transform, sprite) in ball_query.iter_mut() { | |||
if let Ok((mut ball, ball_transform, sprite)) = ball_query.get_unique_mut() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In general however, I think constraining uses like this not ideal. For example, the current version would trivially extend to having multiple fields with the same controls (to allow something like those things where people play multiple pokemon games with one set of inputs)
Although on the other hand, for this simple example this is probably fine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah this is sort of the crux of my hesitance to adopt get_unique(). It encourages people to design around "single entity" patterns. Maybe thats a good thing, but its a paradigm shift with implications we need to consider.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I expect that the cases where we don't mind this pattern would be for rendering Resources
, i.e. it's just for getting data from resources onto the screen, which I've called out as a sensible use above. That is, it's a logic error to have multiple scoreboard texts but a single Score resource
But this case is just using that API for the sake of using it, where there's not actually an especially compelling reason to.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I agree. In this case, there might be a future "breakout++" where multiple balls come into play. And I think that for most things even if there is only one item we should encourage query.iter()
. Imo get_unique()
should be reserved for cases that are truly game breaking.
I ran into another really useful application for this today: unit testing and debugging with marker components. When unit testing (or interactively debugging), you'll often set up toy examples where you create a few entities, give them each a different marker component, and then query them to inspect their behavior. This pattern is made much more ergonomic with The other approaches don't work well here:
|
@cart I think this is good to merge, if you've been convinced that it's a useful API :) |
Another really central pattern that's coming up for my turn-based RPG:
This is a clear, idiomatic use of |
Regarding naming, I think "unique" is unfortunate. In many contexts, "unique" is understood as de-duplication, i.e. removing duplicate elements. Instead, I would suggest "single" for this functionality, as it clarifies you're dealing with a single element and is more common:
Could thus be Regarding semantics, I think cart's point is important here:
I've only started to use Bevy recently, but it seems to me that its components are specifically designed and optimized for being used across a large number of entities, and there are already different approaches for the problems stated here:
|
I'm alright with that. The idea of "there should only be one" is conveyed a bit more clearly with "unique" over "single" however.
This doesn't work for complex singletons whose fields you want to be able to mutate in parallel. It also fails badly when you have common behavior that you would like to share with other entities. This is a common pattern for really simple games like roguelikes and platformers, and I'd hate to either push people into making their singleton players resources, iterating over a query that they know is always length one or rolling their own version of this constantly. Re the paradigm shift discussed by @cart: pushing people towards using complex resources in Bevy is commonly a trap: it strips away a huge amount of the modular, expressive power of the ECS and tends to block parallelism. Singleton entities are superior to resources for everything that isn't a simple natural monolith.
FWIW, you'll almost never want to use
With the addition of sparse set components, marker components will become even faster, and I would expect to benchmark much better than the Resource approach, in addition to having better conceptual clarity. |
Yes, i am strongly in favor of encouraging "singleton entities" over resources, because they get many advantages due to being made of components. We should not encourage the liberal use of resources. For example, a game might only have one player. But some components of that player (like health) might be common with other entities. It's nice to have systems that operate on all those entities (say, health system), while also having systems that operate specifically on the player. This is the idiomatic and preferable way to do things as it is, regardless of whether this new API is merged or not. The big selling point of this new API is that it makes these use cases safer. It gives you a way to actually be explicit about your intentions/invariants and enforce them. Because there are many use cases in typical games where you'd only have one of a particular entity. It doesn't make sense to think of "but i want to leave the possibility of there being more than one player at some point", if you know very well that that will never be the case in your game. You are probably designing many things around that assumption. Overall, i am strongly in favor of this new API. It helps make things clear and explicit, and gives extra safety for a common pattern (that is currently only able to be expressed implicitly). EDIT: as for the concern about the kinds of patterns/mindsets that new APIs would encourage for new users learning bevy, this is something to be addressed with proper teaching and documentation. We shouldn't leave out useful APIs because they might be confusing to newcomers. |
@alice-i-cecile @jamadazi Thanks a lot for the clarification! That sounds like From an API design perspective and Bevy's aspiration to be simple, I believe it would make sense if there are some clear guidelines on when to use which approach ( |
Yes, as I was saying, it is a matter of teaching. We can teach users to have the proper understanding. The API is useful and idiomatic IMO, so it should be accepted. Even with the concern that it might make things slightly more confusing to beginners. We already have people (notably alice and myself) who are actively teaching newcomers good practices and mentality around resources vs entities, and other ECS patterns. We'll just adapt to this. |
I think if this gets adapted to the latest changes on main, I think this is good to merge. |
Cool to see this moving ahead! 🙂 Maybe concerning the name of the methods, just so this isn't overlooked:
If the consensus is that "unique" is a better term than "single", that's OK! Regarding semantics, is there a guideline/rule of thumb when to use resources vs. this pattern? |
crates/bevy_ecs/src/system/query.rs
Outdated
@@ -212,6 +212,42 @@ where | |||
Err(QueryComponentError::MissingWriteAccess) | |||
} | |||
} | |||
|
|||
pub fn get_unique(&self) -> Result<<Q::Fetch as Fetch<'_>>::Item, UniqueQueryError<'_, Q>> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You may want to box the error to shrink the return type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How would that help?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could improve performance if the smaller return type fits into two registers by avoiding stack accesses.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, I see... Personally I don't think this would change the performance much, but I don't have a way to benchmark that. This seems quite micro-optimization-y, so I'd rather wait on this until we have realistic uses of this function we can benchmark.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we don't need to return Item
at all in the Error? The query isn't consumed by this operation, so users that want the other values in the event of an error can always iterate over them. Generally when someone wants a "single" item and there is more than one, there is nothing particularly special about the first item.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should resolve this conversation before merging. My preference is to remove "result: Item" from Error. What do you think @TheRawMeatball ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, while I prefer it as-is I suppose the use case is very slim and I'm willing to compromise to get the ball rolling. I'll update the PR to remove this now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks!
Ah yeah sorry for not commenting here (overlooked is the right word). I don't have strong opinions here. My vote is for
This is still TBD, but i think its fair to say that Resources are for "unique" types that do not have an "identity" other than their type and should not be "correlated" to other values/types. They are "singletons" for things like Asset collections, Time, etc.
In my mind nothing has really changed here in the Entity vs Resource relationship other than the fact that you can now add a "single" assertion to entity access. |
FWIW, I like |
My justification for the "inconsistency" is that |
Could |
This makes sense. |
Thanks for all the feedback! 🙂 I tend to agree with cart that it's a special method very similar to
|
I really don't like
i like the |
@cart, there is a lot of opinions here. If you can make a final call, I'd like to finalize this PR. |
I still prefer |
#[derive(Error)] | ||
pub enum QuerySingleError<'a, Q: WorldQuery> { | ||
#[error("No entities fit the query {0}")] | ||
NoEntities(&'static str), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it make sense to have the same layout for NoEntities
as for MultipleEntities
,
i.e NoEntities { query_name: &'static str }
?
This removes the slightly strange asymmetry and would allow to extend the error information in the future.
bors r+ |
Adds `get_unique` and `get_unique_mut` to extend the query api and cover a common use case. Also establishes a second impl block where non-core APIs that don't access the internal fields of queries can live.
Pull request successfully merged into main. Build succeeded: |
@cart I found the ecosystem equivalent of this method: exactly_one. Might be worth renaming and unifying behavior? |
Is this used outside of
|
Adds
get_unique
andget_unique_mut
to extend the query api and cover a common use case. Also establishes a second impl block where non-core APIs that don't access the internal fields of queries can live.