Skip to content

Commit

Permalink
Break out Breakout components into a more sensible organization (#4261)
Browse files Browse the repository at this point in the history
# 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.
  • Loading branch information
alice-i-cecile committed Mar 23, 2022
1 parent 9e450f2 commit d51b54a
Showing 1 changed file with 49 additions and 53 deletions.
102 changes: 49 additions & 53 deletions examples/game/breakout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,30 +54,29 @@ 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)
.run();
}

#[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,
}
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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 {
Expand Down Expand Up @@ -173,7 +172,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
},
..default()
})
.insert(Collider::Solid);
.insert(Collider);
// right
commands
.spawn_bundle(SpriteBundle {
Expand All @@ -188,7 +187,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
},
..default()
})
.insert(Collider::Solid);
.insert(Collider);
// bottom
commands
.spawn_bundle(SpriteBundle {
Expand All @@ -203,7 +202,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
},
..default()
})
.insert(Collider::Solid);
.insert(Collider);
// top
commands
.spawn_bundle(SpriteBundle {
Expand All @@ -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;
Expand All @@ -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()
Expand All @@ -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;
Expand All @@ -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>) {
Expand All @@ -285,24 +288,23 @@ 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,
transform.translation,
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();
}
Expand All @@ -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;
}
}
}
Expand Down

0 comments on commit d51b54a

Please sign in to comment.