diff options
Diffstat (limited to 'examples/breakout/breakout.rs')
-rw-r--r-- | examples/breakout/breakout.rs | 421 |
1 files changed, 50 insertions, 371 deletions
diff --git a/examples/breakout/breakout.rs b/examples/breakout/breakout.rs index 2c26566..f6fd3ff 100644 --- a/examples/breakout/breakout.rs +++ b/examples/breakout/breakout.rs @@ -11,8 +11,12 @@ use bevy_quinnet::{ client::QuinnetClientPlugin, server::{QuinnetServerPlugin, Server}, }; -use client::{handle_server_messages, start_connection}; -use server::{handle_client_messages, handle_server_events, start_listening}; +use client::{ + handle_menu_buttons, handle_server_messages, move_paddle, play_collision_sound, setup_breakout, + setup_main_menu, start_connection, teardown_main_menu, update_scoreboard, NetworkMapping, + BACKGROUND_COLOR, +}; +use server::{handle_client_messages, handle_server_events, start_listening, update_paddles}; mod client; mod protocol; @@ -29,11 +33,8 @@ 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 @@ -51,25 +52,6 @@ const GAP_BETWEEN_BRICKS: f32 = 5.0; 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, @@ -89,7 +71,8 @@ fn main() { .insert_resource(ClearColor(BACKGROUND_COLOR)) // Resources .insert_resource(server::Players::default()) //TODO Move ? - .insert_resource(client::Users::default()) + .insert_resource(client::ClientData::default()) + .insert_resource(NetworkMapping::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)) @@ -143,7 +126,7 @@ fn main() { }, )) // .with_system(check_for_collisions) - // .with_system(move_paddle.before(check_for_collisions)) + .with_system(update_paddles.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) @@ -155,9 +138,6 @@ fn main() { } #[derive(Component)] -struct Paddle; - -#[derive(Component)] struct Ball; #[derive(Component, Deref, DerefMut)] @@ -175,25 +155,8 @@ struct Brick; #[derive(Component)] struct Score; -/// The buttons in the main menu. -#[derive(Clone, Copy, Component)] -pub enum MenuItem { - Host, - Join, -} - struct CollisionSound(Handle<AudioSource>); -// 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, @@ -230,33 +193,6 @@ impl WallLocation { } } -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, @@ -269,246 +205,6 @@ fn run_if_host(server: Res<Server>) -> ShouldRun { } } -fn setup_main_menu(mut commands: Commands, asset_server: Res<AssetServer>) { - // 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<Interaction>, With<Button>), - >, - mut game_state: ResMut<State<GameState>>, -) { - for (interaction, mut color, item) in &mut interaction_query { - match *interaction { - Interaction::Clicked => { - *color = PRESSED_BUTTON_COLOR.into(); - match item { - MenuItem::Host => game_state.set(GameState::HostingLobby).unwrap(), - MenuItem::Join => game_state.set(GameState::JoiningLobby).unwrap(), - } - } - Interaction::Hovered => { - *color = HOVERED_BUTTON_COLOR.into(); - } - Interaction::None => { - *color = NORMAL_BUTTON_COLOR.into(); - } - } - } -} - -fn teardown_main_menu(mut commands: Commands, query: Query<Entity, With<Button>>) { - for entity in query.iter() { - commands.entity(entity).despawn_recursive(); - } -} - -// Add the game's entities to our world -fn setup_breakout(mut commands: Commands, asset_server: Res<AssetServer>) { - // Sound - let ball_collision_sound = asset_server.load(COLLISION_SOUND_EFFECT); - commands.insert_resource(CollisionSound(ball_collision_sound)); - - // Paddle - let paddle_y = BOTTOM_WALL + GAP_BETWEEN_PADDLE_AND_FLOOR; - - commands - .spawn() - .insert(Paddle) - .insert_bundle(SpriteBundle { - transform: Transform { - translation: Vec3::new(0.0, paddle_y, 0.0), - scale: PADDLE_SIZE, - ..default() - }, - sprite: Sprite { - color: PADDLE_COLOR, - ..default() - }, - ..default() - }) - .insert(Collider); - - // Ball - commands - .spawn() - .insert(Ball) - .insert_bundle(SpriteBundle { - transform: Transform { - scale: BALL_SIZE, - translation: BALL_STARTING_POSITION, - ..default() - }, - sprite: Sprite { - color: BALL_COLOR, - ..default() - }, - ..default() - }) - .insert(Velocity(INITIAL_BALL_DIRECTION.normalize() * BALL_SPEED)); - - // Scoreboard - commands - .spawn() - .insert_bundle( - TextBundle::from_sections([ - TextSection::new( - "Score: ", - TextStyle { - font: asset_server.load(BOLD_FONT), - font_size: SCOREBOARD_FONT_SIZE, - color: TEXT_COLOR, - }, - ), - TextSection::from_style(TextStyle { - font: asset_server.load(NORMAL_FONT), - font_size: SCOREBOARD_FONT_SIZE, - color: SCORE_COLOR, - }), - ]) - .with_style(Style { - position_type: PositionType::Absolute, - position: UiRect { - top: SCOREBOARD_TEXT_PADDING, - left: SCOREBOARD_TEXT_PADDING, - ..default() - }, - ..default() - }), - ) - .insert(Score); - - // Walls - commands.spawn_bundle(WallBundle::new(WallLocation::Left)); - commands.spawn_bundle(WallBundle::new(WallLocation::Right)); - commands.spawn_bundle(WallBundle::new(WallLocation::Bottom)); - commands.spawn_bundle(WallBundle::new(WallLocation::Top)); - - // Bricks - // Negative scales result in flipped sprites / meshes, - // which is definitely not what we want here - assert!(BRICK_SIZE.x > 0.0); - assert!(BRICK_SIZE.y > 0.0); - - 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() - .insert(Brick) - .insert_bundle(SpriteBundle { - sprite: Sprite { - color: BRICK_COLOR, - ..default() - }, - transform: Transform { - translation: brick_position.extend(0.0), - scale: Vec3::new(BRICK_SIZE.x, BRICK_SIZE.y, 1.0), - ..default() - }, - ..default() - }) - .insert(Collider); - } - } -} - -fn move_paddle( - keyboard_input: Res<Input<KeyCode>>, - mut query: Query<&mut Transform, With<Paddle>>, -) { - let mut paddle_transform = query.single_mut(); - let mut direction = 0.0; - - if keyboard_input.pressed(KeyCode::Left) { - direction -= 1.0; - } - - if keyboard_input.pressed(KeyCode::Right) { - direction += 1.0; - } - - // Calculate the new horizontal paddle position based on player input - let new_paddle_position = paddle_transform.translation.x + direction * PADDLE_SPEED * TIME_STEP; - - // Update the paddle position, - // making sure it doesn't cause the paddle to leave the arena - let left_bound = LEFT_WALL + WALL_THICKNESS / 2.0 + PADDLE_SIZE.x / 2.0 + PADDLE_PADDING; - let right_bound = RIGHT_WALL - WALL_THICKNESS / 2.0 - PADDLE_SIZE.x / 2.0 - PADDLE_PADDING; - - paddle_transform.translation.x = new_paddle_position.clamp(left_bound, right_bound); -} - fn apply_velocity(mut query: Query<(&mut Transform, &Velocity)>) { for (mut transform, velocity) in &mut query { transform.translation.x += velocity.x * TIME_STEP; @@ -516,11 +212,6 @@ fn apply_velocity(mut query: Query<(&mut Transform, &Velocity)>) { } } -fn update_scoreboard(scoreboard: Res<Scoreboard>, mut query: Query<&mut Text, With<Score>>) { - let mut text = query.single_mut(); - text.sections[1].value = scoreboard.score.to_string(); -} - fn check_for_collisions( mut commands: Commands, mut scoreboard: ResMut<Scoreboard>, @@ -528,63 +219,51 @@ fn check_for_collisions( collider_query: Query<(Entity, &Transform, Option<&Brick>), With<Collider>>, mut collision_events: EventWriter<CollisionEvent>, ) { - let (mut ball_velocity, ball_transform) = ball_query.single_mut(); - let ball_size = ball_transform.scale.truncate(); - - // check collision with walls - for (collider_entity, transform, maybe_brick) in &collider_query { - let collision = collide( - ball_transform.translation, - ball_size, - transform.translation, - transform.scale.truncate(), - ); - if let Some(collision) = collision { - // Sends a collision event so that other systems can react to the collision - collision_events.send_default(); - - // Bricks should be despawned and increment the scoreboard on collision - if maybe_brick.is_some() { - scoreboard.score += 1; - commands.entity(collider_entity).despawn(); - } - - // reflect the ball when it collides - let mut reflect_x = false; - let mut reflect_y = false; + for (mut ball_velocity, ball_transform) in ball_query.iter_mut() { + let ball_size = ball_transform.scale.truncate(); + + // check collision with walls + for (collider_entity, transform, maybe_brick) in &collider_query { + let collision = collide( + ball_transform.translation, + ball_size, + transform.translation, + transform.scale.truncate(), + ); + if let Some(collision) = collision { + // Sends a collision event so that other systems can react to the collision + collision_events.send_default(); + + // Bricks should be despawned and increment the scoreboard on collision + if maybe_brick.is_some() { + scoreboard.score += 1; + commands.entity(collider_entity).despawn(); + } - // only reflect if the ball's velocity is going in the opposite direction of the - // collision - match collision { - Collision::Left => reflect_x = ball_velocity.x > 0.0, - Collision::Right => reflect_x = ball_velocity.x < 0.0, - Collision::Top => reflect_y = ball_velocity.y < 0.0, - Collision::Bottom => reflect_y = ball_velocity.y > 0.0, - Collision::Inside => { /* do nothing */ } - } + // reflect the ball when it collides + let mut reflect_x = false; + let mut reflect_y = false; + + // only reflect if the ball's velocity is going in the opposite direction of the + // collision + match collision { + Collision::Left => reflect_x = ball_velocity.x > 0.0, + Collision::Right => reflect_x = ball_velocity.x < 0.0, + Collision::Top => reflect_y = ball_velocity.y < 0.0, + Collision::Bottom => reflect_y = ball_velocity.y > 0.0, + Collision::Inside => { /* do nothing */ } + } - // reflect velocity on the x-axis if we hit something on the x-axis - if reflect_x { - ball_velocity.x = -ball_velocity.x; - } + // reflect velocity on the x-axis if we hit something on the x-axis + if reflect_x { + ball_velocity.x = -ball_velocity.x; + } - // reflect velocity on the y-axis if we hit something on the y-axis - if reflect_y { - ball_velocity.y = -ball_velocity.y; + // reflect velocity on the y-axis if we hit something on the y-axis + if reflect_y { + ball_velocity.y = -ball_velocity.y; + } } } } } - -fn play_collision_sound( - collision_events: EventReader<CollisionEvent>, - audio: Res<Audio>, - sound: Res<CollisionSound>, -) { - // Play a sound once per frame if a collision occurred. - if !collision_events.is_empty() { - // This prevents events staying active on the next frame. - collision_events.clear(); - audio.play(sound.0.clone()); - } -} |