//! A simple demo to showcase how player could send inputs to move the square and server replicates position back. //! Also demonstrates the single-player and how sever also could be a player. use std::{ error::Error, net::{IpAddr, Ipv4Addr, SocketAddr, UdpSocket}, time::SystemTime, }; use bevy::{ color::palettes::css::GREEN, prelude::*, winit::{UpdateMode::Continuous, WinitSettings}, }; use bevy_replicon::prelude::*; use bevy_replicon_renet::{ renet::{ transport::{ ClientAuthentication, NetcodeClientTransport, NetcodeServerTransport, ServerAuthentication, ServerConfig, }, ConnectionConfig, RenetClient, RenetServer, }, RenetChannelsExt, RepliconRenetPlugins, }; use clap::Parser; use serde::{Deserialize, Serialize}; fn main() { App::new() .init_resource::() // Parse CLI before creating window. // Makes the server/client update continuously even while unfocused. .insert_resource(WinitSettings { focused_mode: Continuous, unfocused_mode: Continuous, }) .add_plugins(( DefaultPlugins, RepliconPlugins, RepliconRenetPlugins, SimpleBoxPlugin, )) .run(); } struct SimpleBoxPlugin; impl Plugin for SimpleBoxPlugin { fn build(&self, app: &mut App) { app.replicate::() .replicate::() .add_client_event::(ChannelKind::Ordered) .add_systems( Startup, (Self::read_cli.map(Result::unwrap), Self::spawn_camera), ) .add_systems( Update, ( Self::apply_movement.run_if(has_authority), // Runs only on the server or a single player. Self::handle_connections.run_if(server_running), // Runs only on the server. (Self::draw_boxes, Self::read_input), ), ); } } impl SimpleBoxPlugin { fn read_cli( mut commands: Commands, cli: Res, channels: Res, ) -> Result<(), Box> { match *cli { Cli::SinglePlayer => { commands.spawn(PlayerBundle::new( ClientId::SERVER, Vec2::ZERO, GREEN.into(), )); } Cli::Server { port } => { let server_channels_config = channels.get_server_configs(); let client_channels_config = channels.get_client_configs(); let server = RenetServer::new(ConnectionConfig { server_channels_config, client_channels_config, ..Default::default() }); let current_time = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?; let socket = UdpSocket::bind((Ipv4Addr::UNSPECIFIED, port))?; let server_config = ServerConfig { current_time, max_clients: 10, protocol_id: PROTOCOL_ID, authentication: ServerAuthentication::Unsecure, public_addresses: Default::default(), }; let transport = NetcodeServerTransport::new(server_config, socket)?; commands.insert_resource(server); commands.insert_resource(transport); commands.spawn(TextBundle::from_section( "Server", TextStyle { font_size: 30.0, color: Color::WHITE, ..default() }, )); commands.spawn(PlayerBundle::new( ClientId::SERVER, Vec2::ZERO, GREEN.into(), )); } Cli::Client { port, ip } => { let server_channels_config = channels.get_server_configs(); let client_channels_config = channels.get_client_configs(); let client = RenetClient::new(ConnectionConfig { server_channels_config, client_channels_config, ..Default::default() }); let current_time = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?; let client_id = current_time.as_millis() as u64; let server_addr = SocketAddr::new(ip, port); let socket = UdpSocket::bind((Ipv4Addr::UNSPECIFIED, 0))?; let authentication = ClientAuthentication::Unsecure { client_id, protocol_id: PROTOCOL_ID, server_addr, user_data: None, }; let transport = NetcodeClientTransport::new(current_time, authentication, socket)?; commands.insert_resource(client); commands.insert_resource(transport); commands.spawn(TextBundle::from_section( format!("Client: {client_id:?}"), TextStyle { font_size: 30.0, color: Color::WHITE, ..default() }, )); } } Ok(()) } fn spawn_camera(mut commands: Commands) { commands.spawn(Camera2dBundle::default()); } /// Logs server events and spawns a new player whenever a client connects. fn handle_connections(mut commands: Commands, mut server_events: EventReader) { for event in server_events.read() { match event { ServerEvent::ClientConnected { client_id } => { info!("{client_id:?} connected"); // Generate pseudo random color from client id. 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::srgb(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, ); } } /// Reads player inputs and sends [`MoveDirection`] events. fn read_input(mut move_events: EventWriter, input: Res>) { 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())); } } /// Mutates [`PlayerPosition`] based on [`MoveDirection`] events. /// /// Fast-paced games usually you don't want to wait until server send a position back because of the latency. /// But this example just demonstrates simple replication concept. fn apply_movement( time: Res