From 4a097e7ed8323ac2cf973bfea21f86f9281d3c2b Mon Sep 17 00:00:00 2001 From: Michael Bradley Date: Sat, 24 May 2025 22:27:44 -0400 Subject: [PATCH 1/3] Scope GameObjects for automatic cleanup --- src/game/objects.rs | 4 +++- src/lib.rs | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/game/objects.rs b/src/game/objects.rs index da4f096..1ede2d5 100644 --- a/src/game/objects.rs +++ b/src/game/objects.rs @@ -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, Restitution = Restitution::new(1.0), RigidBody, TransformInterpolation, Transform)] +#[require(Collider, Mesh2d, MeshMaterial2d, Restitution = Restitution::new(1.0), RigidBody, TransformInterpolation, Transform, StateScoped = StateScoped(AppState::InGame))] struct GameObject; /// Radius of a ball diff --git a/src/lib.rs b/src/lib.rs index 113b8b6..be09d25 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -40,6 +40,7 @@ struct Source { } #[derive(States, Default, Debug, Clone, PartialEq, Eq, Hash)] +#[states(scoped_entities)] enum AppState { #[default] Loading, From 50ef78f7aa4f2a588b55c1d1999d00c2564e8cf6 Mon Sep 17 00:00:00 2001 From: Michael Bradley Date: Sat, 24 May 2025 22:28:15 -0400 Subject: [PATCH 2/3] Move seed check to FixedUpdate --- src/lib.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index be09d25..52b2a2c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -73,10 +73,13 @@ 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) .run_if(in_state(AppState::InGame)), quit.run_if(input_pressed(KeyCode::KeyQ)), From dad37262a55a5a0aacc5e6a3589b709814db59c5 Mon Sep 17 00:00:00 2001 From: Michael Bradley Date: Sun, 25 May 2025 00:54:16 -0400 Subject: [PATCH 3/3] Implement very basic and bad and specific netcode The game can now retrieve its seed from another game on the network --- .vscode/launch.json | 2 +- .vscode/tasks.json | 2 +- Cargo.lock | 1 + Cargo.toml | 1 + src/game/seed.rs | 13 +++++++ src/lib.rs | 8 ++-- src/net.rs | 94 +++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 116 insertions(+), 5 deletions(-) create mode 100644 src/net.rs diff --git a/.vscode/launch.json b/.vscode/launch.json index f4dd30c..076d413 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -11,7 +11,7 @@ "cargo": { "args": ["build"] }, - "args": ["--seed=gargamel"], + "args": ["--seed=:)"], "cwd": "${workspaceFolder}", "env": { "CARGO_MANIFEST_DIR": "${workspaceFolder}", diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 779bf54..b346e26 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -35,7 +35,7 @@ "problemMatcher": ["$rustc"], "group": { "kind": "build", - "isDefault": true + "isDefault": false } } ] diff --git a/Cargo.lock b/Cargo.lock index 1af9a77..3dc023e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1921,6 +1921,7 @@ dependencies = [ "bevy", "bevy_rand", "clap", + "crossbeam-channel", "log", "rand 0.9.1", "wyrand", diff --git a/Cargo.toml b/Cargo.toml index ba7dbfa..a0f83e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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", diff --git a/src/game/seed.rs b/src/game/seed.rs index af8ee58..5fabafa 100644 --- a/src/game/seed.rs +++ b/src/game/seed.rs @@ -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 for [u8; 8] { value.0.to_le_bytes() } } + +impl From for Seed { + fn from(value: u64) -> Self { + Seed(value) + } +} + +impl From for u64 { + fn from(value: Seed) -> Self { + value.0 + } +} diff --git a/src/lib.rs b/src/lib.rs index 52b2a2c..cb4d69c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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)] @@ -63,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::() .add_systems(Startup, setup_ui) @@ -80,7 +83,7 @@ impl Plugin for AppSettings { .add_systems( Update, ( - (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)), ), @@ -89,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()); } diff --git a/src/net.rs b/src/net.rs new file mode 100644 index 0000000..1055e92 --- /dev/null +++ b/src/net.rs @@ -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, + send: Res, + seed: Option>, + 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::(message.into()); + } + Ok(()) +} + +pub struct NetIOPlugin { + listen: u16, + peer: Option, +} + +impl NetIOPlugin { + pub fn new(listen: u16, peer: Option) -> 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)); + } +}