aboutsummaryrefslogtreecommitdiff
path: root/examples
diff options
context:
space:
mode:
authorHenauxg <19689618+Henauxg@users.noreply.github.com>2022-11-03 15:47:30 +0100
committerHenauxg <19689618+Henauxg@users.noreply.github.com>2022-11-03 15:47:30 +0100
commit1a2ff09fc27f10a779aac875a55dbdd0d8791b96 (patch)
treec3bf558782f8948a5c4f5da476e4f6210e3041c1 /examples
parentd2b244966e1ccdad8e8a025b3cf3c65fd8520f20 (diff)
[example:breakout] Networked balls
Diffstat (limited to 'examples')
-rw-r--r--examples/breakout/breakout.rs140
-rw-r--r--examples/breakout/client.rs40
-rw-r--r--examples/breakout/protocol.rs5
-rw-r--r--examples/breakout/server.rs81
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() {