//! A simplified implementation of the classic game "Breakout". use bevy::{ ecs::schedule::ShouldRun, prelude::*, sprite::collide_aabb::{collide, Collision}, time::FixedTimestep, }; use bevy_quinnet::{client::QuinnetClientPlugin, server::QuinnetServerPlugin}; use client::{handle_server_messages, start_connection}; use server::{handle_client_messages, handle_server_events, start_listening, Users}; mod client; mod protocol; mod server; // Defines the amount of time that should elapse between each physics step. const TIME_STEP: f32 = 1.0 / 60.0; // These constants are defined in `Transform` units. // Using the default 2D camera they correspond 1:1 with screen pixels. const PADDLE_SIZE: Vec3 = Vec3::new(120.0, 20.0, 0.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_SIZE: Vec3 = Vec3::new(30.0, 30.0, 0.0); 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 = 40.0; const SCOREBOARD_TEXT_PADDING: Val = Val::Px(5.0); const BACKGROUND_COLOR: Color = Color::rgb(0.9, 0.9, 0.9); const PADDLE_COLOR: Color = Color::rgb(0.3, 0.3, 0.7); const BALL_COLOR: Color = Color::rgb(1.0, 0.5, 0.5); const BRICK_COLOR: Color = Color::rgb(0.5, 0.5, 1.0); const WALL_COLOR: Color = Color::rgb(0.8, 0.8, 0.8); const TEXT_COLOR: Color = Color::rgb(0.5, 0.5, 1.0); const SCORE_COLOR: Color = Color::rgb(1.0, 0.5, 0.5); const NORMAL_BUTTON_COLOR: Color = Color::rgb(0.15, 0.15, 0.15); const HOVERED_BUTTON_COLOR: Color = Color::rgb(0.25, 0.25, 0.25); const PRESSED_BUTTON_COLOR: Color = Color::rgb(0.35, 0.75, 0.35); const BUTTON_TEXT_COLOR: Color = Color::rgb(0.9, 0.9, 0.9); const BOLD_FONT: &str = "fonts/FiraSans-Bold.ttf"; const NORMAL_FONT: &str = "fonts/FiraMono-Medium.ttf"; const COLLISION_SOUND_EFFECT: &str = "sounds/breakout_collision.ogg"; #[derive(Clone, Eq, PartialEq, Debug, Hash)] enum GameState { MainMenu, Hosting, Joining, Running, } fn main() { App::new() .add_plugins(DefaultPlugins) .add_plugin(QuinnetServerPlugin::default()) .add_plugin(QuinnetClientPlugin::default()) .add_event::() .add_state(GameState::MainMenu) .insert_resource(Scoreboard { score: 0 }) .insert_resource(ClearColor(BACKGROUND_COLOR)) // Resources .insert_resource(server::Users::default()) //TODO Move ? .insert_resource(client::Users::default()) // Main menu .add_system_set(SystemSet::on_enter(GameState::MainMenu).with_system(setup_main_menu)) .add_system_set(SystemSet::on_update(GameState::MainMenu).with_system(handle_menu_buttons)) .add_system_set(SystemSet::on_exit(GameState::MainMenu).with_system(teardown_main_menu)) // Hosting .add_system_set(SystemSet::on_enter(GameState::Hosting).with_system(start_listening)) .add_system_set( SystemSet::on_update(GameState::Hosting) .with_system(handle_client_messages) .with_system(handle_server_events), ) // or Joining .add_system_set(SystemSet::on_enter(GameState::Joining).with_system(start_connection)) .add_system_set( SystemSet::on_update(GameState::Joining) .with_system(handle_server_messages) .with_system(handle_server_events), ) // Running the game .add_system_set(SystemSet::on_enter(GameState::Running).with_system(setup_breakout)) .add_system_set( SystemSet::new() // https://github.com/bevyengine/bevy/issues/1839 .with_run_criteria(FixedTimestep::step(TIME_STEP as f64).chain( |In(input): In, state: Res>| match state.current() { GameState::Running => input, _ => ShouldRun::No, }, )) .with_system(check_for_collisions) .with_system(move_paddle.before(check_for_collisions)) .with_system(apply_velocity.before(check_for_collisions)) .with_system(play_collision_sound.after(check_for_collisions)) .with_system(update_scoreboard), ) .add_system(bevy::window::close_on_esc) .run(); } #[derive(Component)] struct Paddle; #[derive(Component)] struct Ball; #[derive(Component, Deref, DerefMut)] struct Velocity(Vec2); #[derive(Component)] struct Collider; #[derive(Default)] struct CollisionEvent; #[derive(Component)] struct Brick; #[derive(Component)] struct Score; /// The buttons in the main menu. #[derive(Clone, Copy, Component)] pub enum MenuItem { Host, Join, } struct CollisionSound(Handle); // This bundle is a collection of the components that define a "wall" in our game #[derive(Bundle)] struct WallBundle { // You can nest bundles inside of other bundles like this // Allowing you to compose their functionality #[bundle] sprite_bundle: SpriteBundle, collider: Collider, } /// Which side of the arena is this wall located on? enum WallLocation { Left, Right, Bottom, Top, } impl WallLocation { 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), } } 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 WallBundle { // 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 fn new(location: WallLocation) -> WallBundle { WallBundle { sprite_bundle: SpriteBundle { transform: 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() }, sprite: Sprite { color: WALL_COLOR, ..default() }, ..default() }, collider: Collider, } } } // This resource tracks the game's score struct Scoreboard { score: usize, } fn setup_main_menu(mut commands: Commands, asset_server: Res) { // Camera commands.spawn_bundle(Camera2dBundle::default()); let button_style = Style { size: Size::new(Val::Px(150.0), Val::Px(65.0)), // center button margin: UiRect::all(Val::Auto), // horizontally center child text justify_content: JustifyContent::Center, // vertically center child text align_items: AlignItems::Center, ..default() }; let text_style = TextStyle { font: asset_server.load(BOLD_FONT), font_size: 40.0, color: BUTTON_TEXT_COLOR, }; commands .spawn_bundle(ButtonBundle { style: button_style.clone(), color: NORMAL_BUTTON_COLOR.into(), ..default() }) .insert(MenuItem::Host) .with_children(|parent| { parent.spawn_bundle(TextBundle::from_section("Host", text_style.clone())); }); commands .spawn_bundle(ButtonBundle { style: button_style, color: NORMAL_BUTTON_COLOR.into(), ..default() }) .insert(MenuItem::Join) .with_children(|parent| { parent.spawn_bundle(TextBundle::from_section("Join", text_style)); }); } fn handle_menu_buttons( mut interaction_query: Query< (&Interaction, &mut UiColor, &MenuItem), (Changed, With