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

[Merged by Bors] - Query::get_unique #1263

Closed
wants to merge 17 commits into from
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions crates/bevy_ecs/src/system/query/extensions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
use std::fmt::Debug;

use thiserror::Error;

use crate::{Fetch, Query, QueryFilter, ReadOnlyFetch, WorldQuery};

impl<'a, Q: WorldQuery, F: QueryFilter> Query<'a, Q, F> {
/// Takes exactly one result from the query. If there are no results, or more than 1 result, this will return an error instead.
pub fn get_unique(&self) -> Result<<Q::Fetch as Fetch<'_>>::Item, UniqueQueryError<'_, Q>>
where
Q::Fetch: ReadOnlyFetch,
{
let mut query = self.iter();
let first = query.next();
let extra = query.next().is_some();

match (first, extra) {
(Some(r), false) => Ok(r),
(None, _) => Err(UniqueQueryError::NoEntities(std::any::type_name::<Self>())),
(Some(r), _) => Err(UniqueQueryError::MultipleEntities {
result: r,
query_name: std::any::type_name::<Self>(),
}),
}
}

/// See [`Query::get_unique`]
pub fn get_unique_mut(
&mut self,
) -> Result<<Q::Fetch as Fetch<'_>>::Item, UniqueQueryError<'_, Q>> {
let mut query = self.iter_mut();
let first = query.next();
let extra = query.next().is_some();

match (first, extra) {
(Some(r), false) => Ok(r),
(None, _) => Err(UniqueQueryError::NoEntities(std::any::type_name::<Self>())),
(Some(r), _) => Err(UniqueQueryError::MultipleEntities {
result: r,
query_name: std::any::type_name::<Self>(),
}),
}
}
}

#[derive(Error)]
pub enum UniqueQueryError<'a, Q: WorldQuery> {
#[error("No entities fit the query {0}")]
NoEntities(&'static str),
#[error("Multiple entities fit the query {query_name}!")]
MultipleEntities {
result: <Q::Fetch as Fetch<'a>>::Item,
query_name: &'static str,
},
}

impl<'a, Q: WorldQuery> Debug for UniqueQueryError<'a, Q> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NoEntities(_) => f.debug_tuple("NoEntities").finish(),
Self::MultipleEntities { .. } => f.debug_tuple("MultipleEntities").finish(),
}
}
}
1 change: 1 addition & 0 deletions crates/bevy_ecs/src/system/query/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod extensions;
mod query_set;
pub use query_set::*;

Expand Down
10 changes: 4 additions & 6 deletions examples/game/breakout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ fn paddle_movement_system(
keyboard_input: Res<Input<KeyCode>>,
mut query: Query<(&Paddle, &mut Transform)>,
) {
for (paddle, mut transform) in query.iter_mut() {
if let Ok((paddle, mut transform)) = query.get_unique_mut() {
let mut direction = 0.0;
if keyboard_input.pressed(KeyCode::Left) {
direction -= 1.0;
Expand All @@ -184,15 +184,13 @@ fn ball_movement_system(time: Res<Time>, mut ball_query: Query<(&Ball, &mut Tran
// clamp the timestep to stop the ball from escaping when the game starts
let delta_seconds = f32::min(0.2, time.delta_seconds());

for (ball, mut transform) in ball_query.iter_mut() {
if let Ok((ball, mut transform)) = ball_query.get_unique_mut() {
transform.translation += ball.velocity * delta_seconds;
}
}

fn scoreboard_system(scoreboard: Res<Scoreboard>, mut query: Query<&mut Text>) {
for mut text in query.iter_mut() {
text.value = format!("Score: {}", scoreboard.score);
}
query.get_unique_mut().unwrap().value = format!("Score: {}", scoreboard.score)
Copy link
Member

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

}

fn ball_collision_system(
Expand All @@ -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() {
Copy link
Member

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.

Copy link
Member

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.

Copy link
Member

@DJMcNab DJMcNab Jan 24, 2021

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.

Copy link
Member

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.

let ball_size = sprite.size;
let velocity = &mut ball.velocity;

Expand Down