add get_single variant (#2793)

# Objective

The vast majority of `.single()` usage I've seen is immediately followed by a `.unwrap()`. Since it seems most people use it without handling the error, I think making it easier to just get what you want fast while also having a more verbose alternative when you want to handle the error could help.

## Solution

Instead of having a lot of `.unwrap()` everywhere, this PR introduces a `try_single()` variant that behaves like the current `.single()` and make the new `.single()` panic on error.
This commit is contained in:
Charles Giguere 2021-09-10 20:23:50 +00:00
parent 5ff96b8e7e
commit 51a5070cd2
6 changed files with 115 additions and 81 deletions

View file

@ -574,7 +574,7 @@ mod tests {
let (a, query, _) = system_state.get(&world); let (a, query, _) = system_state.get(&world);
assert_eq!(*a, A(42), "returned resource matches initial value"); assert_eq!(*a, A(42), "returned resource matches initial value");
assert_eq!( assert_eq!(
*query.single().unwrap(), *query.single(),
B(7), B(7),
"returned component matches initial value" "returned component matches initial value"
); );
@ -601,7 +601,7 @@ mod tests {
let (a, mut query) = system_state.get_mut(&mut world); let (a, mut query) = system_state.get_mut(&mut world);
assert_eq!(*a, A(42), "returned resource matches initial value"); assert_eq!(*a, A(42), "returned resource matches initial value");
assert_eq!( assert_eq!(
*query.single_mut().unwrap(), *query.single_mut(),
B(7), B(7),
"returned component matches initial value" "returned component matches initial value"
); );
@ -618,18 +618,18 @@ mod tests {
let mut system_state: SystemState<Query<&A, Changed<A>>> = SystemState::new(&mut world); let mut system_state: SystemState<Query<&A, Changed<A>>> = SystemState::new(&mut world);
{ {
let query = system_state.get(&world); let query = system_state.get(&world);
assert_eq!(*query.single().unwrap(), A(1)); assert_eq!(*query.single(), A(1));
} }
{ {
let query = system_state.get(&world); let query = system_state.get(&world);
assert!(query.single().is_err()); assert!(query.get_single().is_err());
} }
world.entity_mut(entity).get_mut::<A>().unwrap().0 = 2; world.entity_mut(entity).get_mut::<A>().unwrap().0 = 2;
{ {
let query = system_state.get(&world); let query = system_state.get(&world);
assert_eq!(*query.single().unwrap(), A(2)); assert_eq!(*query.single(), A(2));
} }
} }

View file

@ -488,6 +488,33 @@ where
} }
} }
/// Gets the result of a single-result query.
///
/// Assumes this query has only one result and panics if there are no or multiple results.
/// Use [`Self::get_single`] to handle the error cases explicitly
///
/// # Example
///
/// ```
/// # use bevy_ecs::prelude::{IntoSystem, Query, With};
/// struct Player;
/// struct Position(f32, f32);
/// fn player_system(query: Query<&Position, With<Player>>) {
/// let player_position = query.single();
/// // do something with player_position
/// }
/// # let _check_that_its_a_system = player_system.system();
/// ```
///
/// This can only be called for read-only queries, see [`Self::single_mut`] for write-queries.
#[track_caller]
pub fn single(&'s self) -> <Q::Fetch as Fetch<'w, 's>>::Item
where
Q::Fetch: ReadOnlyFetch,
{
self.get_single().unwrap()
}
/// Gets the result of a single-result query. /// Gets the result of a single-result query.
/// ///
/// If the query has exactly one result, returns the result inside `Ok` /// If the query has exactly one result, returns the result inside `Ok`
@ -497,27 +524,28 @@ where
/// # Examples /// # Examples
/// ///
/// ``` /// ```
/// # use bevy_ecs::prelude::{IntoSystem, With};
/// # use bevy_ecs::system::{Query, QuerySingleError}; /// # use bevy_ecs::system::{Query, QuerySingleError};
/// # use bevy_ecs::prelude::IntoSystem; /// struct Player;
/// struct PlayerScore(i32); /// struct Position(f32, f32);
/// fn player_scoring_system(query: Query<&PlayerScore>) { /// fn player_system(query: Query<&Position, With<Player>>) {
/// match query.single() { /// match query.get_single() {
/// Ok(PlayerScore(score)) => { /// Ok(position) => {
/// // do something with score /// // do something with position
/// } /// }
/// Err(QuerySingleError::NoEntities(_)) => { /// Err(QuerySingleError::NoEntities(_)) => {
/// // no PlayerScore /// // no position with Player
/// } /// }
/// Err(QuerySingleError::MultipleEntities(_)) => { /// Err(QuerySingleError::MultipleEntities(_)) => {
/// // multiple PlayerScore /// // multiple position with Player
/// } /// }
/// } /// }
/// } /// }
/// # let _check_that_its_a_system = player_scoring_system.system(); /// # let _check_that_its_a_system = player_system.system();
/// ``` /// ```
/// ///
/// This can only be called for read-only queries, see [`Self::single_mut`] for write-queries. /// This can only be called for read-only queries, see [`Self::get_single_mut`] for write-queries.
pub fn single(&'s self) -> Result<<Q::Fetch as Fetch<'w, 's>>::Item, QuerySingleError> pub fn get_single(&'s self) -> Result<<Q::Fetch as Fetch<'w, 's>>::Item, QuerySingleError>
where where
Q::Fetch: ReadOnlyFetch, Q::Fetch: ReadOnlyFetch,
{ {
@ -534,9 +562,18 @@ where
} }
} }
/// Gets the query result if it is only a single result, otherwise panics
/// If you want to handle the error case yourself you can use the [`Self::get_single_mut`] variant.
#[track_caller]
pub fn single_mut(&mut self) -> <Q::Fetch as Fetch<'_, '_>>::Item {
self.get_single_mut().unwrap()
}
/// Gets the query result if it is only a single result, otherwise returns a /// Gets the query result if it is only a single result, otherwise returns a
/// [`QuerySingleError`]. /// [`QuerySingleError`].
pub fn single_mut(&mut self) -> Result<<Q::Fetch as Fetch<'_, '_>>::Item, QuerySingleError> { pub fn get_single_mut(
&mut self,
) -> Result<<Q::Fetch as Fetch<'_, '_>>::Item, QuerySingleError> {
let mut query = self.iter_mut(); let mut query = self.iter_mut();
let first = query.next(); let first = query.next();
let extra = query.next().is_some(); let extra = query.next().is_some();

View file

@ -75,7 +75,7 @@ fn setup(
// System for rotating and translating the camera // System for rotating and translating the camera
fn move_camera_system(time: Res<Time>, mut camera_query: Query<&mut Transform, With<Camera>>) { fn move_camera_system(time: Res<Time>, mut camera_query: Query<&mut Transform, With<Camera>>) {
let mut camera_transform = camera_query.single_mut().unwrap(); let mut camera_transform = camera_query.single_mut();
camera_transform.rotate(Quat::from_rotation_z(time.delta_seconds() * 0.5)); camera_transform.rotate(Quat::from_rotation_z(time.delta_seconds() * 0.5));
*camera_transform = *camera_transform *camera_transform = *camera_transform
* Transform::from_translation(Vec3::X * CAMERA_SPEED * time.delta_seconds()); * Transform::from_translation(Vec3::X * CAMERA_SPEED * time.delta_seconds());
@ -83,7 +83,7 @@ fn move_camera_system(time: Res<Time>, mut camera_query: Query<&mut Transform, W
// System for printing the number of sprites on every tick of the timer // System for printing the number of sprites on every tick of the timer
fn tick_system(time: Res<Time>, sprites_query: Query<&Sprite>, mut timer_query: Query<&mut Timer>) { fn tick_system(time: Res<Time>, sprites_query: Query<&Sprite>, mut timer_query: Query<&mut Timer>) {
let mut timer = timer_query.single_mut().unwrap(); let mut timer = timer_query.single_mut();
timer.tick(time.delta()); timer.tick(time.delta());
if timer.just_finished() { if timer.just_finished() {

View file

@ -352,7 +352,7 @@ fn rotate_bonus(game: Res<Game>, time: Res<Time>, mut transforms: Query<&mut Tra
// update the score displayed during the game // update the score displayed during the game
fn scoreboard_system(game: Res<Game>, mut query: Query<&mut Text>) { fn scoreboard_system(game: Res<Game>, mut query: Query<&mut Text>) {
let mut text = query.single_mut().unwrap(); let mut text = query.single_mut();
text.sections[0].value = format!("Sugar Rush: {}", game.score); text.sections[0].value = format!("Sugar Rush: {}", game.score);
} }

View file

@ -185,32 +185,30 @@ fn paddle_movement_system(
keyboard_input: Res<Input<KeyCode>>, keyboard_input: Res<Input<KeyCode>>,
mut query: Query<(&Paddle, &mut Transform)>, mut query: Query<(&Paddle, &mut Transform)>,
) { ) {
if let Ok((paddle, mut transform)) = query.single_mut() { let (paddle, mut transform) = query.single_mut();
let mut direction = 0.0; let mut direction = 0.0;
if keyboard_input.pressed(KeyCode::Left) { if keyboard_input.pressed(KeyCode::Left) {
direction -= 1.0; direction -= 1.0;
}
if keyboard_input.pressed(KeyCode::Right) {
direction += 1.0;
}
let translation = &mut transform.translation;
// move the paddle horizontally
translation.x += direction * paddle.speed * TIME_STEP;
// bound the paddle within the walls
translation.x = translation.x.min(380.0).max(-380.0);
} }
if keyboard_input.pressed(KeyCode::Right) {
direction += 1.0;
}
let translation = &mut transform.translation;
// move the paddle horizontally
translation.x += direction * paddle.speed * TIME_STEP;
// bound the paddle within the walls
translation.x = translation.x.min(380.0).max(-380.0);
} }
fn ball_movement_system(mut ball_query: Query<(&Ball, &mut Transform)>) { fn ball_movement_system(mut ball_query: Query<(&Ball, &mut Transform)>) {
if let Ok((ball, mut transform)) = ball_query.single_mut() { let (ball, mut transform) = ball_query.single_mut();
transform.translation += ball.velocity * TIME_STEP; transform.translation += ball.velocity * TIME_STEP;
}
} }
fn scoreboard_system(scoreboard: Res<Scoreboard>, mut query: Query<&mut Text>) { fn scoreboard_system(scoreboard: Res<Scoreboard>, mut query: Query<&mut Text>) {
let mut text = query.single_mut().unwrap(); let mut text = query.single_mut();
text.sections[1].value = format!("{}", scoreboard.score); text.sections[1].value = format!("{}", scoreboard.score);
} }
@ -220,53 +218,52 @@ fn ball_collision_system(
mut ball_query: Query<(&mut Ball, &Transform, &Sprite)>, mut ball_query: Query<(&mut Ball, &Transform, &Sprite)>,
collider_query: Query<(Entity, &Collider, &Transform, &Sprite)>, collider_query: Query<(Entity, &Collider, &Transform, &Sprite)>,
) { ) {
if let Ok((mut ball, ball_transform, sprite)) = ball_query.single_mut() { let (mut ball, ball_transform, sprite) = ball_query.single_mut();
let ball_size = sprite.size; let ball_size = sprite.size;
let velocity = &mut ball.velocity; let velocity = &mut ball.velocity;
// check collision with walls // check collision with walls
for (collider_entity, collider, transform, sprite) in collider_query.iter() { for (collider_entity, collider, transform, sprite) in collider_query.iter() {
let collision = collide( let collision = collide(
ball_transform.translation, ball_transform.translation,
ball_size, ball_size,
transform.translation, transform.translation,
sprite.size, sprite.size,
); );
if let Some(collision) = collision { if let Some(collision) = collision {
// scorable colliders should be despawned and increment the scoreboard on collision // scorable colliders should be despawned and increment the scoreboard on collision
if let Collider::Scorable = *collider { if let Collider::Scorable = *collider {
scoreboard.score += 1; scoreboard.score += 1;
commands.entity(collider_entity).despawn(); commands.entity(collider_entity).despawn();
} }
// reflect the ball when it collides // reflect the ball when it collides
let mut reflect_x = false; let mut reflect_x = false;
let mut reflect_y = false; let mut reflect_y = false;
// only reflect if the ball's velocity is going in the opposite direction of the // only reflect if the ball's velocity is going in the opposite direction of the
// collision // collision
match collision { match collision {
Collision::Left => reflect_x = velocity.x > 0.0, Collision::Left => reflect_x = velocity.x > 0.0,
Collision::Right => reflect_x = velocity.x < 0.0, Collision::Right => reflect_x = velocity.x < 0.0,
Collision::Top => reflect_y = velocity.y < 0.0, Collision::Top => reflect_y = velocity.y < 0.0,
Collision::Bottom => reflect_y = velocity.y > 0.0, Collision::Bottom => reflect_y = velocity.y > 0.0,
} }
// reflect velocity on the x-axis if we hit something on the x-axis // reflect velocity on the x-axis if we hit something on the x-axis
if reflect_x { if reflect_x {
velocity.x = -velocity.x; velocity.x = -velocity.x;
} }
// reflect velocity on the y-axis if we hit something on the y-axis // reflect velocity on the y-axis if we hit something on the y-axis
if reflect_y { if reflect_y {
velocity.y = -velocity.y; velocity.y = -velocity.y;
} }
// break if this collide is on a solid, otherwise continue check whether a solid is // break if this collide is on a solid, otherwise continue check whether a solid is
// also in collision // also in collision
if let Collider::Solid = *collider { if let Collider::Solid = *collider {
break; break;
}
} }
} }
} }

View file

@ -120,6 +120,6 @@ fn setup(
/// `time.seconds_since_startup()` as the `value` of the `TimeComponent`. This value will be /// `time.seconds_since_startup()` as the `value` of the `TimeComponent`. This value will be
/// accessed by the fragment shader and used to animate the shader. /// accessed by the fragment shader and used to animate the shader.
fn animate_shader(time: Res<Time>, mut query: Query<&mut TimeUniform>) { fn animate_shader(time: Res<Time>, mut query: Query<&mut TimeUniform>) {
let mut time_uniform = query.single_mut().unwrap(); let mut time_uniform = query.single_mut();
time_uniform.value = time.seconds_since_startup() as f32; time_uniform.value = time.seconds_since_startup() as f32;
} }