Skip to content

Commit 8570b65

Browse files
Clean up Breakout logic (#4311)
# Objective 1. Spawning walls in the Breakout example was hard to follow and error-prone. 2. The strategy used in `paddle_movement_system` was somewhat convoluted. 3. Correctly modifying the size of the arena was hard, due to implicit coupling between the bounds and the bounds that the paddle can move in. ## Solution 1. Refactor this to use a WallBundle struct with a builder; neatly demonstrating some essential patterns along the way. 2. Use clamp and avoid using weird &mut strategies. 3. Refactor logic to allow users to tweak the brick size, and automatically adjust the number of rows and columns to match. 4. Make the brick layout more like classic breakout! ![image](https://user-images.githubusercontent.com/3579909/160019864-06747361-3b5b-4944-b3fd-4978604e2ef5.png)
1 parent a190cd5 commit 8570b65

File tree

1 file changed

+159
-100
lines changed

1 file changed

+159
-100
lines changed

examples/game/breakout.rs

Lines changed: 159 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -13,30 +13,39 @@ const TIME_STEP: f32 = 1.0 / 60.0;
1313
// These constants are defined in `Transform` units.
1414
// Using the default 2D camera they correspond 1:1 with screen pixels.
1515
// The `const_vec3!` macros are needed as functions that operate on floats cannot be constant in Rust.
16-
const PADDLE_HEIGHT: f32 = -215.0;
17-
const PADDLE_SIZE: Vec3 = const_vec3!([120.0, 30.0, 0.0]);
16+
const PADDLE_SIZE: Vec3 = const_vec3!([120.0, 20.0, 0.0]);
17+
const GAP_BETWEEN_PADDLE_AND_FLOOR: f32 = 60.0;
1818
const PADDLE_SPEED: f32 = 500.0;
19-
const PADDLE_BOUNDS: f32 = 380.0;
19+
// How close can the paddle get to the wall
20+
const PADDLE_PADDING: f32 = 10.0;
2021

2122
// We set the z-value of the ball to 1 so it renders on top in the case of overlapping sprites.
2223
const BALL_STARTING_POSITION: Vec3 = const_vec3!([0.0, -50.0, 1.0]);
2324
const BALL_SIZE: Vec3 = const_vec3!([30.0, 30.0, 0.0]);
2425
const BALL_SPEED: f32 = 400.0;
2526
const INITIAL_BALL_DIRECTION: Vec2 = const_vec2!([0.5, -0.5]);
2627

27-
const PLAY_AREA_BOUNDS: Vec2 = const_vec2!([900.0, 600.0]);
2828
const WALL_THICKNESS: f32 = 10.0;
29+
// x coordinates
30+
const LEFT_WALL: f32 = -450.;
31+
const RIGHT_WALL: f32 = 450.;
32+
// y coordinates
33+
const BOTTOM_WALL: f32 = -300.;
34+
const TOP_WALL: f32 = 300.;
2935

30-
const BRICK_ROWS: u8 = 4;
31-
const BRICK_COLUMNS: u8 = 5;
32-
const BRICK_SPACING: f32 = 20.0;
33-
const BRICK_SIZE: Vec3 = const_vec3!([150.0, 30.0, 1.0]);
36+
const BRICK_SIZE: Vec2 = const_vec2!([100., 30.]);
37+
// These values are exact
38+
const GAP_BETWEEN_PADDLE_AND_BRICKS: f32 = 270.0;
39+
const GAP_BETWEEN_BRICKS: f32 = 5.0;
40+
// These values are lower bounds, as the number of bricks is computed
41+
const GAP_BETWEEN_BRICKS_AND_CEILING: f32 = 20.0;
42+
const GAP_BETWEEN_BRICKS_AND_SIDES: f32 = 20.0;
3443

3544
const SCOREBOARD_FONT_SIZE: f32 = 40.0;
3645
const SCOREBOARD_TEXT_PADDING: Val = Val::Px(5.0);
3746

3847
const BACKGROUND_COLOR: Color = Color::rgb(0.9, 0.9, 0.9);
39-
const PADDLE_COLOR: Color = Color::rgb(0.5, 0.5, 1.0);
48+
const PADDLE_COLOR: Color = Color::rgb(0.3, 0.3, 0.7);
4049
const BALL_COLOR: Color = Color::rgb(1.0, 0.5, 0.5);
4150
const BRICK_COLOR: Color = Color::rgb(0.5, 0.5, 1.0);
4251
const WALL_COLOR: Color = Color::rgb(0.8, 0.8, 0.8);
@@ -52,9 +61,9 @@ fn main() {
5261
.add_system_set(
5362
SystemSet::new()
5463
.with_run_criteria(FixedTimestep::step(TIME_STEP as f64))
55-
.with_system(move_paddle)
5664
.with_system(check_for_collisions)
57-
.with_system(apply_velocity),
65+
.with_system(move_paddle.before(check_for_collisions))
66+
.with_system(apply_velocity.before(check_for_collisions)),
5867
)
5968
.add_system(update_scoreboard)
6069
.add_system(bevy::input::system::exit_on_esc_system)
@@ -76,24 +85,97 @@ struct Collider;
7685
#[derive(Component)]
7786
struct Brick;
7887

88+
// This bundle is a collection of the components that define a "wall" in our game
89+
#[derive(Bundle)]
90+
struct WallBundle {
91+
// You can nest bundles inside of other bundles like this
92+
// Allowing you to compose their functionality
93+
#[bundle]
94+
sprite_bundle: SpriteBundle,
95+
collider: Collider,
96+
}
97+
98+
/// Which side of the arena is this wall located on?
99+
enum WallLocation {
100+
Left,
101+
Right,
102+
Bottom,
103+
Top,
104+
}
105+
106+
impl WallLocation {
107+
fn position(&self) -> Vec2 {
108+
match self {
109+
WallLocation::Left => Vec2::new(LEFT_WALL, 0.),
110+
WallLocation::Right => Vec2::new(RIGHT_WALL, 0.),
111+
WallLocation::Bottom => Vec2::new(0., BOTTOM_WALL),
112+
WallLocation::Top => Vec2::new(0., TOP_WALL),
113+
}
114+
}
115+
116+
fn size(&self) -> Vec2 {
117+
let arena_height = TOP_WALL - BOTTOM_WALL;
118+
let arena_width = RIGHT_WALL - LEFT_WALL;
119+
// Make sure we haven't messed up our constants
120+
assert!(arena_height > 0.0);
121+
assert!(arena_width > 0.0);
122+
123+
match self {
124+
WallLocation::Left => Vec2::new(WALL_THICKNESS, arena_height + WALL_THICKNESS),
125+
WallLocation::Right => Vec2::new(WALL_THICKNESS, arena_height + WALL_THICKNESS),
126+
WallLocation::Bottom => Vec2::new(arena_width + WALL_THICKNESS, WALL_THICKNESS),
127+
WallLocation::Top => Vec2::new(arena_width + WALL_THICKNESS, WALL_THICKNESS),
128+
}
129+
}
130+
}
131+
132+
impl WallBundle {
133+
// This "builder method" allows us to reuse logic across our wall entities,
134+
// making our code easier to read and less prone to bugs when we change the logic
135+
fn new(location: WallLocation) -> WallBundle {
136+
WallBundle {
137+
sprite_bundle: SpriteBundle {
138+
transform: Transform {
139+
// We need to convert our Vec2 into a Vec3, by giving it a z-coordinate
140+
// This is used to determine the order of our sprites
141+
translation: location.position().extend(0.0),
142+
// The z-scale of 2D objects must always be 1.0,
143+
// or their ordering will be affected in surprising ways.
144+
// See https://github.com/bevyengine/bevy/issues/4149
145+
scale: location.size().extend(1.0),
146+
..default()
147+
},
148+
sprite: Sprite {
149+
color: WALL_COLOR,
150+
..default()
151+
},
152+
..default()
153+
},
154+
collider: Collider,
155+
}
156+
}
157+
}
158+
79159
// This resource tracks the game's score
80160
struct Scoreboard {
81161
score: usize,
82162
}
83163

164+
// Add the game's entities to our world
84165
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
85-
// Add the game's entities to our world
86-
87-
// cameras
166+
// Cameras
88167
commands.spawn_bundle(OrthographicCameraBundle::new_2d());
89168
commands.spawn_bundle(UiCameraBundle::default());
90-
// paddle
169+
170+
// Paddle
171+
let paddle_y = BOTTOM_WALL + GAP_BETWEEN_PADDLE_AND_FLOOR;
172+
91173
commands
92174
.spawn()
93175
.insert(Paddle)
94176
.insert_bundle(SpriteBundle {
95177
transform: Transform {
96-
translation: Vec3::new(0.0, PADDLE_HEIGHT, 0.0),
178+
translation: Vec3::new(0.0, paddle_y, 0.0),
97179
scale: PADDLE_SIZE,
98180
..default()
99181
},
@@ -104,9 +186,8 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
104186
..default()
105187
})
106188
.insert(Collider);
107-
// ball
108-
let ball_velocity = INITIAL_BALL_DIRECTION.normalize() * BALL_SPEED;
109189

190+
// Ball
110191
commands
111192
.spawn()
112193
.insert(Ball)
@@ -122,8 +203,9 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
122203
},
123204
..default()
124205
})
125-
.insert(Velocity(ball_velocity));
126-
// scoreboard
206+
.insert(Velocity(INITIAL_BALL_DIRECTION.normalize() * BALL_SPEED));
207+
208+
// Scoreboard
127209
commands.spawn_bundle(TextBundle {
128210
text: Text {
129211
sections: vec![
@@ -158,79 +240,51 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
158240
..default()
159241
});
160242

161-
// left
162-
commands
163-
.spawn_bundle(SpriteBundle {
164-
transform: Transform {
165-
translation: Vec3::new(-PLAY_AREA_BOUNDS.x / 2.0, 0.0, 0.0),
166-
scale: Vec3::new(WALL_THICKNESS, PLAY_AREA_BOUNDS.y + WALL_THICKNESS, 1.0),
167-
..default()
168-
},
169-
sprite: Sprite {
170-
color: WALL_COLOR,
171-
..default()
172-
},
173-
..default()
174-
})
175-
.insert(Collider);
176-
// right
177-
commands
178-
.spawn_bundle(SpriteBundle {
179-
transform: Transform {
180-
translation: Vec3::new(PLAY_AREA_BOUNDS.x / 2.0, 0.0, 0.0),
181-
scale: Vec3::new(WALL_THICKNESS, PLAY_AREA_BOUNDS.y + WALL_THICKNESS, 1.0),
182-
..default()
183-
},
184-
sprite: Sprite {
185-
color: WALL_COLOR,
186-
..default()
187-
},
188-
..default()
189-
})
190-
.insert(Collider);
191-
// bottom
192-
commands
193-
.spawn_bundle(SpriteBundle {
194-
transform: Transform {
195-
translation: Vec3::new(0.0, -PLAY_AREA_BOUNDS.y / 2.0, 0.0),
196-
scale: Vec3::new(PLAY_AREA_BOUNDS.x + WALL_THICKNESS, WALL_THICKNESS, 1.0),
197-
..default()
198-
},
199-
sprite: Sprite {
200-
color: WALL_COLOR,
201-
..default()
202-
},
203-
..default()
204-
})
205-
.insert(Collider);
206-
// top
207-
commands
208-
.spawn_bundle(SpriteBundle {
209-
transform: Transform {
210-
translation: Vec3::new(0.0, PLAY_AREA_BOUNDS.y / 2.0, 0.0),
211-
scale: Vec3::new(PLAY_AREA_BOUNDS.x + WALL_THICKNESS, WALL_THICKNESS, 1.0),
212-
..default()
213-
},
214-
sprite: Sprite {
215-
color: WALL_COLOR,
216-
..default()
217-
},
218-
..default()
219-
})
220-
.insert(Collider);
243+
// Walls
244+
commands.spawn_bundle(WallBundle::new(WallLocation::Left));
245+
commands.spawn_bundle(WallBundle::new(WallLocation::Right));
246+
commands.spawn_bundle(WallBundle::new(WallLocation::Bottom));
247+
commands.spawn_bundle(WallBundle::new(WallLocation::Top));
248+
249+
// Bricks
250+
// Negative scales result in flipped sprites / meshes,
251+
// which is definitely not what we want here
252+
assert!(BRICK_SIZE.x > 0.0);
253+
assert!(BRICK_SIZE.y > 0.0);
254+
255+
let total_width_of_bricks = (RIGHT_WALL - LEFT_WALL) - 2. * GAP_BETWEEN_BRICKS_AND_SIDES;
256+
let bottom_edge_of_bricks = paddle_y + GAP_BETWEEN_PADDLE_AND_BRICKS;
257+
let total_height_of_bricks = TOP_WALL - bottom_edge_of_bricks - GAP_BETWEEN_BRICKS_AND_CEILING;
258+
259+
assert!(total_width_of_bricks > 0.0);
260+
assert!(total_height_of_bricks > 0.0);
261+
262+
// Given the space available, compute how many rows and columns of bricks we can fit
263+
let n_columns = (total_width_of_bricks / (BRICK_SIZE.x + GAP_BETWEEN_BRICKS)).floor() as usize;
264+
let n_rows = (total_height_of_bricks / (BRICK_SIZE.y + GAP_BETWEEN_BRICKS)).floor() as usize;
265+
let n_vertical_gaps = n_columns - 1;
266+
267+
// Because we need to round the number of columns,
268+
// the space on the top and sides of the bricks only captures a lower bound, not an exact value
269+
let center_of_bricks = (LEFT_WALL + RIGHT_WALL) / 2.0;
270+
let left_edge_of_bricks = center_of_bricks
271+
// Space taken up by the bricks
272+
- (n_columns as f32 / 2.0 * BRICK_SIZE.x)
273+
// Space taken up by the gaps
274+
- n_vertical_gaps as f32 / 2.0 * GAP_BETWEEN_BRICKS;
275+
276+
// In Bevy, the `translation` of an entity describes the center point,
277+
// not its bottom-left corner
278+
let offset_x = left_edge_of_bricks + BRICK_SIZE.x / 2.;
279+
let offset_y = bottom_edge_of_bricks + BRICK_SIZE.y / 2.;
280+
281+
for row in 0..n_rows {
282+
for column in 0..n_columns {
283+
let brick_position = Vec2::new(
284+
offset_x + column as f32 * (BRICK_SIZE.x + GAP_BETWEEN_BRICKS),
285+
offset_y + row as f32 * (BRICK_SIZE.y + GAP_BETWEEN_BRICKS),
286+
);
221287

222-
// Add bricks
223-
let bricks_width = BRICK_COLUMNS as f32 * (BRICK_SIZE.x + BRICK_SPACING) - BRICK_SPACING;
224-
// center the bricks and move them up a bit
225-
let bricks_offset = Vec3::new(-(bricks_width - BRICK_SIZE.x) / 2.0, 100.0, 0.0);
226-
for row in 0..BRICK_ROWS {
227-
let y_position = row as f32 * (BRICK_SIZE.y + BRICK_SPACING);
228-
for column in 0..BRICK_COLUMNS {
229-
let brick_position = Vec3::new(
230-
column as f32 * (BRICK_SIZE.x + BRICK_SPACING),
231-
y_position,
232-
0.0,
233-
) + bricks_offset;
234288
// brick
235289
commands
236290
.spawn()
@@ -241,8 +295,8 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
241295
..default()
242296
},
243297
transform: Transform {
244-
translation: brick_position,
245-
scale: BRICK_SIZE,
298+
translation: brick_position.extend(0.0),
299+
scale: Vec3::new(BRICK_SIZE.x, BRICK_SIZE.y, 1.0),
246300
..default()
247301
},
248302
..default()
@@ -256,8 +310,9 @@ fn move_paddle(
256310
keyboard_input: Res<Input<KeyCode>>,
257311
mut query: Query<&mut Transform, With<Paddle>>,
258312
) {
259-
let mut transform = query.single_mut();
313+
let mut paddle_transform = query.single_mut();
260314
let mut direction = 0.0;
315+
261316
if keyboard_input.pressed(KeyCode::Left) {
262317
direction -= 1.0;
263318
}
@@ -266,11 +321,15 @@ fn move_paddle(
266321
direction += 1.0;
267322
}
268323

269-
let translation = &mut transform.translation;
270-
// move the paddle horizontally
271-
translation.x += direction * PADDLE_SPEED * TIME_STEP;
272-
// bound the paddle within the walls
273-
translation.x = translation.x.min(PADDLE_BOUNDS).max(-PADDLE_BOUNDS);
324+
// Calculate the new horizontal paddle position based on player input
325+
let new_paddle_position = paddle_transform.translation.x + direction * PADDLE_SPEED * TIME_STEP;
326+
327+
// Update the paddle position,
328+
// making sure it doesn't cause the paddle to leave the arena
329+
let left_bound = LEFT_WALL + WALL_THICKNESS / 2.0 + PADDLE_SIZE.x / 2.0 + PADDLE_PADDING;
330+
let right_bound = RIGHT_WALL - WALL_THICKNESS / 2.0 - PADDLE_SIZE.x / 2.0 - PADDLE_PADDING;
331+
332+
paddle_transform.translation.x = new_paddle_position.clamp(left_bound, right_bound);
274333
}
275334

276335
fn apply_velocity(mut query: Query<(&mut Transform, &Velocity)>) {

0 commit comments

Comments
 (0)