use std::{
f32::consts::TAU,
net::{IpAddr, Ipv4Addr, SocketAddr, UdpSocket},
time::SystemTime,
};
use bevy::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 fastrand::Rng;
use fastrand_contrib::RngExt;
use serde::{Deserialize, Serialize};
fn main() {
App::new()
.init_resource::<Cli>()
.add_plugins((DefaultPlugins, RepliconPlugins, RepliconRenetPlugins))
.replicate_once::<Boid>()
.replicate_once::<Bias>()
.replicate_once::<Velocity>()
.replicate_once_as::<Transform, Transform2DWithoutScale>()
.add_observer(init_boid)
.add_systems(Startup, setup)
.add_systems(FixedUpdate, update)
.run();
}
const MAX_SPEED: f32 = 6.0;
const MIN_SPEED: f32 = 3.0;
const MAX_BIAS: f32 = 0.01;
const BIAS_INCREMENT: f32 = 0.00004;
fn setup(mut commands: Commands, cli: Res<Cli>, channels: Res<RepliconChannels>) -> Result<()> {
const PROTOCOL_ID: u64 = 0;
commands.spawn(Camera2d);
match *cli {
Cli::Local => {
info!("starting local");
spawn_boids(&mut commands);
}
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((
Text::new("Server"),
TextFont {
font_size: FontSize::Px(30.0),
..Default::default()
},
TextColor::WHITE,
));
spawn_boids(&mut commands);
}
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}")),
TextFont {
font_size: FontSize::Px(30.0),
..default()
},
TextColor::WHITE,
));
}
}
Ok(())
}
fn spawn_boids(commands: &mut Commands) {
let mut rng = Rng::new();
const BOIDS_COUNT: usize = 250;
for _ in 0..BOIDS_COUNT {
let color = Srgba::gray(rng.f32_range(0.4..=1.0)).with_alpha(0.8).into();
let group = match rng.u8(0..=1) {
0 => BoidGroup::LeftSide,
1 => BoidGroup::RightSide,
_ => unreachable!(),
};
let bias = rng.f32_range(BIAS_INCREMENT..=MAX_BIAS);
let velocity = Dir2::NORTH_WEST * rng.f32_range(MIN_SPEED..=MAX_SPEED);
let x = rng.f32_range(-0.250..=250.0);
let y = rng.f32_range(-0.250..=250.0);
let angle = rng.f32_range(0.0..TAU);
commands.spawn((
Boid { color, group },
Replicated,
Bias(bias),
Velocity(velocity),
Transform {
translation: Vec3::new(x, y, 0.0),
rotation: Quat::from_rotation_z(angle),
..Default::default()
},
));
}
}
fn init_boid(
insert: On<Insert, Boid>,
boid_mesh: Local<BoidMesh>,
mut materials: ResMut<Assets<ColorMaterial>>,
mut boids: Query<(&Boid, &mut Mesh2d, &mut MeshMaterial2d<ColorMaterial>)>,
) {
let (boid, mut mesh, mut material) = boids.get_mut(insert.entity).unwrap();
**mesh = boid_mesh.0.clone();
**material = materials.add(boid.color);
}
fn update(
mut cached_boids: Local<Vec<(Vec2, Velocity)>>,
mut boids: Query<(&Boid, &mut Bias, &mut Transform, &mut Velocity)>,
) {
cached_boids.clear();
cached_boids.reserve(boids.iter().len());
cached_boids.extend(
boids
.iter()
.map(|(.., t, &v)| (t.translation.truncate(), v)),
);
const VISUAL_RANGE: f32 = 40.0;
const PROTECTED_RANGE: f32 = 8.0;
const CENTERING_FACTOR: f32 = 0.0005;
const MATCHING_FACTOR: f32 = 0.05;
const AVOID_FACTOR: f32 = 0.05;
const TURN_FACTOR: f32 = 0.2;
const HALF_AREA: Vec2 = Vec2::new(640.0, 360.0);
const MARGIN: f32 = 100.0;
for (index, (boid, mut bias, mut transform, mut velocity)) in boids.iter_mut().enumerate() {
let position = transform.translation.truncate();
let mut close_diff = Vec2::ZERO;
let mut position_avg = Vec2::ZERO;
let mut velocity_avg = Vec2::ZERO;
let mut neighbors = 0;
for (other_index, &(other_position, other_velocity)) in cached_boids.iter().enumerate() {
if index == other_index {
continue;
}
let diff = position - other_position;
if diff.x.abs() < VISUAL_RANGE && diff.y.abs() < VISUAL_RANGE {
let len_squared = diff.length_squared();
if len_squared < PROTECTED_RANGE * PROTECTED_RANGE {
close_diff += diff;
} else if len_squared < VISUAL_RANGE * VISUAL_RANGE {
position_avg += other_position;
velocity_avg += *other_velocity;
neighbors += 1;
}
}
}
if neighbors > 0 {
position_avg /= neighbors as f32;
velocity_avg /= neighbors as f32;
let centering = (position_avg - position) * CENTERING_FACTOR;
let matching = (velocity_avg - **velocity) * MATCHING_FACTOR;
**velocity += centering + matching;
}
**velocity += close_diff * AVOID_FACTOR;
if position.x <= -HALF_AREA.x + MARGIN {
velocity.x += TURN_FACTOR;
} else if position.x >= HALF_AREA.x - MARGIN {
velocity.x -= TURN_FACTOR;
}
if position.y <= -HALF_AREA.y + MARGIN {
velocity.y += TURN_FACTOR;
} else if position.y >= HALF_AREA.y - MARGIN {
velocity.y -= TURN_FACTOR;
}
let sign = match boid.group {
BoidGroup::LeftSide => -1.0,
BoidGroup::RightSide => 1.0,
};
if sign * velocity.x > 0.0 {
**bias = MAX_BIAS.min(**bias + BIAS_INCREMENT)
} else {
**bias = BIAS_INCREMENT.max(**bias - BIAS_INCREMENT)
}
velocity.x = (1.0 - **bias) * velocity.x + sign * **bias;
let speed = velocity.length();
if speed < MIN_SPEED {
**velocity = **velocity / speed * MIN_SPEED;
} else if speed > MAX_SPEED {
**velocity = **velocity / speed * MAX_SPEED;
}
transform.translation.x += velocity.x;
transform.translation.y += velocity.y;
transform.rotation = Quat::from_rotation_z(velocity.to_angle());
}
}
const DEFAULT_PORT: u16 = 5000;
#[derive(Parser, PartialEq, Resource)]
enum Cli {
Local,
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(Mesh2d, MeshMaterial2d<ColorMaterial>)]
#[component(immutable)]
struct Boid {
color: Color,
group: BoidGroup,
}
#[derive(Serialize, Deserialize)]
enum BoidGroup {
LeftSide,
RightSide,
}
#[derive(Component, Serialize, Deserialize, Deref, DerefMut)]
struct Bias(f32);
#[derive(Component, Deref, DerefMut, Serialize, Deserialize, Clone, Copy)]
struct Velocity(Vec2);
struct BoidMesh(Handle<Mesh>);
impl FromWorld for BoidMesh {
fn from_world(world: &mut World) -> Self {
let triangle = Triangle2d::new(
Vec2::new(8.0, 0.0),
Vec2::new(-5.0, 5.0),
Vec2::new(-5.0, -5.0),
);
let mut meshes = world.resource_mut::<Assets<Mesh>>();
let mesh = meshes.add(triangle);
Self(mesh)
}
}
#[derive(Serialize, Deserialize, Clone, Copy)]
struct Transform2DWithoutScale {
translation: Vec2,
rotation: Quat,
}
impl From<Transform> for Transform2DWithoutScale {
fn from(value: Transform) -> Self {
Self {
translation: value.translation.truncate(),
rotation: value.rotation,
}
}
}
impl From<Transform2DWithoutScale> for Transform {
fn from(value: Transform2DWithoutScale) -> Self {
Self {
translation: value.translation.extend(0.0),
rotation: value.rotation,
..Default::default()
}
}
}