diff options
author | Henauxg <19689618+Henauxg@users.noreply.github.com> | 2022-11-03 17:48:19 +0100 |
---|---|---|
committer | Henauxg <19689618+Henauxg@users.noreply.github.com> | 2022-11-03 17:48:19 +0100 |
commit | e104ce1299a64fb2b575a2193ec2151b48667165 (patch) | |
tree | 274e3ad13d9a66e1b30f15bb8c61d6f377e79328 | |
parent | a6f63d755caf51cd9866ba5aaf0c9fd3c0d56bde (diff) |
[example:breakout] Networked bricks destruction
-rw-r--r-- | examples/breakout/breakout.rs | 113 | ||||
-rw-r--r-- | examples/breakout/client.rs | 52 | ||||
-rw-r--r-- | examples/breakout/protocol.rs | 7 | ||||
-rw-r--r-- | examples/breakout/server.rs | 102 |
4 files changed, 172 insertions, 102 deletions
diff --git a/examples/breakout/breakout.rs b/examples/breakout/breakout.rs index 96527c2..894364a 100644 --- a/examples/breakout/breakout.rs +++ b/examples/breakout/breakout.rs @@ -12,6 +12,7 @@ mod client; mod protocol; mod server; +const SERVER_HOST: &str = "127.0.0.1"; const SERVER_PORT: u16 = 6000; // Defines the amount of time that should elapse between each physics step. @@ -51,6 +52,60 @@ enum GameState { Running, } +#[derive(Component, Deref, DerefMut)] +struct Velocity(Vec2); + +#[derive(Default)] +struct CollisionEvent; + +#[derive(Component)] +struct Score; + +struct CollisionSound(Handle<AudioSource>); + +pub type BrickId = u64; + +/// 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) + } + } + } +} + +// This resource tracks the game's score +struct Scoreboard { + score: usize, +} + fn main() { App::new() .add_plugins(DefaultPlugins) @@ -63,7 +118,8 @@ fn main() { .insert_resource(ClearColor(BACKGROUND_COLOR)) .insert_resource(server::Players::default()) .insert_resource(client::ClientData::default()) - .insert_resource(NetworkMapping::default()) + .insert_resource(client::NetworkMapping::default()) + .insert_resource(client::BricksMapping::default()) // Main menu .add_system_set( SystemSet::on_enter(GameState::MainMenu).with_system(client::setup_main_menu), @@ -139,58 +195,3 @@ fn main() { .add_system(bevy::window::close_on_esc) .run(); } - -#[derive(Component, Deref, DerefMut)] -struct Velocity(Vec2); - -#[derive(Component)] -struct Collider; - -#[derive(Default)] -struct CollisionEvent; - -#[derive(Component)] -struct Score; - -struct CollisionSound(Handle<AudioSource>); - -/// 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) - } - } - } -} - -// This resource tracks the game's score -struct Scoreboard { - score: usize, -} diff --git a/examples/breakout/client.rs b/examples/breakout/client.rs index 6598ec4..4b757c4 100644 --- a/examples/breakout/client.rs +++ b/examples/breakout/client.rs @@ -20,8 +20,9 @@ use bevy_quinnet::{ use crate::{ protocol::{ClientMessage, PaddleInput, ServerMessage}, - Collider, CollisionEvent, CollisionSound, GameState, Score, Scoreboard, Velocity, WallLocation, - BALL_SIZE, BALL_SPEED, BRICK_SIZE, GAP_BETWEEN_BRICKS, PADDLE_SIZE, SERVER_PORT, TIME_STEP, + BrickId, CollisionEvent, CollisionSound, GameState, Score, Scoreboard, Velocity, WallLocation, + BALL_SIZE, BALL_SPEED, BRICK_SIZE, GAP_BETWEEN_BRICKS, PADDLE_SIZE, SERVER_HOST, SERVER_PORT, + TIME_STEP, }; const SCOREBOARD_FONT_SIZE: f32 = 40.0; @@ -53,6 +54,10 @@ pub(crate) struct NetworkMapping { // Network entity id to local entity id map: HashMap<Entity, Entity>, } +#[derive(Default)] +pub struct BricksMapping { + map: HashMap<BrickId, Entity>, +} #[derive(Component)] pub(crate) struct Paddle; @@ -60,7 +65,6 @@ pub(crate) struct Paddle; #[derive(Component)] pub(crate) struct Ball; -pub type BrickId = u64; #[derive(Component)] pub(crate) struct Brick(BrickId); @@ -74,18 +78,15 @@ pub(crate) enum MenuItem { // 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, } pub(crate) fn start_connection(client: ResMut<Client>) { client .connect( ClientConfigurationData::new( - "127.0.0.1".to_string(), + SERVER_HOST.to_string(), SERVER_PORT, "0.0.0.0".to_string(), 0, @@ -110,7 +111,6 @@ fn spawn_paddle(commands: &mut Commands, position: &Vec3) -> Entity { }, ..default() }) - .insert(Collider) .insert(Paddle) .id() } @@ -135,7 +135,13 @@ fn spawn_ball(commands: &mut Commands, pos: &Vec3, direction: &Vec2) -> Entity { .id() } -pub(crate) fn spawn_bricks(commands: &mut Commands, offset: Vec2, rows: usize, columns: usize) { +pub(crate) fn spawn_bricks( + commands: &mut Commands, + bricks: &mut ResMut<BricksMapping>, + offset: Vec2, + rows: usize, + columns: usize, +) { let mut brick_id = 0; for row in 0..rows { for column in 0..columns { @@ -144,8 +150,7 @@ pub(crate) fn spawn_bricks(commands: &mut Commands, offset: Vec2, rows: usize, c offset.y + row as f32 * (BRICK_SIZE.y + GAP_BETWEEN_BRICKS), ); - // brick - commands + let brick = commands .spawn() .insert(Brick(brick_id)) .insert_bundle(SpriteBundle { @@ -160,7 +165,8 @@ pub(crate) fn spawn_bricks(commands: &mut Commands, offset: Vec2, rows: usize, c }, ..default() }) - .insert(Collider); + .id(); + bricks.map.insert(brick_id, brick); brick_id += 1; } } @@ -174,10 +180,8 @@ pub(crate) fn handle_server_messages( 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 bricks: Query< - (&mut Transform, &mut Velocity), - (With<Brick>, Without<Paddle>, Without<Ball>), - >, + // mut bricks: Query<(&mut Transform, &mut Velocity, Entity, &Brick)>, // (With<Brick>, Without<Paddle>, Without<Ball>), + mut bricks: ResMut<BricksMapping>, mut collision_events: EventWriter<CollisionEvent>, ) { while let Ok(Some(message)) = client.receive_message::<ServerMessage>() { @@ -186,7 +190,7 @@ pub(crate) fn handle_server_messages( client_data.self_id = client_id; } ServerMessage::SpawnPaddle { - client_id, + owner_client_id: client_id, entity, position, } => { @@ -205,9 +209,16 @@ pub(crate) fn handle_server_messages( offset, rows, columns, - } => spawn_bricks(&mut commands, offset, rows, columns), + } => spawn_bricks(&mut commands, &mut bricks, offset, rows, columns), ServerMessage::StartGame {} => game_state.set(GameState::Running).unwrap(), - ServerMessage::BrickDestroyed { client_id } => todo!(), + ServerMessage::BrickDestroyed { + by_client_id, + brick_id, + } => { + if let Some(brick_entity) = bricks.map.get(&brick_id) { + commands.entity(*brick_entity).despawn(); + } + } ServerMessage::BallCollided { entity, position, @@ -409,8 +420,6 @@ pub(crate) fn apply_velocity(mut query: Query<(&mut Transform, &Velocity), With< } 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 { @@ -430,7 +439,6 @@ impl WallBundle { }, ..default() }, - collider: Collider, } } } diff --git a/examples/breakout/protocol.rs b/examples/breakout/protocol.rs index 4e2b309..92a0cfb 100644 --- a/examples/breakout/protocol.rs +++ b/examples/breakout/protocol.rs @@ -2,6 +2,8 @@ use bevy::prelude::{Entity, Vec2, Vec3}; use bevy_quinnet::ClientId; use serde::{Deserialize, Serialize}; +use crate::BrickId; + #[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)] pub(crate) enum PaddleInput { #[default] @@ -23,7 +25,7 @@ pub(crate) enum ServerMessage { client_id: ClientId, }, SpawnPaddle { - client_id: ClientId, + owner_client_id: ClientId, entity: Entity, position: Vec3, }, @@ -39,7 +41,8 @@ pub(crate) enum ServerMessage { }, StartGame, BrickDestroyed { - client_id: ClientId, + by_client_id: ClientId, + brick_id: BrickId, }, BallCollided { entity: Entity, diff --git a/examples/breakout/server.rs b/examples/breakout/server.rs index fb430f4..8ab796a 100644 --- a/examples/breakout/server.rs +++ b/examples/breakout/server.rs @@ -2,8 +2,8 @@ use std::collections::HashMap; use bevy::{ prelude::{ - default, Commands, Component, Entity, EventReader, Query, ResMut, Transform, Vec2, Vec3, - With, + default, Bundle, Commands, Component, Entity, EventReader, Query, ResMut, Transform, Vec2, + Vec3, With, }, sprite::collide_aabb::{collide, Collision}, transform::TransformBundle, @@ -15,10 +15,10 @@ use bevy_quinnet::{ use crate::{ protocol::{ClientMessage, PaddleInput, ServerMessage}, - Collider, Scoreboard, Velocity, BALL_SIZE, BALL_SPEED, BOTTOM_WALL, BRICK_SIZE, + BrickId, Scoreboard, Velocity, WallLocation, BALL_SIZE, BALL_SPEED, BOTTOM_WALL, BRICK_SIZE, GAP_BETWEEN_BRICKS, GAP_BETWEEN_BRICKS_AND_SIDES, GAP_BETWEEN_PADDLE_AND_BRICKS, GAP_BETWEEN_PADDLE_AND_FLOOR, LEFT_WALL, PADDLE_PADDING, PADDLE_SIZE, PADDLE_SPEED, RIGHT_WALL, - SERVER_PORT, TIME_STEP, TOP_WALL, WALL_THICKNESS, + SERVER_HOST, SERVER_PORT, TIME_STEP, TOP_WALL, WALL_THICKNESS, }; const GAP_BETWEEN_PADDLE_AND_BALL: f32 = 35.; @@ -59,18 +59,29 @@ pub(crate) struct Paddle { player_id: ClientId, } -pub type BrickId = u64; #[derive(Component)] pub(crate) struct Brick(BrickId); #[derive(Component)] -pub(crate) struct Ball; +pub(crate) struct Collider; + +#[derive(Component)] +pub(crate) struct Ball { + last_hit_by: ClientId, +} + +#[derive(Bundle)] +struct WallBundle { + #[bundle] + transform_bundle: TransformBundle, + collider: Collider, +} pub(crate) fn start_listening(mut server: ResMut<Server>) { server .start( ServerConfigurationData::new( - "127.0.0.1".to_string(), + SERVER_HOST.to_string(), SERVER_PORT, "0.0.0.0".to_string(), ), @@ -108,7 +119,6 @@ pub(crate) fn handle_server_events( Player { score: 0, input: PaddleInput::None, - // paddle: None, }, ); if players.map.len() == 2 { @@ -163,14 +173,14 @@ 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>>, + mut ball_query: Query<(&mut Velocity, &Transform, Entity, &mut Ball)>, + collider_query: Query<(Entity, &Transform, Option<&Brick>, Option<&Paddle>), With<Collider>>, ) { - for (mut ball_velocity, ball_transform, ball) in ball_query.iter_mut() { + for (mut ball_velocity, ball_transform, ball_entity, mut 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 { + for (collider_entity, transform, maybe_brick, maybe_paddle) in &collider_query { let collision = collide( ball_transform.translation, ball_size, @@ -178,10 +188,22 @@ pub(crate) fn check_for_collisions( transform.scale.truncate(), ); if let Some(collision) = collision { + // When a ball hit a paddle, mark this ball as belonging to this client + if let Some(paddle) = maybe_paddle { + ball.last_hit_by = paddle.player_id; + } + // Bricks should be despawned and increment the scoreboard on collision - if maybe_brick.is_some() { + if let Some(brick) = maybe_brick { scoreboard.score += 1; commands.entity(collider_entity).despawn(); + + server + .broadcast_message(ServerMessage::BrickDestroyed { + by_client_id: ball.last_hit_by, + brick_id: brick.0, + }) + .unwrap(); } // reflect the ball when it collides @@ -210,7 +232,7 @@ pub(crate) fn check_for_collisions( server .broadcast_message(ServerMessage::BallCollided { - entity: ball, + entity: ball_entity, position: ball_transform.translation, velocity: ball_velocity.0, }) @@ -229,26 +251,30 @@ pub(crate) fn apply_velocity(mut query: Query<(&mut Transform, &Velocity), With< fn start_game(commands: &mut Commands, server: &mut ResMut<Server>, players: &ResMut<Players>) { // Spawn paddles - for (index, (client_id, _)) in players.map.iter().enumerate() { - let paddle = spawn_paddle(commands, *client_id, &PADDLES_STARTING_POSITION[index]); + for (position, client_id) in PADDLES_STARTING_POSITION + .iter() + .zip(players.map.keys().into_iter()) + { + let paddle = spawn_paddle(commands, *client_id, &position); server .send_group_message( players.map.keys().into_iter(), ServerMessage::SpawnPaddle { - client_id: *client_id, + owner_client_id: *client_id, entity: paddle, - position: PADDLES_STARTING_POSITION[index], + position: *position, }, ) .unwrap(); } // Spawn balls - for (position, direction) in BALLS_STARTING_POSITION + for ((position, direction), client_id) in BALLS_STARTING_POSITION .iter() .zip(INITIAL_BALLS_DIRECTION.iter()) + .zip(players.map.keys().into_iter()) { - let ball = spawn_ball(commands, position, direction); + let ball = spawn_ball(commands, *client_id, position, direction); server .send_group_message( players.map.keys().into_iter(), @@ -261,6 +287,12 @@ fn start_game(commands: &mut Commands, server: &mut ResMut<Server>, players: &Re .unwrap(); } + // Spawn 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)); + // Spawn bricks // Negative scales result in flipped sprites / meshes, // which is definitely not what we want here @@ -362,10 +394,17 @@ fn spawn_paddle(commands: &mut Commands, client_id: ClientId, pos: &Vec3) -> Ent .id() } -fn spawn_ball(commands: &mut Commands, pos: &Vec3, direction: &Vec2) -> Entity { +fn spawn_ball( + commands: &mut Commands, + client_id: ClientId, + pos: &Vec3, + direction: &Vec2, +) -> Entity { commands .spawn() - .insert(Ball) + .insert(Ball { + last_hit_by: client_id, + }) .insert_bundle(TransformBundle { local: Transform { scale: BALL_SIZE, @@ -377,3 +416,22 @@ fn spawn_ball(commands: &mut Commands, pos: &Vec3, direction: &Vec2) -> Entity { .insert(Velocity(direction.normalize() * BALL_SPEED)) .id() } + +impl WallBundle { + fn new(location: WallLocation) -> WallBundle { + WallBundle { + transform_bundle: TransformBundle { + local: Transform { + 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() + }, + ..default() + }, + collider: Collider, + } + } +} |