diff options
author | Henauxg <19689618+Henauxg@users.noreply.github.com> | 2022-11-03 15:47:30 +0100 |
---|---|---|
committer | Henauxg <19689618+Henauxg@users.noreply.github.com> | 2022-11-03 15:47:30 +0100 |
commit | 1a2ff09fc27f10a779aac875a55dbdd0d8791b96 (patch) | |
tree | c3bf558782f8948a5c4f5da476e4f6210e3041c1 /examples | |
parent | d2b244966e1ccdad8e8a025b3cf3c65fd8520f20 (diff) |
[example:breakout] Networked balls
Diffstat (limited to 'examples')
-rw-r--r-- | examples/breakout/breakout.rs | 140 | ||||
-rw-r--r-- | examples/breakout/client.rs | 40 | ||||
-rw-r--r-- | examples/breakout/protocol.rs | 5 | ||||
-rw-r--r-- | examples/breakout/server.rs | 81 |
4 files changed, 149 insertions, 117 deletions
diff --git a/examples/breakout/breakout.rs b/examples/breakout/breakout.rs index 1ee350b..1511037 100644 --- a/examples/breakout/breakout.rs +++ b/examples/breakout/breakout.rs @@ -1,22 +1,12 @@ //! A simplified implementation of the classic game "Breakout". //! => Original example by Bevy, modified for Bevy Quinnet to add a 2 players versus mode. -use bevy::{ - ecs::schedule::ShouldRun, - prelude::*, - sprite::collide_aabb::{collide, Collision}, - time::FixedTimestep, -}; +use bevy::{ecs::schedule::ShouldRun, prelude::*, time::FixedTimestep}; use bevy_quinnet::{ client::QuinnetClientPlugin, server::{QuinnetServerPlugin, Server}, }; -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}; +use client::{NetworkMapping, BACKGROUND_COLOR}; mod client; mod protocol; @@ -74,66 +64,74 @@ fn main() { .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)) - .add_system_set(SystemSet::on_exit(GameState::MainMenu).with_system(teardown_main_menu)) - // Hosting + .add_system_set( + SystemSet::on_enter(GameState::MainMenu).with_system(client::setup_main_menu), + ) + .add_system_set( + SystemSet::on_update(GameState::MainMenu).with_system(client::handle_menu_buttons), + ) + .add_system_set( + SystemSet::on_exit(GameState::MainMenu).with_system(client::teardown_main_menu), + ) + // Hosting a server on a client .add_system_set( SystemSet::on_enter(GameState::HostingLobby) - .with_system(start_listening) - .with_system(start_connection), + .with_system(server::start_listening) + .with_system(client::start_connection), ) .add_system_set( SystemSet::on_update(GameState::HostingLobby) - .with_system(handle_client_messages) - .with_system(handle_server_events) - .with_system(handle_server_messages), + .with_system(server::handle_client_messages) + .with_system(server::handle_server_events) + .with_system(client::handle_server_messages), + ) + // or just Joining as a client + .add_system_set( + SystemSet::on_enter(GameState::JoiningLobby).with_system(client::start_connection), ) - // or just Joining - .add_system_set(SystemSet::on_enter(GameState::JoiningLobby).with_system(start_connection)) .add_system_set( - SystemSet::on_update(GameState::JoiningLobby).with_system(handle_server_messages), + SystemSet::on_update(GameState::JoiningLobby) + .with_system(client::handle_server_messages), ) // Running the game. // Every app is a client - .add_system_set(SystemSet::on_enter(GameState::Running).with_system(setup_breakout)) + .add_system_set(SystemSet::on_enter(GameState::Running).with_system(client::setup_breakout)) .add_system_set( SystemSet::new() // https://github.com/bevyengine/bevy/issues/1839 + // Run on a fixed Timestep,on all clients, in GameState::Running .with_run_criteria(FixedTimestep::step(TIME_STEP as f64).chain( |In(input): In<ShouldRun>, state: Res<State<GameState>>| match state.current() { GameState::Running => input, _ => ShouldRun::No, }, )) - .with_system(handle_server_messages) - // .with_system(check_for_collisions) - // .with_system(move_paddle.before(check_for_collisions)) - .with_system(move_paddle) - // .with_system(apply_velocity.before(check_for_collisions)) - // .with_system(play_collision_sound.after(check_for_collisions)) - .with_system(update_scoreboard), + .with_system(client::handle_server_messages.before(client::apply_velocity)) + .with_system(client::apply_velocity) + .with_system(client::move_paddle) + .with_system(client::update_scoreboard) + .with_system(client::play_collision_sound.after(client::handle_server_messages)), ) // But hosting apps are also a server .add_system_set( SystemSet::new() // https://github.com/bevyengine/bevy/issues/1839 - // Run on a fixed Timestep, only for the server, in GameState::Running + // Run on a fixed Timestep, only for the hosting client, in GameState::Running .with_run_criteria(FixedTimestep::step(TIME_STEP as f64).chain( |In(input): In<ShouldRun>, state: Res<State<GameState>>, server: Res<Server>| match state.current() { GameState::Running => match server.is_listening() { true => input, - false => ShouldRun::Yes, + false => ShouldRun::No, }, _ => ShouldRun::No, }, )) - .with_system(handle_client_messages.before(update_paddles)) - .with_system(update_paddles.before(check_for_collisions)) - .with_system(check_for_collisions), - // .with_system(apply_velocity.before(check_for_collisions)) + .with_system(server::handle_client_messages.before(server::update_paddles)) + .with_system(server::update_paddles.before(server::check_for_collisions)) + .with_system(server::apply_velocity.before(server::check_for_collisions)) + .with_system(server::check_for_collisions), // .with_system(play_collision_sound.after(check_for_collisions)) // .with_system(update_scoreboard) ) @@ -141,9 +139,6 @@ fn main() { .run(); } -#[derive(Component)] -struct Ball; - #[derive(Component, Deref, DerefMut)] struct Velocity(Vec2); @@ -201,66 +196,3 @@ impl WallLocation { struct Scoreboard { score: usize, } - -fn apply_velocity(mut query: Query<(&mut Transform, &Velocity)>) { - for (mut transform, velocity) in &mut query { - transform.translation.x += velocity.x * TIME_STEP; - transform.translation.y += velocity.y * TIME_STEP; - } -} - -fn check_for_collisions( - mut commands: Commands, - mut scoreboard: ResMut<Scoreboard>, - mut ball_query: Query<(&mut Velocity, &Transform), With<Ball>>, - collider_query: Query<(Entity, &Transform, Option<&Brick>), With<Collider>>, - mut collision_events: EventWriter<CollisionEvent>, -) { - 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(); - } - - // 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 y-axis if we hit something on the y-axis - if reflect_y { - ball_velocity.y = -ball_velocity.y; - } - } - } - } -} diff --git a/examples/breakout/client.rs b/examples/breakout/client.rs index 1596287..ff55257 100644 --- a/examples/breakout/client.rs +++ b/examples/breakout/client.rs @@ -3,8 +3,9 @@ use std::collections::HashMap; use bevy::{ prelude::{ default, AssetServer, Audio, BuildChildren, Bundle, Button, ButtonBundle, Camera2dBundle, - Changed, Color, Commands, Component, DespawnRecursiveExt, Entity, EventReader, Input, - KeyCode, Local, Query, Res, ResMut, State, TextBundle, Transform, Vec2, Vec3, With, + Changed, Color, Commands, Component, DespawnRecursiveExt, Entity, EventReader, EventWriter, + Input, KeyCode, Local, Query, Res, ResMut, State, TextBundle, Transform, Vec2, Vec3, With, + Without, }, sprite::{Sprite, SpriteBundle}, text::{Text, TextSection, TextStyle}, @@ -19,10 +20,10 @@ use bevy_quinnet::{ use crate::{ protocol::{ClientMessage, PaddleInput, ServerMessage}, - Ball, Brick, Collider, CollisionEvent, CollisionSound, GameState, Score, Scoreboard, Velocity, + Brick, Collider, CollisionEvent, CollisionSound, GameState, Score, Scoreboard, Velocity, WallLocation, BALL_SIZE, BALL_SPEED, BOTTOM_WALL, BRICK_SIZE, GAP_BETWEEN_BRICKS, GAP_BETWEEN_BRICKS_AND_CEILING, GAP_BETWEEN_BRICKS_AND_SIDES, GAP_BETWEEN_PADDLE_AND_BRICKS, - GAP_BETWEEN_PADDLE_AND_FLOOR, LEFT_WALL, PADDLE_SIZE, RIGHT_WALL, TOP_WALL, + GAP_BETWEEN_PADDLE_AND_FLOOR, LEFT_WALL, PADDLE_SIZE, RIGHT_WALL, TIME_STEP, TOP_WALL, }; const SCOREBOARD_FONT_SIZE: f32 = 40.0; @@ -58,6 +59,9 @@ pub(crate) struct NetworkMapping { #[derive(Component)] pub(crate) struct Paddle; +#[derive(Component)] +pub(crate) struct Ball; + /// The buttons in the main menu. #[derive(Clone, Copy, Component)] pub(crate) enum MenuItem { @@ -87,7 +91,6 @@ pub(crate) fn start_connection(client: ResMut<Client>) { fn spawn_paddle(commands: &mut Commands, position: &Vec3) -> Entity { commands .spawn() - // .insert(Paddle) .insert_bundle(SpriteBundle { transform: Transform { translation: *position, @@ -132,6 +135,8 @@ pub(crate) fn handle_server_messages( mut entity_mapping: ResMut<NetworkMapping>, mut game_state: ResMut<State<GameState>>, mut paddles: Query<&mut Transform, With<Paddle>>, + mut balls: Query<(&mut Transform, &mut Velocity), (With<Ball>, Without<Paddle>)>, + mut collision_events: EventWriter<CollisionEvent>, ) { while let Ok(Some(message)) = client.receive_message::<ServerMessage>() { match message { @@ -156,8 +161,22 @@ pub(crate) fn handle_server_messages( } ServerMessage::StartGame {} => game_state.set(GameState::Running).unwrap(), ServerMessage::BrickDestroyed { client_id } => todo!(), - ServerMessage::BallPosition { entity, position } => todo!(), - ServerMessage::PaddlePosition { entity, position } => { + ServerMessage::BallCollided { + entity, + position, + velocity, + } => { + if let Some(local_ball) = entity_mapping.map.get(&entity) { + if let Ok((mut ball_transform, mut ball_velocity)) = balls.get_mut(*local_ball) + { + ball_transform.translation = position; + ball_velocity.0 = velocity; + } + } + // Sends a collision event so that other systems can react to the collision + collision_events.send_default(); + } + ServerMessage::PaddleMoved { entity, position } => { if let Some(local_paddle) = entity_mapping.map.get(&entity) { if let Ok(mut paddle_transform) = paddles.get_mut(*local_paddle) { paddle_transform.translation = position; @@ -396,6 +415,13 @@ pub(crate) fn setup_breakout(mut commands: Commands, asset_server: Res<AssetServ } } +pub(crate) fn apply_velocity(mut query: Query<(&mut Transform, &Velocity), With<Ball>>) { + for (mut transform, velocity) in &mut query { + transform.translation.x += velocity.x * TIME_STEP; + transform.translation.y += velocity.y * TIME_STEP; + } +} + 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 diff --git a/examples/breakout/protocol.rs b/examples/breakout/protocol.rs index 766094a..a42e0e4 100644 --- a/examples/breakout/protocol.rs +++ b/examples/breakout/protocol.rs @@ -36,11 +36,12 @@ pub(crate) enum ServerMessage { BrickDestroyed { client_id: ClientId, }, - BallPosition { + BallCollided { entity: Entity, position: Vec3, + velocity: Vec2, }, - PaddlePosition { + PaddleMoved { entity: Entity, position: Vec3, }, diff --git a/examples/breakout/server.rs b/examples/breakout/server.rs index 8d79523..663cd00 100644 --- a/examples/breakout/server.rs +++ b/examples/breakout/server.rs @@ -3,7 +3,9 @@ use std::collections::HashMap; use bevy::{ prelude::{ default, Commands, Component, Entity, EventReader, Query, ResMut, Transform, Vec2, Vec3, + With, }, + sprite::collide_aabb::{collide, Collision}, transform::TransformBundle, }; use bevy_quinnet::{ @@ -13,9 +15,9 @@ use bevy_quinnet::{ use crate::{ protocol::{ClientMessage, PaddleInput, ServerMessage}, - Ball, Collider, Velocity, BALL_SIZE, BALL_SPEED, BOTTOM_WALL, GAP_BETWEEN_PADDLE_AND_FLOOR, - LEFT_WALL, PADDLE_PADDING, PADDLE_SIZE, PADDLE_SPEED, RIGHT_WALL, TIME_STEP, TOP_WALL, - WALL_THICKNESS, + Brick, Collider, Scoreboard, Velocity, BALL_SIZE, BALL_SPEED, BOTTOM_WALL, + GAP_BETWEEN_PADDLE_AND_FLOOR, LEFT_WALL, PADDLE_PADDING, PADDLE_SIZE, PADDLE_SPEED, RIGHT_WALL, + TIME_STEP, TOP_WALL, WALL_THICKNESS, }; const GAP_BETWEEN_PADDLE_AND_BALL: f32 = 35.; @@ -56,6 +58,9 @@ pub(crate) struct Paddle { player_id: ClientId, } +#[derive(Component)] +pub(crate) struct Ball; + pub(crate) fn start_listening(mut server: ResMut<Server>) { server .start( @@ -134,7 +139,7 @@ pub(crate) fn update_paddles( server .send_group_message( players.map.keys().into_iter(), - ServerMessage::PaddlePosition { + ServerMessage::PaddleMoved { entity: paddle_entity, position: paddle_transform.translation, }, @@ -145,6 +150,74 @@ pub(crate) fn update_paddles( } } +pub(crate) fn check_for_collisions( + mut commands: Commands, + mut server: ResMut<Server>, + mut scoreboard: ResMut<Scoreboard>, + mut ball_query: Query<(&mut Velocity, &Transform, Entity), With<Ball>>, + collider_query: Query<(Entity, &Transform, Option<&Brick>), With<Collider>>, +) { + for (mut ball_velocity, ball_transform, ball) 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 { + // 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; + + // 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 y-axis if we hit something on the y-axis + if reflect_y { + ball_velocity.y = -ball_velocity.y; + } + + server + .broadcast_message(ServerMessage::BallCollided { + entity: ball, + position: ball_transform.translation, + velocity: ball_velocity.0, + }) + .unwrap(); + } + } + } +} + +pub(crate) fn apply_velocity(mut query: Query<(&mut Transform, &Velocity), With<Ball>>) { + for (mut transform, velocity) in &mut query { + transform.translation.x += velocity.x * TIME_STEP; + transform.translation.y += velocity.y * TIME_STEP; + } +} + fn start_game(commands: &mut Commands, server: &mut ResMut<Server>, players: &ResMut<Players>) { // Paddles for (index, (client_id, _)) in players.map.iter().enumerate() { |