Compare commits

...

3 commits

Author SHA1 Message Date
dad37262a5
Implement very basic and bad and specific netcode
All checks were successful
CI / Formatting (push) Successful in 1m14s
The game can now retrieve its seed from another game on the network
2025-05-25 00:54:16 -04:00
50ef78f7aa
Move seed check to FixedUpdate 2025-05-24 22:28:15 -04:00
4a097e7ed8
Scope GameObjects for automatic cleanup 2025-05-24 22:27:44 -04:00
8 changed files with 124 additions and 7 deletions

2
.vscode/launch.json vendored
View file

@ -11,7 +11,7 @@
"cargo": {
"args": ["build"]
},
"args": ["--seed=gargamel"],
"args": ["--seed=:)"],
"cwd": "${workspaceFolder}",
"env": {
"CARGO_MANIFEST_DIR": "${workspaceFolder}",

2
.vscode/tasks.json vendored
View file

@ -35,7 +35,7 @@
"problemMatcher": ["$rustc"],
"group": {
"kind": "build",
"isDefault": true
"isDefault": false
}
}
]

1
Cargo.lock generated
View file

@ -1921,6 +1921,7 @@ dependencies = [
"bevy",
"bevy_rand",
"clap",
"crossbeam-channel",
"log",
"rand 0.9.1",
"wyrand",

View file

@ -36,6 +36,7 @@ bevy = { version = "0.16.0", default-features = false, features = [
] }
bevy_rand = { version = "0.11.0", features = ["wyrand", "std"] }
clap = { version = "4.5.32", features = ["derive"] }
crossbeam-channel = "0.5.15"
log = { version = "*", features = [
"max_level_debug",
"release_max_level_warn",

View file

@ -1,9 +1,11 @@
use avian2d::prelude::*;
use bevy::prelude::*;
use crate::AppState;
/// Basic implementation of a physics object
#[derive(Component, Default)]
#[require(Collider, Mesh2d, MeshMaterial2d<ColorMaterial>, Restitution = Restitution::new(1.0), RigidBody, TransformInterpolation, Transform)]
#[require(Collider, Mesh2d, MeshMaterial2d<ColorMaterial>, Restitution = Restitution::new(1.0), RigidBody, TransformInterpolation, Transform, StateScoped<AppState> = StateScoped(AppState::InGame))]
struct GameObject;
/// Radius of a ball

View file

@ -8,6 +8,7 @@ use rand::random;
pub struct Seed(u64);
impl Seed {
/// Use a random integer as the seed
pub fn random() -> Self {
Self(random())
}
@ -30,3 +31,15 @@ impl From<Seed> for [u8; 8] {
value.0.to_le_bytes()
}
}
impl From<u64> for Seed {
fn from(value: u64) -> Self {
Seed(value)
}
}
impl From<Seed> for u64 {
fn from(value: Seed) -> Self {
value.0
}
}

View file

@ -14,6 +14,8 @@ use game::{
setup::{check_for_seed, setup_balls, setup_from_seed, setup_player, setup_ui, setup_walls},
};
mod net;
/// The initial configuration passed to the game's setup functions.
/// Also functions as a Bevy plugin to pass the configuration into the app.
#[derive(Parser)]
@ -40,6 +42,7 @@ struct Source {
}
#[derive(States, Default, Debug, Clone, PartialEq, Eq, Hash)]
#[states(scoped_entities)]
enum AppState {
#[default]
Loading,
@ -62,6 +65,7 @@ impl Plugin for AppSettings {
PhysicsPlugins::default().with_length_unit(50.0),
#[cfg(feature = "dev")]
dev::dev_tools,
net::NetIOPlugin::new(self.port, self.source.connect),
))
.init_state::<AppState>()
.add_systems(Startup, setup_ui)
@ -72,11 +76,14 @@ impl Plugin for AppSettings {
(setup_player, setup_balls, setup_walls).after(setup_from_seed),
),
)
.add_systems(
FixedUpdate,
check_for_seed.run_if(in_state(AppState::Loading)),
)
.add_systems(
Update,
(
check_for_seed.run_if(in_state(AppState::Loading)),
(move_player, move_camera.after(move_player), zoom_camera)
((move_player, move_camera).chain(), zoom_camera)
.run_if(in_state(AppState::InGame)),
quit.run_if(input_pressed(KeyCode::KeyQ)),
),
@ -85,8 +92,7 @@ impl Plugin for AppSettings {
if let Some(ref seed) = self.source.seed {
app.insert_resource(seed.clone());
} else if let Some(ref peer) = self.source.connect {
info!("Got peer: {peer}");
todo!("Handle connecting to peer and retrieving seed");
info!("Will retrieve seed from peer => {peer}");
} else {
app.insert_resource(Seed::random());
}

94
src/net.rs Normal file
View file

@ -0,0 +1,94 @@
use std::{
net::{Ipv6Addr, SocketAddr, UdpSocket},
thread,
};
use bevy::prelude::*;
use crossbeam_channel::{Receiver, Sender, unbounded};
use crate::game::seed::Seed;
#[derive(Resource)]
pub struct NetworkSend(Sender<(u64, SocketAddr)>);
#[derive(Resource)]
pub struct NetworkReceive(Receiver<(u64, SocketAddr)>);
fn handle_network_io(
receive: Res<NetworkReceive>,
send: Res<NetworkSend>,
seed: Option<Res<Seed>>,
mut commands: Commands,
) -> Result {
let Ok((message, address)) = receive.0.try_recv() else {
return Ok(());
};
if let Some(value) = seed {
send.0.try_send((value.clone().into(), address))?;
} else {
commands.insert_resource::<Seed>(message.into());
}
Ok(())
}
pub struct NetIOPlugin {
listen: u16,
peer: Option<SocketAddr>,
}
impl NetIOPlugin {
pub fn new(listen: u16, peer: Option<SocketAddr>) -> Self {
Self { listen, peer }
}
}
impl Plugin for NetIOPlugin {
fn build(&self, app: &mut App) {
app.add_systems(FixedUpdate, handle_network_io);
let (send, receive) = match UdpSocket::bind((Ipv6Addr::LOCALHOST, self.listen)) {
Ok(socket) => {
socket.set_read_timeout(None).unwrap();
socket.set_write_timeout(None).unwrap();
let (send_outbound, receive_outbound) = unbounded::<(u64, SocketAddr)>();
let send_socket = socket.try_clone().unwrap();
thread::spawn(move || {
loop {
match receive_outbound.recv() {
Ok((message, address)) => send_socket
.send_to(&message.to_le_bytes(), address)
.unwrap(),
Err(err) => {
error!("{err}");
break;
}
};
}
});
let (send_inbound, receive_inbound) = unbounded::<(u64, SocketAddr)>();
thread::spawn(move || {
loop {
let mut message = [0u8; 8];
let (len, address) = socket.recv_from(&mut message).unwrap();
info!("Received {len} bytes");
send_inbound
.try_send((u64::from_le_bytes(message), address))
.unwrap();
}
});
(send_outbound, receive_inbound)
}
Err(err) => {
error!("Could not bind socket: {err}");
todo!("bounded(0) is apparently meaningful so find another solution")
}
};
if let Some(socket) = self.peer {
send.try_send((0, socket)).unwrap();
}
app.insert_resource(NetworkSend(send));
app.insert_resource(NetworkReceive(receive));
}
}