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