use std::collections::HashMap;
use bevy::{
ecs::message::MessageReader,
math::bounding::{Aabb2d, BoundingCircle, BoundingVolume, IntersectsVolume},
prelude::{
default, Bundle, Commands, Component, Entity, Query, ResMut, Resource, Transform, Vec2,
Vec3, With,
},
};
use bevy_quinnet::{
server::{
certificate::CertificateRetrievalMode, ConnectionEvent, EndpointAddrConfiguration,
QuinnetServer, ServerEndpointConfiguration, ServerEndpointConfigurationDefaultables,
},
shared::ClientId,
};
use crate::{
protocol::{
ClientChannel, ClientMessage, PaddleInput, ServerChannel, ServerEvent, ServerSetupMessage,
ServerUpdate,
},
server, BrickId, Velocity, WallLocation, BALL_DIAMETER, 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, LOCAL_BIND_IP, PADDLE_PADDING, PADDLE_SIZE,
PADDLE_SPEED, RIGHT_WALL, SERVER_HOST, SERVER_PORT, TIME_STEP, TOP_WALL, WALL_THICKNESS,
};
const GAP_BETWEEN_PADDLE_AND_BALL: f32 = 35.;
const BALLS_STARTING_POSITION: [Vec3; 2] = [
Vec3::new(
0.0,
BOTTOM_WALL + GAP_BETWEEN_PADDLE_AND_FLOOR + GAP_BETWEEN_PADDLE_AND_BALL,
1.0,
),
Vec3::new(
0.0,
TOP_WALL - GAP_BETWEEN_PADDLE_AND_FLOOR - GAP_BETWEEN_PADDLE_AND_BALL,
1.0,
),
];
const INITIAL_BALLS_DIRECTION: [Vec2; 2] = [Vec2::new(0.5, -0.5), Vec2::new(-0.5, 0.5)];
const PADDLES_STARTING_POSITION: [Vec3; 2] = [
Vec3::new(0.0, BOTTOM_WALL + GAP_BETWEEN_PADDLE_AND_FLOOR, 0.0),
Vec3::new(0.0, TOP_WALL - GAP_BETWEEN_PADDLE_AND_FLOOR, 0.0),
];
#[derive(Debug, Clone, Default)]
pub(crate) struct Player {
input: PaddleInput,
}
#[derive(Resource, Debug, Clone, Default)]
pub(crate) struct Players {
map: HashMap<ClientId, Player>,
}
#[derive(Component)]
pub(crate) struct Paddle {
player_id: ClientId,
}
#[derive(Component)]
pub(crate) struct Brick(BrickId);
#[derive(Component)]
pub(crate) struct Collider;
#[derive(Component)]
pub(crate) struct Ball {
last_hit_by: ClientId,
}
#[derive(Bundle)]
struct WallBundle {
transform: Transform,
collider: Collider,
}
pub(crate) fn start_listening(mut commands: Commands, mut server: ResMut<QuinnetServer>) {
commands.insert_resource(server::Players::default());
server
.start_endpoint(ServerEndpointConfiguration {
addr_config: EndpointAddrConfiguration::from_ip(LOCAL_BIND_IP, SERVER_PORT),
cert_mode: CertificateRetrievalMode::GenerateSelfSigned {
server_hostname: SERVER_HOST.to_string(),
},
defaultables: ServerEndpointConfigurationDefaultables {
send_channels_cfg: ServerChannel::channels_configuration(),
..Default::default()
},
})
.unwrap();
}
pub(crate) fn handle_client_messages(
mut server: ResMut<QuinnetServer>,
mut players: ResMut<Players>,
) {
let endpoint = server.endpoint_mut();
for client_id in endpoint.clients() {
while let Some(message) = endpoint
.try_receive_message_from::<ClientMessage, _>(client_id, ClientChannel::PaddleCommands)
{
match message {
ClientMessage::PaddleInput { input } => {
if let Some(player) = players.map.get_mut(&client_id) {
player.input = input;
}
}
}
}
}
}
pub(crate) fn handle_server_events(
mut commands: Commands,
mut connection_events: MessageReader<ConnectionEvent>,
mut server: ResMut<QuinnetServer>,
mut players: ResMut<Players>,
) {
for client in connection_events.read() {
if players.map.len() >= 2 {
server.endpoint_mut().disconnect_client(client.id).unwrap();
} else {
players.map.insert(
client.id,
Player {
input: PaddleInput::None,
},
);
if players.map.len() == 2 {
start_game(&mut commands, &mut server, &players);
}
}
}
}
pub(crate) fn update_paddles(
mut server: ResMut<QuinnetServer>,
players: ResMut<Players>,
mut paddles: Query<(&mut Transform, &Paddle, Entity)>,
) {
for (mut paddle_transform, paddle, paddle_entity) in paddles.iter_mut() {
if let Some(player) = players.map.get(&paddle.player_id) {
if player.input != PaddleInput::None {
let mut direction = 0.0;
match player.input {
PaddleInput::Left => direction -= 1.0,
PaddleInput::Right => direction = 1.0,
_ => {}
}
let new_paddle_position =
paddle_transform.translation.x + direction * PADDLE_SPEED * TIME_STEP;
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);
server.endpoint_mut().try_send_group_message_on(
players.map.keys(),
ServerChannel::PaddleUpdates,
ServerUpdate::PaddleMoved {
entity: paddle_entity,
position: paddle_transform.translation,
},
);
}
}
}
}
pub(crate) fn check_for_collisions(
mut commands: Commands,
mut server: ResMut<QuinnetServer>,
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_entity, mut ball) in ball_query.iter_mut() {
for (collider_entity, transform, maybe_brick, maybe_paddle) in &collider_query {
let collision = collide_with_side(
BoundingCircle::new(ball_transform.translation.truncate(), BALL_DIAMETER / 2.),
Aabb2d::new(
transform.translation.truncate(),
transform.scale.truncate() / 2.,
),
);
if let Some(collision) = collision {
if let Some(paddle) = maybe_paddle {
ball.last_hit_by = paddle.player_id;
}
let endpoint = server.endpoint_mut();
if let Some(brick) = maybe_brick {
commands.entity(collider_entity).despawn();
endpoint.try_broadcast_message_on(
ServerChannel::GameEvents,
ServerEvent::BrickDestroyed {
by_client_id: ball.last_hit_by,
brick_id: brick.0,
},
);
}
let mut reflect_x = false;
let mut reflect_y = false;
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,
}
if reflect_x {
ball_velocity.x = -ball_velocity.x;
}
if reflect_y {
ball_velocity.y = -ball_velocity.y;
}
endpoint.try_broadcast_message_on(
ServerChannel::GameEvents,
ServerEvent::BallCollided {
owner_client_id: ball.last_hit_by,
entity: ball_entity,
position: ball_transform.translation,
velocity: ball_velocity.0,
},
);
}
}
}
}
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<QuinnetServer>,
players: &ResMut<Players>,
) {
let endpoint = server.endpoint_mut();
for client_id in players.map.keys() {
endpoint
.send_message(
*client_id,
ServerSetupMessage::InitClient {
client_id: *client_id,
},
)
.unwrap();
}
for (position, client_id) in PADDLES_STARTING_POSITION
.iter()
.zip(players.map.keys().into_iter())
{
let paddle = spawn_paddle(commands, *client_id, &position);
endpoint
.send_group_message_on(
players.map.keys(),
ServerChannel::GameSetup,
ServerSetupMessage::SpawnPaddle {
owner_client_id: *client_id,
entity: paddle,
position: *position,
},
)
.unwrap();
}
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, *client_id, position, direction);
endpoint
.send_group_message_on(
players.map.keys(),
ServerChannel::GameSetup,
ServerSetupMessage::SpawnBall {
owner_client_id: *client_id,
entity: ball,
position: *position,
direction: *direction,
},
)
.unwrap();
}
commands.spawn(WallBundle::new(WallLocation::Left));
commands.spawn(WallBundle::new(WallLocation::Right));
commands.spawn(WallBundle::new(WallLocation::Bottom));
commands.spawn(WallBundle::new(WallLocation::Top));
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 =
BOTTOM_WALL + GAP_BETWEEN_PADDLE_AND_FLOOR + GAP_BETWEEN_PADDLE_AND_BRICKS;
let available_height_for_bricks = TOP_WALL
- bottom_edge_of_bricks
- (GAP_BETWEEN_PADDLE_AND_FLOOR + GAP_BETWEEN_PADDLE_AND_BRICKS);
assert!(total_width_of_bricks > 0.0);
assert!(available_height_for_bricks > 0.0);
let n_columns = (total_width_of_bricks / (BRICK_SIZE.x + GAP_BETWEEN_BRICKS)).floor() as usize;
let n_rows =
(available_height_for_bricks / (BRICK_SIZE.y + GAP_BETWEEN_BRICKS)).floor() as usize;
let height_occupied_by_bricks =
n_rows as f32 * (BRICK_SIZE.y + GAP_BETWEEN_BRICKS) - GAP_BETWEEN_BRICKS;
let n_vertical_gaps = n_columns - 1;
let center_of_bricks = (LEFT_WALL + RIGHT_WALL) / 2.0;
let left_edge_of_bricks = center_of_bricks
- (n_columns as f32 / 2.0 * BRICK_SIZE.x)
- n_vertical_gaps as f32 / 2.0 * GAP_BETWEEN_BRICKS;
let offset_x = left_edge_of_bricks + BRICK_SIZE.x / 2.;
let offset_y = bottom_edge_of_bricks
+ BRICK_SIZE.y / 2.
+ (available_height_for_bricks - height_occupied_by_bricks) / 2.;
let mut brick_id = 0;
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),
);
commands.spawn((
Transform {
translation: brick_position.extend(0.0),
scale: Vec3::new(BRICK_SIZE.x, BRICK_SIZE.y, 1.0),
..default()
},
Brick(brick_id),
Collider,
));
brick_id += 1;
}
}
endpoint
.send_group_message(
players.map.keys(),
ServerSetupMessage::SpawnBricks {
offset: Vec2 {
x: offset_x,
y: offset_y,
},
rows: n_rows,
columns: n_columns,
},
)
.unwrap();
endpoint
.send_group_message(players.map.keys(), ServerSetupMessage::StartGame {})
.unwrap();
}
fn spawn_paddle(commands: &mut Commands, client_id: ClientId, pos: &Vec3) -> Entity {
commands
.spawn((
Paddle {
player_id: client_id,
},
Transform {
translation: *pos,
scale: PADDLE_SIZE,
..default()
},
Collider,
))
.id()
}
fn spawn_ball(
commands: &mut Commands,
client_id: ClientId,
pos: &Vec3,
direction: &Vec2,
) -> Entity {
commands
.spawn((
Ball {
last_hit_by: client_id,
},
Transform {
scale: BALL_SIZE,
translation: *pos,
..default()
},
Velocity(direction.normalize() * BALL_SPEED),
))
.id()
}
impl WallBundle {
fn new(location: WallLocation) -> WallBundle {
WallBundle {
transform: Transform {
translation: location.position().extend(0.0),
scale: location.size().extend(1.0),
..default()
},
collider: Collider,
}
}
}
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
enum Collision {
Left,
Right,
Top,
Bottom,
}
fn collide_with_side(ball: BoundingCircle, wall: Aabb2d) -> Option<Collision> {
if !ball.intersects(&wall) {
return None;
}
let closest = wall.closest_point(ball.center());
let offset = ball.center() - closest;
let side = if offset.x.abs() > offset.y.abs() {
if offset.x < 0. {
Collision::Left
} else {
Collision::Right
}
} else if offset.y > 0. {
Collision::Top
} else {
Collision::Bottom
};
Some(side)
}