aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHenauxg <19689618+Henauxg@users.noreply.github.com>2022-11-03 17:48:19 +0100
committerHenauxg <19689618+Henauxg@users.noreply.github.com>2022-11-03 17:48:19 +0100
commite104ce1299a64fb2b575a2193ec2151b48667165 (patch)
tree274e3ad13d9a66e1b30f15bb8c61d6f377e79328
parenta6f63d755caf51cd9866ba5aaf0c9fd3c0d56bde (diff)
[example:breakout] Networked bricks destruction
-rw-r--r--examples/breakout/breakout.rs113
-rw-r--r--examples/breakout/client.rs52
-rw-r--r--examples/breakout/protocol.rs7
-rw-r--r--examples/breakout/server.rs102
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,
+ }
+ }
+}