Break out Breakout components into a more sensible organization (#4261)

# Objective

- The components in the Breakout game are defined in a strange fashion.
   - Components should decouple behavior wherever possible.
   - Systems should be as general as possible, to make extending behavior easier.
   - Marker components are idiomatic and useful, but marker components and query filters were not used.
- The existing design makes it challenging for beginners to extend the example into a high-quality game.

## Solution

- Refactor component definitions in the Breakout example to reflect principles above.

## Context

A small portion of the changes made in #2094. Interacts with changes in #4255; merge conflicts will have to be resolved.
This commit is contained in:
Alice Cecile 2022-03-23 21:17:50 +00:00
parent 9e450f2827
commit d51b54a658

View file

@ -54,7 +54,7 @@ fn main() {
.with_run_criteria(FixedTimestep::step(TIME_STEP as f64))
.with_system(paddle_movement_system)
.with_system(ball_collision_system)
.with_system(ball_movement_system),
.with_system(apply_velocity_system),
)
.add_system(scoreboard_system)
.add_system(bevy::input::system::exit_on_esc_system)
@ -62,22 +62,21 @@ fn main() {
}
#[derive(Component)]
struct Paddle {
speed: f32,
}
struct Paddle;
#[derive(Component)]
struct Ball {
velocity: Vec3,
}
struct Ball;
#[derive(Component)]
enum Collider {
Solid,
Scorable,
Paddle,
}
struct Velocity(Vec2);
#[derive(Component)]
struct Collider;
#[derive(Component)]
struct Brick;
// This resource tracks the game's score
struct Scoreboard {
score: usize,
}
@ -90,7 +89,9 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn_bundle(UiCameraBundle::default());
// paddle
commands
.spawn_bundle(SpriteBundle {
.spawn()
.insert(Paddle)
.insert_bundle(SpriteBundle {
transform: Transform {
translation: Vec3::new(0.0, PADDLE_HEIGHT, 0.0),
scale: PADDLE_SIZE,
@ -102,13 +103,14 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
},
..default()
})
.insert(Paddle {
speed: PADDLE_SPEED,
})
.insert(Collider::Paddle);
.insert(Collider);
// ball
let ball_velocity = INITIAL_BALL_DIRECTION.normalize() * BALL_SPEED;
commands
.spawn_bundle(SpriteBundle {
.spawn()
.insert(Ball)
.insert_bundle(SpriteBundle {
transform: Transform {
scale: BALL_SIZE,
translation: BALL_STARTING_POSITION,
@ -120,10 +122,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
},
..default()
})
.insert(Ball {
// We can create a velocity by multiplying our speed by a normalized direction.
velocity: BALL_SPEED * INITIAL_BALL_DIRECTION.extend(0.0).normalize(),
});
.insert(Velocity(ball_velocity));
// scoreboard
commands.spawn_bundle(TextBundle {
text: Text {
@ -173,7 +172,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
},
..default()
})
.insert(Collider::Solid);
.insert(Collider);
// right
commands
.spawn_bundle(SpriteBundle {
@ -188,7 +187,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
},
..default()
})
.insert(Collider::Solid);
.insert(Collider);
// bottom
commands
.spawn_bundle(SpriteBundle {
@ -203,7 +202,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
},
..default()
})
.insert(Collider::Solid);
.insert(Collider);
// top
commands
.spawn_bundle(SpriteBundle {
@ -218,7 +217,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
},
..default()
})
.insert(Collider::Solid);
.insert(Collider);
// Add bricks
let bricks_width = BRICK_COLUMNS as f32 * (BRICK_SIZE.x + BRICK_SPACING) - BRICK_SPACING;
@ -234,7 +233,9 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
) + bricks_offset;
// brick
commands
.spawn_bundle(SpriteBundle {
.spawn()
.insert(Brick)
.insert_bundle(SpriteBundle {
sprite: Sprite {
color: BRICK_COLOR,
..default()
@ -246,16 +247,16 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
},
..default()
})
.insert(Collider::Scorable);
.insert(Collider);
}
}
}
fn paddle_movement_system(
keyboard_input: Res<Input<KeyCode>>,
mut query: Query<(&Paddle, &mut Transform)>,
mut query: Query<&mut Transform, With<Paddle>>,
) {
let (paddle, mut transform) = query.single_mut();
let mut transform = query.single_mut();
let mut direction = 0.0;
if keyboard_input.pressed(KeyCode::Left) {
direction -= 1.0;
@ -267,14 +268,16 @@ fn paddle_movement_system(
let translation = &mut transform.translation;
// move the paddle horizontally
translation.x += direction * paddle.speed * TIME_STEP;
translation.x += direction * PADDLE_SPEED * TIME_STEP;
// bound the paddle within the walls
translation.x = translation.x.min(PADDLE_BOUNDS).max(-PADDLE_BOUNDS);
}
fn ball_movement_system(mut ball_query: Query<(&Ball, &mut Transform)>) {
let (ball, mut transform) = ball_query.single_mut();
transform.translation += ball.velocity * TIME_STEP;
fn apply_velocity_system(mut query: Query<(&mut Transform, &Velocity)>) {
for (mut transform, velocity) in query.iter_mut() {
transform.translation.x += velocity.0.x * TIME_STEP;
transform.translation.y += velocity.0.y * TIME_STEP;
}
}
fn scoreboard_system(scoreboard: Res<Scoreboard>, mut query: Query<&mut Text>) {
@ -285,15 +288,14 @@ fn scoreboard_system(scoreboard: Res<Scoreboard>, mut query: Query<&mut Text>) {
fn ball_collision_system(
mut commands: Commands,
mut scoreboard: ResMut<Scoreboard>,
mut ball_query: Query<(&mut Ball, &Transform)>,
collider_query: Query<(Entity, &Collider, &Transform)>,
mut ball_query: Query<(&mut Velocity, &Transform), With<Ball>>,
collider_query: Query<(Entity, &Transform, Option<&Brick>), With<Collider>>,
) {
let (mut ball, ball_transform) = ball_query.single_mut();
let (mut ball_velocity, ball_transform) = ball_query.single_mut();
let ball_size = ball_transform.scale.truncate();
let velocity = &mut ball.velocity;
// check collision with walls
for (collider_entity, collider, transform) in collider_query.iter() {
for (collider_entity, transform, maybe_brick) in collider_query.iter() {
let collision = collide(
ball_transform.translation,
ball_size,
@ -301,8 +303,8 @@ fn ball_collision_system(
transform.scale.truncate(),
);
if let Some(collision) = collision {
// scorable colliders should be despawned and increment the scoreboard on collision
if let Collider::Scorable = *collider {
// Bricks should be despawned and increment the scoreboard on collision
if maybe_brick.is_some() {
scoreboard.score += 1;
commands.entity(collider_entity).despawn();
}
@ -314,27 +316,21 @@ fn ball_collision_system(
// 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,
Collision::Left => reflect_x = ball_velocity.0.x > 0.0,
Collision::Right => reflect_x = ball_velocity.0.x < 0.0,
Collision::Top => reflect_y = ball_velocity.0.y < 0.0,
Collision::Bottom => reflect_y = ball_velocity.0.y > 0.0,
Collision::Inside => { /* do nothing */ }
}
// reflect velocity on the x-axis if we hit something on the x-axis
if reflect_x {
velocity.x = -velocity.x;
ball_velocity.0.x = -ball_velocity.0.x;
}
// 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;
ball_velocity.0.y = -ball_velocity.0.y;
}
}
}