use std::{
net::{IpAddr, Ipv4Addr, SocketAddr, UdpSocket},
time::SystemTime,
};
use bevy::{ecs::entity::MapEntities, prelude::*};
use bevy_replicon::prelude::*;
use bevy_replicon_renet::{
RenetChannelsExt, RenetClient, RenetServer, RepliconRenetPlugins,
netcode::{
ClientAuthentication, NetcodeClientTransport, NetcodeServerTransport, ServerAuthentication,
ServerConfig,
},
renet::ConnectionConfig,
};
use clap::Parser;
use serde::{Deserialize, Serialize};
fn main() {
App::new()
.init_resource::<Cli>() .add_plugins((DefaultPlugins, RepliconPlugins, RepliconRenetPlugins))
.replicate::<UiRoot>()
.replicate::<ToggleButton>()
.replicate_filtered::<ChildOf, With<ToggleButton>>() .add_mapped_client_event::<RemoteToggle>(Channel::Unordered)
.add_observer(init_toggle_button)
.add_observer(trigger_remote_toggle)
.add_observer(apply_remote_toggle)
.add_systems(Startup, setup)
.add_systems(Update, (update_button_background, update_toggle_text))
.run();
}
fn setup(mut commands: Commands, cli: Res<Cli>, channels: Res<RepliconChannels>) -> Result<()> {
const PROTOCOL_ID: u64 = 0;
commands.spawn(Camera2d);
match *cli {
Cli::SinglePlayer => {
info!("starting single-player game");
commands.spawn((UiRoot, children![ToggleButton(false)]));
}
Cli::Server { port } => {
info!("starting server at port {port}");
let server_channels_config = channels.server_configs();
let client_channels_config = channels.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: 1,
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((
UiRoot,
Replicated,
children![(ToggleButton(false), Replicated)],
));
commands.spawn(Text::new("Server"));
}
Cli::Client { ip, port } => {
info!("connecting to {ip}:{port}");
let server_channels_config = channels.server_configs();
let client_channels_config = channels.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 addr = socket.local_addr()?;
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(Text(format!("Client: {addr}")));
}
}
Ok(())
}
fn init_toggle_button(add: On<Add, ToggleButton>, mut commands: Commands) {
commands.entity(add.entity).with_child((
Text::default(),
TextShadow::default(),
TextFont {
font_size: FontSize::Px(30.0),
..Default::default()
},
));
}
fn trigger_remote_toggle(
click: On<Pointer<Click>>,
mut commands: Commands,
buttons: Query<(), With<ToggleButton>>,
) {
if buttons.contains(click.entity) {
commands.client_trigger(RemoteToggle {
entity: click.entity,
});
}
}
fn apply_remote_toggle(
toggle: On<FromClient<RemoteToggle>>,
mut buttons: Query<&mut ToggleButton>,
) {
if let Ok(mut toggle) = buttons.get_mut(toggle.entity) {
**toggle = !**toggle
}
}
fn update_button_background(
mut buttons: Query<(&Interaction, &mut BackgroundColor), (Changed<Interaction>, With<Button>)>,
) {
for (interaction, mut background_color) in &mut buttons {
*background_color = match interaction {
Interaction::Pressed => Color::srgb(0.35, 0.75, 0.35).into(),
Interaction::Hovered => Color::srgb(0.25, 0.25, 0.25).into(),
Interaction::None => Color::srgb(0.15, 0.15, 0.15).into(),
}
}
}
fn update_toggle_text(
buttons: Query<(&ToggleButton, &Children), Changed<ToggleButton>>,
mut texts: Query<&mut Text>,
) {
for (&toggle, children) in buttons {
if let Some(mut text) = texts.iter_many_mut(children).fetch_next() {
text.clear();
if *toggle {
text.push_str("On");
} else {
text.push_str("Off");
}
}
}
}
const DEFAULT_PORT: u16 = 5000;
#[derive(Parser, PartialEq, Resource)]
enum Cli {
SinglePlayer,
Server {
#[arg(short, long, default_value_t = DEFAULT_PORT)]
port: u16,
},
Client {
#[arg(short, long, default_value_t = Ipv4Addr::LOCALHOST.into())]
ip: IpAddr,
#[arg(short, long, default_value_t = DEFAULT_PORT)]
port: u16,
},
}
impl Default for Cli {
fn default() -> Self {
Self::parse()
}
}
#[derive(Component, Serialize, Deserialize)]
#[require(
Node {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..Default::default()
}
)]
struct UiRoot;
#[derive(Component, Serialize, Deserialize, Deref, DerefMut, Clone, Copy)]
#[require(
Button,
Node {
width: Val::Px(150.0),
height: Val::Px(65.0),
border: UiRect::all(Val::Px(5.0)),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
border_radius: BorderRadius::MAX,
..Default::default()
},
BorderColor::all(Color::WHITE),
)]
struct ToggleButton(bool);
#[derive(EntityEvent, MapEntities, Serialize, Deserialize, Clone, Copy)]
struct RemoteToggle {
#[entities]
entity: Entity,
}