use std::hash::{DefaultHasher, Hash, Hasher};
use std::net::{Ipv4Addr, SocketAddrV4};
use bevy::log::{Level, LogPlugin};
use bevy::{
color::palettes::css::GREEN,
prelude::*,
winit::{UpdateMode::Continuous, WinitSettings},
};
use bevy_matchbox::matchbox_signaling::SignalingServer;
use bevy_replicon::prelude::*;
use bevy_replicon_matchbox::{MatchboxClient, MatchboxHost, RepliconMatchboxPlugins};
use clap::Parser;
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, Default, Deserialize, Event, Serialize)]
struct ExampleEvent {
pub i: usize
}
fn main() {
let log_plugin = LogPlugin {
level: Level::INFO,
filter: "bevy_replicon_matchbox=debug,wgpu=error,bevy_matchbox=error,webrtc_ice=error"
.into(),
..default()
};
let mut app =App::new();
app .init_resource::<Cli>() .insert_resource(WinitSettings {
focused_mode: Continuous,
unfocused_mode: Continuous,
})
.add_plugins((
DefaultPlugins.set(log_plugin),
RepliconPlugins,
RepliconMatchboxPlugins,
))
.replicate::<BoxPosition>()
.replicate::<PlayerBox>()
.add_client_trigger::<MoveBox>(Channel::Ordered)
.add_observer(spawn_clients)
.add_observer(despawn_clients)
.add_observer(apply_movement)
.add_systems(Startup, (read_cli, spawn_camera))
.add_systems(Update, (read_input, draw_boxes));
app.run();
}
fn read_cli(mut commands: Commands, cli: Res<Cli>, channels: Res<RepliconChannels>) -> Result<()> {
match *cli {
Cli::SinglePlayer => {
info!("starting single-player game");
commands.spawn((
PlayerBox {
color: GREEN.into(),
},
BoxOwner(SERVER),
));
}
Cli::Server { port } => {
info!("starting server at port {port}");
start_signaling_server(&mut commands, port);
let room_url = format!("ws://localhost:{port}/simple-box");
let server = MatchboxHost::new(room_url, &channels)?;
commands.insert_resource(server);
commands.spawn((
Text::new("Server"),
TextFont {
font_size: 30.0,
..Default::default()
},
TextColor::WHITE,
));
commands.spawn((
PlayerBox {
color: GREEN.into(),
},
BoxOwner(SERVER),
));
}
Cli::Client { port } => {
info!("connecting to port {port}");
let room_url = format!("ws://localhost:{port}/simple-box");
let client = MatchboxClient::new(room_url, &channels)?;
commands.insert_resource(client);
commands.spawn((
Text(format!("Client")),
TextFont {
font_size: 30.0,
..default()
},
TextColor::WHITE,
));
}
}
Ok(())
}
fn start_signaling_server(commands: &mut Commands, port: u16) {
info!("Starting signaling server on port {port}");
let addr = SocketAddrV4::new(Ipv4Addr::LOCALHOST, port);
let signaling_server = bevy_matchbox::MatchboxServer::from(
SignalingServer::client_server_builder(addr)
.on_connection_request(|connection| {
info!("Connecting: {connection:?}");
Ok(true) })
.on_id_assignment(|(socket, id)| info!("{socket} received {id}"))
.on_host_connected(|id| info!("Host joined: {id}"))
.on_host_disconnected(|id| info!("Host left: {id}"))
.on_client_connected(|id| info!("Client joined: {id}"))
.on_client_disconnected(|id| info!("Client left: {id}"))
.cors()
.trace()
.build(),
);
commands.insert_resource(signaling_server);
}
fn spawn_camera(mut commands: Commands) {
commands.spawn(Camera2d);
}
fn spawn_clients(trigger: Trigger<OnAdd, ConnectedClient>, mut commands: Commands) {
let mut hasher = DefaultHasher::new();
trigger.target().index().hash(&mut hasher);
let hash = hasher.finish();
let r = ((hash >> 16) & 0xFF) as f32 / 255.0;
let g = ((hash >> 8) & 0xFF) as f32 / 255.0;
let b = (hash & 0xFF) as f32 / 255.0;
info!("spawning box for `{}`", trigger.target());
commands.spawn((
PlayerBox {
color: Color::srgb(r, g, b),
},
BoxOwner(trigger.target()),
));
}
fn despawn_clients(
trigger: Trigger<OnRemove, ConnectedClient>,
mut commands: Commands,
boxes: Query<(Entity, &BoxOwner)>,
) {
let (entity, _) = boxes
.iter()
.find(|&(_, owner)| **owner == trigger.target())
.expect("all clients should have entities");
commands.entity(entity).despawn();
}
fn read_input(mut commands: Commands, input: Res<ButtonInput<KeyCode>>) {
let mut direction = Vec2::ZERO;
if input.pressed(KeyCode::KeyW) {
direction.y += 1.0;
}
if input.pressed(KeyCode::KeyA) {
direction.x -= 1.0;
}
if input.pressed(KeyCode::KeyS) {
direction.y -= 1.0;
}
if input.pressed(KeyCode::KeyD) {
direction.x += 1.0;
}
if direction != Vec2::ZERO {
commands.client_trigger(MoveBox(direction.normalize_or_zero()));
}
}
fn apply_movement(
trigger: Trigger<FromClient<MoveBox>>,
time: Res<Time>,
mut boxes: Query<(&BoxOwner, &mut BoxPosition)>,
) {
const MOVE_SPEED: f32 = 300.0;
let (_, mut position) = boxes
.iter_mut()
.find(|&(owner, _)| **owner == trigger.client_entity)
.unwrap_or_else(|| panic!("`{}` should be connected", trigger.client_entity));
**position += *trigger.event * time.delta_secs() * MOVE_SPEED;
}
fn draw_boxes(mut gizmos: Gizmos, boxes: Query<(&BoxPosition, &PlayerBox)>) {
for (position, player) in &boxes {
gizmos.rect(
Vec3::new(position.x, position.y, 0.0),
Vec2::ONE * 50.0,
player.color,
);
}
}
const PORT: u16 = 5000;
#[derive(Parser, PartialEq, Resource)]
enum Cli {
SinglePlayer,
Server {
#[arg(short, long, default_value_t = PORT)]
port: u16,
},
Client {
#[arg(short, long, default_value_t = PORT)]
port: u16,
},
}
impl Default for Cli {
fn default() -> Self {
Self::parse()
}
}
#[derive(Component, Deref, Deserialize, Serialize, Default)]
#[require(BoxPosition, Replicated)]
struct PlayerBox {
color: Color,
}
#[derive(Component, Deserialize, Serialize, Deref, DerefMut, Default)]
struct BoxPosition(Vec2);
#[derive(Component, Clone, Copy, Deref)]
struct BoxOwner(Entity);
#[derive(Deserialize, Deref, Event, Serialize)]
struct MoveBox(Vec2);