use std::{
error::Error,
net::{IpAddr, Ipv4Addr},
};
use bevy::{
prelude::*,
winit::{UpdateMode::Continuous, WinitSettings},
};
use bevy_quinnet::{
client::{
certificate::CertificateVerificationMode, connection::ClientEndpointConfiguration,
QuinnetClient,
},
server::{certificate::CertificateRetrievalMode, QuinnetServer, ServerEndpointConfiguration},
};
use bevy_replicon::prelude::*;
use bevy_replicon_quinnet::{ChannelsConfigurationExt, RepliconQuinnetPlugins};
use clap::Parser;
use serde::{Deserialize, Serialize};
const PORT: u16 = 5000;
fn main() {
App::new()
.init_resource::<Cli>() .insert_resource(WinitSettings {
focused_mode: Continuous,
unfocused_mode: Continuous,
})
.add_plugins((
DefaultPlugins,
RepliconPlugins,
RepliconQuinnetPlugins,
SimpleBoxPlugin,
))
.run();
}
struct SimpleBoxPlugin;
impl Plugin for SimpleBoxPlugin {
fn build(&self, app: &mut App) {
app.replicate::<PlayerPosition>()
.replicate::<PlayerColor>()
.add_client_event::<MoveDirection>(ChannelKind::Ordered)
.add_systems(
Startup,
(Self::read_cli.map(Result::unwrap), Self::spawn_camera),
)
.add_systems(
Update,
(
Self::apply_movement.run_if(has_authority), Self::handle_connections.run_if(server_running), (Self::draw_boxes, Self::read_input),
),
);
}
}
impl SimpleBoxPlugin {
fn read_cli(
mut commands: Commands,
cli: Res<Cli>,
channels: Res<RepliconChannels>,
mut server: ResMut<QuinnetServer>,
mut client: ResMut<QuinnetClient>,
) -> Result<(), Box<dyn Error>> {
match *cli {
Cli::SinglePlayer => {
commands.spawn(PlayerBundle::new(
ClientId::SERVER,
Vec2::ZERO,
Color::GREEN,
));
}
Cli::Server { port } => {
server
.start_endpoint(
ServerEndpointConfiguration::from_ip(IpAddr::V4(Ipv4Addr::LOCALHOST), port),
CertificateRetrievalMode::GenerateSelfSigned {
server_hostname: Ipv4Addr::LOCALHOST.to_string(),
},
channels.get_server_configs(),
)
.unwrap();
commands.spawn(TextBundle::from_section(
"Server",
TextStyle {
font_size: 30.0,
color: Color::WHITE,
..default()
},
));
commands.spawn(PlayerBundle::new(
ClientId::SERVER,
Vec2::ZERO,
Color::GREEN,
));
}
Cli::Client { port, ip } => {
client
.open_connection(
ClientEndpointConfiguration::from_ips(
ip,
port,
IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)),
0,
),
CertificateVerificationMode::SkipVerification,
channels.get_client_configs(),
)
.unwrap();
commands.spawn(TextBundle::from_section(
"Client",
TextStyle {
font_size: 30.0,
color: Color::WHITE,
..default()
},
));
}
}
Ok(())
}
fn spawn_camera(mut commands: Commands) {
commands.spawn(Camera2dBundle::default());
}
fn handle_connections(mut commands: Commands, mut server_events: EventReader<ServerEvent>) {
for event in server_events.read() {
match event {
ServerEvent::ClientConnected { client_id } => {
info!("{client_id:?} connected");
let r = ((client_id.get() % 23) as f32) / 23.0;
let g = ((client_id.get() % 27) as f32) / 27.0;
let b = ((client_id.get() % 39) as f32) / 39.0;
commands.spawn(PlayerBundle::new(
*client_id,
Vec2::ZERO,
Color::rgb(r, g, b),
));
}
ServerEvent::ClientDisconnected { client_id, reason } => {
info!("{client_id:?} disconnected: {reason}");
}
}
}
}
fn draw_boxes(mut gizmos: Gizmos, players: Query<(&PlayerPosition, &PlayerColor)>) {
for (position, color) in &players {
gizmos.rect(
Vec3::new(position.x, position.y, 0.0),
Quat::IDENTITY,
Vec2::ONE * 50.0,
color.0,
);
}
}
fn read_input(mut move_events: EventWriter<MoveDirection>, input: Res<ButtonInput<KeyCode>>) {
let mut direction = Vec2::ZERO;
if input.pressed(KeyCode::ArrowRight) {
direction.x += 1.0;
}
if input.pressed(KeyCode::ArrowLeft) {
direction.x -= 1.0;
}
if input.pressed(KeyCode::ArrowUp) {
direction.y += 1.0;
}
if input.pressed(KeyCode::ArrowDown) {
direction.y -= 1.0;
}
if direction != Vec2::ZERO {
move_events.send(MoveDirection(direction.normalize_or_zero()));
}
}
fn apply_movement(
time: Res<Time>,
mut move_events: EventReader<FromClient<MoveDirection>>,
mut players: Query<(&Player, &mut PlayerPosition)>,
) {
const MOVE_SPEED: f32 = 300.0;
for FromClient { client_id, event } in move_events.read() {
info!("received event {event:?} from {client_id:?}");
for (player, mut position) in &mut players {
if *client_id == player.0 {
**position += event.0 * time.delta_seconds() * MOVE_SPEED;
}
}
}
}
}
#[derive(Parser, PartialEq, Resource)]
enum Cli {
SinglePlayer,
Server {
#[arg(short, long, default_value_t = PORT)]
port: u16,
},
Client {
#[arg(short, long, default_value_t = Ipv4Addr::LOCALHOST.into())]
ip: IpAddr,
#[arg(short, long, default_value_t = PORT)]
port: u16,
},
}
impl Default for Cli {
fn default() -> Self {
Self::parse()
}
}
#[derive(Bundle)]
struct PlayerBundle {
player: Player,
position: PlayerPosition,
color: PlayerColor,
replicated: Replicated,
}
impl PlayerBundle {
fn new(client_id: ClientId, position: Vec2, color: Color) -> Self {
Self {
player: Player(client_id),
position: PlayerPosition(position),
color: PlayerColor(color),
replicated: Replicated,
}
}
}
#[derive(Component, Serialize, Deserialize)]
struct Player(ClientId);
#[derive(Component, Deserialize, Serialize, Deref, DerefMut)]
struct PlayerPosition(Vec2);
#[derive(Component, Deserialize, Serialize)]
struct PlayerColor(Color);
#[derive(Debug, Default, Deserialize, Event, Serialize)]
struct MoveDirection(Vec2);