//! A simplified implementation of the classic game "Breakout". //! //! Demonstrates Bevy's stepping capabilities if compiled with the `bevy_debug_stepping` feature. use bevy::{ math::bounding::{Aabb2d, BoundingCircle, BoundingVolume, IntersectsVolume}, prelude::*, }; mod stepping; // These constants are defined in `Transform` units. // Using the default 2D camera they correspond 1:1 with screen pixels. const PADDLE_SIZE: Vec2 = Vec2::new(120.0, 20.0); const GAP_BETWEEN_PADDLE_AND_FLOOR: f32 = 60.0; const PADDLE_SPEED: f32 = 500.0; // How close can the paddle get to the wall const PADDLE_PADDING: f32 = 10.0; // We set the z-value of the ball to 1 so it renders on top in the case of overlapping sprites. const BALL_STARTING_POSITION: Vec3 = Vec3::new(0.0, -50.0, 1.0); const BALL_DIAMETER: f32 = 30.; const BALL_SPEED: f32 = 400.0; const INITIAL_BALL_DIRECTION: Vec2 = Vec2::new(0.5, -0.5); const WALL_THICKNESS: f32 = 10.0; // x coordinates const LEFT_WALL: f32 = -450.; const RIGHT_WALL: f32 = 450.; // y coordinates const BOTTOM_WALL: f32 = -300.; const TOP_WALL: f32 = 300.; const BRICK_SIZE: Vec2 = Vec2::new(100., 30.); // These values are exact const GAP_BETWEEN_PADDLE_AND_BRICKS: f32 = 270.0; const GAP_BETWEEN_BRICKS: f32 = 5.0; // These values are lower bounds, as the number of bricks is computed const GAP_BETWEEN_BRICKS_AND_CEILING: f32 = 20.0; const GAP_BETWEEN_BRICKS_AND_SIDES: f32 = 20.0; const SCOREBOARD_FONT_SIZE: f32 = 33.0; const SCOREBOARD_TEXT_PADDING: Val = Val::Px(5.0); const BACKGROUND_COLOR: Color = Color::srgb(0.9, 0.9, 0.9); const PADDLE_COLOR: Color = Color::srgb(0.3, 0.3, 0.7); const BALL_COLOR: Color = Color::srgb(1.0, 0.5, 0.5); const BRICK_COLOR: Color = Color::srgb(0.5, 0.5, 1.0); const WALL_COLOR: Color = Color::srgb(0.8, 0.8, 0.8); const TEXT_COLOR: Color = Color::srgb(0.5, 0.5, 1.0); const SCORE_COLOR: Color = Color::srgb(1.0, 0.5, 0.5); fn main() { App::new() .add_plugins(DefaultPlugins) .add_plugins( stepping::SteppingPlugin::default() .add_schedule(Update) .add_schedule(FixedUpdate) .at(Val::Percent(35.0), Val::Percent(50.0)), ) .insert_resource(Score(0)) .insert_resource(ClearColor(BACKGROUND_COLOR)) .add_event::() .add_systems(Startup, setup) // Add our gameplay simulation systems to the fixed timestep schedule // which runs at 64 Hz by default .add_systems( FixedUpdate, ( apply_velocity, move_paddle, check_for_collisions, play_collision_sound, ) // `chain`ing systems together runs them in order .chain(), ) .add_systems(Update, update_scoreboard) .run(); } #[derive(Component)] struct Paddle; #[derive(Component)] struct Ball; #[derive(Component, Deref, DerefMut)] struct Velocity(Vec2); #[derive(Event, Default)] struct CollisionEvent; #[derive(Component)] struct Brick; #[derive(Resource, Deref)] struct CollisionSound(Handle); // Default must be implemented to define this as a required component for the Wall component below #[derive(Component, Default)] struct Collider; // This is a collection of the components that define a "Wall" in our game #[derive(Component)] #[require(Sprite, Transform, Collider)] struct Wall; /// Which side of the arena is this wall located on? enum WallLocation { Left, Right, Bottom, Top, } impl WallLocation { /// Location of the *center* of the wall, used in `transform.translation()` fn position(&self) -> Vec2 { match self { WallLocation::Left => Vec2::new(LEFT_WALL, 0.), WallLocation::Right => Vec2::new(RIGHT_WALL, 0.), WallLocation::Bottom => Vec2::new(0., BOTTOM_WALL), WallLocation::Top => Vec2::new(0., TOP_WALL), } } /// (x, y) dimensions of the wall, used in `transform.scale()` fn size(&self) -> Vec2 { let arena_height = TOP_WALL - BOTTOM_WALL; let arena_width = RIGHT_WALL - LEFT_WALL; // Make sure we haven't messed up our constants assert!(arena_height > 0.0); assert!(arena_width > 0.0); match self { WallLocation::Left | WallLocation::Right => { Vec2::new(WALL_THICKNESS, arena_height + WALL_THICKNESS) } WallLocation::Bottom | WallLocation::Top => { Vec2::new(arena_width + WALL_THICKNESS, WALL_THICKNESS) } } } } impl Wall { // This "builder method" allows us to reuse logic across our wall entities, // making our code easier to read and less prone to bugs when we change the logic // Notice the use of Sprite and Transform alongside Wall, overwriting the default values defined for the required components fn new(location: WallLocation) -> (Wall, Sprite, Transform) { ( Wall, Sprite::from_color(WALL_COLOR, Vec2::ONE), Transform { // We need to convert our Vec2 into a Vec3, by giving it a z-coordinate // This is used to determine the order of our sprites translation: location.position().extend(0.0), // The z-scale of 2D objects must always be 1.0, // or their ordering will be affected in surprising ways. // See https://github.com/bevyengine/bevy/issues/4149 scale: location.size().extend(1.0), ..default() }, ) } } // This resource tracks the game's score #[derive(Resource, Deref, DerefMut)] struct Score(usize); #[derive(Component)] struct ScoreboardUi; // Add the game's entities to our world fn setup( mut commands: Commands, mut meshes: ResMut>, mut materials: ResMut>, asset_server: Res, ) { // Camera commands.spawn(Camera2d); // Sound let ball_collision_sound = asset_server.load("sounds/breakout_collision.ogg"); commands.insert_resource(CollisionSound(ball_collision_sound)); // Paddle let paddle_y = BOTTOM_WALL + GAP_BETWEEN_PADDLE_AND_FLOOR; commands.spawn(( Sprite::from_color(PADDLE_COLOR, Vec2::ONE), Transform { translation: Vec3::new(0.0, paddle_y, 0.0), scale: PADDLE_SIZE.extend(1.0), ..default() }, Paddle, Collider, )); // Ball commands.spawn(( Mesh2d(meshes.add(Circle::default())), MeshMaterial2d(materials.add(BALL_COLOR)), Transform::from_translation(BALL_STARTING_POSITION) .with_scale(Vec2::splat(BALL_DIAMETER).extend(1.)), Ball, Velocity(INITIAL_BALL_DIRECTION.normalize() * BALL_SPEED), )); // Scoreboard commands .spawn(( Text::new("Score: "), TextFont { font_size: SCOREBOARD_FONT_SIZE, ..default() }, TextColor(TEXT_COLOR), ScoreboardUi, Node { position_type: PositionType::Absolute, top: SCOREBOARD_TEXT_PADDING, left: SCOREBOARD_TEXT_PADDING, ..default() }, )) .with_child(( TextSpan::default(), TextFont { font_size: SCOREBOARD_FONT_SIZE, ..default() }, TextColor(SCORE_COLOR), )); // Walls commands.spawn(Wall::new(WallLocation::Left)); commands.spawn(Wall::new(WallLocation::Right)); commands.spawn(Wall::new(WallLocation::Bottom)); commands.spawn(Wall::new(WallLocation::Top)); // Bricks let total_width_of_bricks = (RIGHT_WALL - LEFT_WALL) - 2. * GAP_BETWEEN_BRICKS_AND_SIDES; let bottom_edge_of_bricks = paddle_y + GAP_BETWEEN_PADDLE_AND_BRICKS; let total_height_of_bricks = TOP_WALL - bottom_edge_of_bricks - GAP_BETWEEN_BRICKS_AND_CEILING; assert!(total_width_of_bricks > 0.0); assert!(total_height_of_bricks > 0.0); // Given the space available, compute how many rows and columns of bricks we can fit let n_columns = (total_width_of_bricks / (BRICK_SIZE.x + GAP_BETWEEN_BRICKS)).floor() as usize; let n_rows = (total_height_of_bricks / (BRICK_SIZE.y + GAP_BETWEEN_BRICKS)).floor() as usize; let n_vertical_gaps = n_columns - 1; // Because we need to round the number of columns, // the space on the top and sides of the bricks only captures a lower bound, not an exact value let center_of_bricks = (LEFT_WALL + RIGHT_WALL) / 2.0; let left_edge_of_bricks = center_of_bricks // Space taken up by the bricks - (n_columns as f32 / 2.0 * BRICK_SIZE.x) // Space taken up by the gaps - n_vertical_gaps as f32 / 2.0 * GAP_BETWEEN_BRICKS; // In Bevy, the `translation` of an entity describes the center point, // not its bottom-left corner let offset_x = left_edge_of_bricks + BRICK_SIZE.x / 2.; let offset_y = bottom_edge_of_bricks + BRICK_SIZE.y / 2.; for row in 0..n_rows { for column in 0..n_columns { let brick_position = Vec2::new( offset_x + column as f32 * (BRICK_SIZE.x + GAP_BETWEEN_BRICKS), offset_y + row as f32 * (BRICK_SIZE.y + GAP_BETWEEN_BRICKS), ); // brick commands.spawn(( Sprite { color: BRICK_COLOR, ..default() }, Transform { translation: brick_position.extend(0.0), scale: Vec3::new(BRICK_SIZE.x, BRICK_SIZE.y, 1.0), ..default() }, Brick, Collider, )); } } } fn move_paddle( keyboard_input: Res>, mut paddle_transform: Single<&mut Transform, With>, time: Res