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);
assert_eq!(*a, A(42), "returned resource matches initial value");
assert_eq!(
*query.single().unwrap(),
*query.single(),
B(7),
"returned component matches initial value"
);
@ -601,7 +601,7 @@ mod tests {
let (a, mut query) = system_state.get_mut(&mut world);
assert_eq!(*a, A(42), "returned resource matches initial value");
assert_eq!(
*query.single_mut().unwrap(),
*query.single_mut(),
B(7),
"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 query = system_state.get(&world);
assert_eq!(*query.single().unwrap(), A(1));
assert_eq!(*query.single(), A(1));
}
{
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;
{
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.
///
/// If the query has exactly one result, returns the result inside `Ok`
@ -497,27 +524,28 @@ where
/// # Examples
///
/// ```
/// # use bevy_ecs::prelude::{IntoSystem, With};
/// # use bevy_ecs::system::{Query, QuerySingleError};
/// # use bevy_ecs::prelude::IntoSystem;
/// struct PlayerScore(i32);
/// fn player_scoring_system(query: Query<&PlayerScore>) {
/// match query.single() {
/// Ok(PlayerScore(score)) => {
/// // do something with score
/// struct Player;
/// struct Position(f32, f32);
/// fn player_system(query: Query<&Position, With<Player>>) {
/// match query.get_single() {
/// Ok(position) => {
/// // do something with position
/// }
/// Err(QuerySingleError::NoEntities(_)) => {
/// // no PlayerScore
/// // no position with Player
/// }
/// 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.
pub fn single(&'s self) -> Result<<Q::Fetch as Fetch<'w, 's>>::Item, QuerySingleError>
/// This can only be called for read-only queries, see [`Self::get_single_mut`] for write-queries.
pub fn get_single(&'s self) -> Result<<Q::Fetch as Fetch<'w, 's>>::Item, QuerySingleError>
where
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
/// [`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 first = query.next();
let extra = query.next().is_some();

View file

@ -75,7 +75,7 @@ fn setup(
// System for rotating and translating the 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 = *camera_transform
* 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
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());
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
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);
}

View file

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

View file

@ -120,6 +120,6 @@ fn setup(
/// `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.
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;
}