diff --git a/Cargo.lock b/Cargo.lock index 12cde13..8643555 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -192,12 +192,56 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + [[package]] name = "anstyle" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + [[package]] name = "anyhow" version = "1.0.104" @@ -1007,6 +1051,19 @@ dependencies = [ "smol_str", ] +[[package]] +name = "bevy_ggrs" +version = "0.22.0" +source = "git+https://github.com/gschup/bevy_ggrs#f1853823bfb888fcd9d468a136bb81fd6b9ed481" +dependencies = [ + "bevy", + "disqualified", + "ggrs", + "log", + "seahash", + "serde", +] + [[package]] name = "bevy_gilrs" version = "0.19.0" @@ -2072,6 +2129,15 @@ dependencies = [ "uuid", ] +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bit-set" version = "0.9.1" @@ -2087,6 +2153,42 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" +[[package]] +name = "bitfield-rle" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "215d5574bfd0d6d9243a9c741690f0b7ee8a0f39d3195283779793ed46d984e1" +dependencies = [ + "anyhow", + "varinteger", +] + +[[package]] +name = "bitfields" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5a55e8cbc3b75d1f002a3b62261ab67954755f7e7866f079b8aa63a409b549" +dependencies = [ + "bitfields-impl", +] + +[[package]] +name = "bitfields-impl" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "797cb6b6e8d5e8c2ecbb5ef52b7c273596d58ce1ab79ef2a6c38fa8dca0817c5" +dependencies = [ + "derive_more", + "getset", + "proc-macro2", + "quote", + "rustversion", + "strum", + "strum_macros", + "syn 2.0.119", + "thiserror 2.0.19", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -2357,21 +2459,36 @@ dependencies = [ [[package]] name = "clap" -version = "4.6.5" +version = "4.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301b56658598e48f3648647ac6fc887be7e7108eddfa4e9b63fcf3ec58c0cadf" +checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca" dependencies = [ "clap_builder", + "clap_derive", ] [[package]] name = "clap_builder" -version = "4.6.5" +version = "4.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94a65403d1a1bd28f7dc68eb8506e8874808ee5eecb59298de588e2e1407a078" +checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889" dependencies = [ + "anstream", "anstyle", "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 3.0.3", ] [[package]] @@ -2420,6 +2537,12 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + [[package]] name = "combine" version = "4.6.7" @@ -3281,7 +3404,11 @@ dependencies = [ "bevy", "bevy-inspector-egui", "bevy_egui", + "bevy_ggrs", + "bitfields", + "clap", "jackdaw_runtime", + "serde", ] [[package]] @@ -3310,6 +3437,17 @@ dependencies = [ "windows-link", ] +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "getrandom" version = "0.3.4" @@ -3335,6 +3473,32 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "getset" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cf442baaabe4213ce7d1239afc26c039180b6456da2cededa316ae2c8a77a77" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "ggrs" +version = "0.13.0" +source = "git+https://github.com/gschup/ggrs#0cda022c7871806b89f81c493d303ee1bb26e70e" +dependencies = [ + "bincode", + "bitfield-rle", + "instant", + "js-sys", + "parking_lot", + "rand 0.8.7", + "serde", + "tracing", +] + [[package]] name = "gilrs" version = "0.11.2" @@ -3620,6 +3784,12 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.5.2" @@ -3908,6 +4078,15 @@ dependencies = [ "libc", ] +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + [[package]] name = "inventory" version = "0.3.24" @@ -3928,6 +4107,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itertools" version = "0.10.5" @@ -5061,6 +5246,12 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "oorandom" version = "11.1.5" @@ -5553,13 +5744,24 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "019b4b213425016d7d84a153c4c73afb0946fbb4840e4eece7ba8848b9d6da22" +[[package]] +name = "rand" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + [[package]] name = "rand" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" dependencies = [ - "rand_chacha", + "rand_chacha 0.9.0", "rand_core 0.9.5", ] @@ -5573,6 +5775,16 @@ dependencies = [ "rand_core 0.10.1", ] +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + [[package]] name = "rand_chacha" version = "0.9.0" @@ -5583,6 +5795,15 @@ dependencies = [ "rand_core 0.9.5", ] +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + [[package]] name = "rand_core" version = "0.9.5" @@ -5909,6 +6130,12 @@ dependencies = [ "tiny-skia", ] +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + [[package]] name = "self_cell" version = "1.3.0" @@ -6244,6 +6471,30 @@ dependencies = [ "precomputed-hash", ] +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" + +[[package]] +name = "strum_macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "svg_fmt" version = "0.4.5" @@ -6687,6 +6938,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" version = "1.24.0" @@ -6716,6 +6973,12 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "varinteger" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ea29db9f94ff08bb619656b8120878f280526f71dc88b5262c958a510181812" + [[package]] name = "vec_map" version = "0.8.2" diff --git a/Cargo.toml b/Cargo.toml index e3dbf20..bf73fac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,10 @@ avian3d = "0.7" arrayvec = "0.7.8" bevy-inspector-egui = "0.37.0" bevy_egui = "0.40.0" +bevy_ggrs = { git = "https://github.com/gschup/bevy_ggrs" } +serde = "1.0.229" +bitfields = "3.0.0" +clap = { version = "4.6.6", features = ["derive"] } # Enable a small amount of optimization in the dev profile. [profile.dev] diff --git a/src/args.rs b/src/args.rs new file mode 100644 index 0000000..e6e122d --- /dev/null +++ b/src/args.rs @@ -0,0 +1,15 @@ +use bevy::prelude::*; +use clap::Parser; + +#[derive(Parser, Debug, Clone, Resource)] +#[command(version, about, long_about = None)] +pub struct Args { + #[clap(long)] + pub synctest: bool, + #[clap(long, default_value_t = 2)] + pub input_delay: usize, + #[clap(long, default_value_t = 2)] + pub check_distance: usize, + #[clap(long, default_value_t = 1)] + pub players: usize +} diff --git a/src/fighter.rs b/src/fighter.rs deleted file mode 100644 index 7fe7c39..0000000 --- a/src/fighter.rs +++ /dev/null @@ -1,100 +0,0 @@ -use bevy::{ - color::palettes::css::{HOT_PINK, YELLOW}, - prelude::*, -}; - -pub mod plugin; -pub mod data; -pub mod state; -pub mod states; -pub use plugin::*; -pub use data::*; - -use crate::stage::StagePlane; - -#[derive(Component)] -pub struct Grounded; - -#[derive(Component, Deref, DerefMut, Default, Debug)] -pub struct FighterVelocity(pub Vec2); - -#[derive(Component, Deref, DerefMut, Default)] -pub struct FighterPreviousTranslation(pub Vec2); - -#[derive(Component, Deref, DerefMut, Default)] -#[require(FighterPreviousTranslation)] -pub struct FighterTranslation(pub Vec2); - -#[derive(Component, Debug)] -#[require(Transform)] -pub struct FighterECB { - pub vertical_half: f32, - pub horizontal_half: f32, -} - -impl FighterECB { - pub fn to_3d_lineloop(&self, trf: Transform) -> [Vec3; 4] { - let rel_up = trf.transform_point(Vec3::new(0.0, self.vertical_half, 0.0)); - let rel_down = trf.transform_point(Vec3::new(0.0, -self.vertical_half, 0.0)); - let rel_left = trf.transform_point(Vec3::new(-self.horizontal_half, 0.0, 0.0)); - let rel_right = trf.transform_point(Vec3::new(self.horizontal_half, 0.0, 0.0)); - [rel_up, rel_right, rel_down, rel_left] - } -} - -pub fn debug_draw_ecb(ecbs: Query<(&FighterECB, &FighterTranslation)>, mut gizmos: Gizmos) { - for (ecb, trf) in ecbs { - let transformed = ecb.to_3d_lineloop(Transform::from_xyz(trf.x, trf.y, 0.0)); - gizmos.lineloop(transformed, YELLOW); - } -} - -pub fn integrate_gravity( - _fighters: Query<&mut FighterVelocity>, - _time: Res>, -) { -} - -pub fn apply_motion( - fighters: Query<( - &FighterVelocity, - &mut FighterTranslation, - &mut FighterPreviousTranslation, - )>, - _time: Res>, -) { - for (velocity, mut translation, mut prev_translation) in fighters { - prev_translation.0 = translation.0; - translation.0 += velocity.0; - } -} - -pub fn collide_fighter_with_scene( - fighters: Query<(&FighterECB, &mut FighterVelocity, &mut FighterTranslation)>, - stage_planes: Query<&StagePlane>, - mut gizmos: Gizmos, -) { - for (ecb, mut velocity, mut translation) in fighters { - for plane in stage_planes { - let dist_sq = ecb.vertical_half * ecb.vertical_half; - let ray = Ray2d::new(translation.0, Dir2::NEG_Y); - - let intersection_result = plane - .intersect_ray(ray) - .filter(|result| result.position.distance_squared(translation.0) <= dist_sq); - - if let Some(result) = intersection_result { - gizmos.sphere( - Isometry3d::from(Vec3::new(result.position.x, result.position.y, 0.0)), - 0.1f32, - HOT_PINK, - ); - velocity.0 = plane.project(velocity.0); - translation.0 = result.position + Vec2::new(0.0, ecb.vertical_half); - } - } - } -} - -pub fn fighter_movement() {} - diff --git a/src/fighter/attributes.rs b/src/fighter/attributes.rs new file mode 100644 index 0000000..6a9cf86 --- /dev/null +++ b/src/fighter/attributes.rs @@ -0,0 +1,39 @@ +use bevy::prelude::*; + +use crate::input::FighterInputFrame; + +/// Per-fighter tuning values. These are character data, not global rules — +/// anything that applies to every fighter belongs in +/// [`crate::game_settings::FighterSettingsCommon`] instead. +#[derive(Component, Clone, Copy)] +pub struct FighterAttributes { + pub base_walk_accel: f32, + pub stick_walk_accel: f32, + pub max_walk_vel: f32, + + pub dash_duration: u32, + pub dash_initial_velocity: f32, + pub base_dash_accel: f32, + pub stick_dash_accel: f32, + pub max_dash_vel: f32, + + pub ground_friction: f32, +} + +impl FighterAttributes { + pub fn get_accel_and_target_dashrun(&self, input: &FighterInputFrame) -> (f32, f32) { + let accel = input.movement.x * self.stick_dash_accel; + let accel = accel + input.movement.x.signum() * self.base_dash_accel; + let target_vel = input.movement.x * self.max_dash_vel; + + (accel, target_vel) + } + + pub fn get_accel_and_target_walk(&self, input: &FighterInputFrame) -> (f32, f32) { + let accel = input.movement.x * self.stick_walk_accel; + let accel = accel + input.movement.x.signum() * self.base_walk_accel; + let target_vel = input.movement.x * self.max_walk_vel; + + (accel, target_vel) + } +} diff --git a/src/fighter/collision.rs b/src/fighter/collision.rs new file mode 100644 index 0000000..04f6819 --- /dev/null +++ b/src/fighter/collision.rs @@ -0,0 +1,31 @@ +//! Resolving an integrated fighter position against the stage. + +use bevy::{color::palettes::css::HOT_PINK, prelude::*}; + +use crate::fighter::{FighterECB, FighterTranslation, FighterVelocity}; +use crate::stage::StageLine; + +pub fn collide_fighter_with_scene( + fighters: Query<(&FighterECB, &mut FighterVelocity, &mut FighterTranslation)>, + stage_planes: Query<&StageLine>, + mut gizmos: Gizmos, +) { + for (ecb, mut velocity, mut translation) in fighters { + for plane in stage_planes { + let ray_from = translation.0; + let ray_to = translation.0 + Vec2::new(0.0, -ecb.vertical_half); + + let intersection_result = plane.intersect_ray(Segment2d::new(ray_from, ray_to)); + + if let Some(result) = intersection_result { + gizmos.sphere( + Isometry3d::from(Vec3::new(result.position.x, result.position.y, 0.0)), + 0.1f32, + HOT_PINK, + ); + velocity.0 = plane.project(velocity.0); + translation.0 = result.position + Vec2::new(0.0, ecb.vertical_half); + } + } + } +} diff --git a/src/fighter/data.rs b/src/fighter/data.rs deleted file mode 100644 index c7ce535..0000000 --- a/src/fighter/data.rs +++ /dev/null @@ -1,16 +0,0 @@ -use bevy::prelude::*; - -#[derive(Component)] -pub struct FighterAttributes { - pub base_walk_accel: f32, - pub stick_walk_accel: f32, - pub max_walk_vel: f32, - - pub dash_duration: u32, - pub dash_initial_velocity: f32, - pub base_dash_accel: f32, - pub stick_dash_accel: f32, - pub max_dash_vel: f32, - - pub ground_friction: f32, -} \ No newline at end of file diff --git a/src/fighter/debug.rs b/src/fighter/debug.rs new file mode 100644 index 0000000..e82f11a --- /dev/null +++ b/src/fighter/debug.rs @@ -0,0 +1,30 @@ +//! Fighter debug views: gizmos and the egui inspector window. + +use bevy::{color::palettes::css::YELLOW, prelude::*}; +use bevy_egui::{EguiContexts, egui}; + +use crate::fighter::state::StateNameDebug; +use crate::fighter::{FighterECB, FighterTranslation, FighterVelocity}; +use crate::input::FighterInput; + +pub fn debug_draw_ecb(ecbs: Query<(&FighterECB, &FighterTranslation)>, mut gizmos: Gizmos) { + for (ecb, trf) in ecbs { + let transformed = ecb.to_3d_lineloop(Transform::from_xyz(trf.x, trf.y, 0.0)); + gizmos.lineloop(transformed, YELLOW); + } +} + +pub fn fighter_debug( + mut contexts: EguiContexts, + query: Query<(&FighterVelocity, &FighterInput, &StateNameDebug)>, +) -> Result { + for (vel, input, name) in query { + egui::Window::new("Hello").show(contexts.ctx_mut()?, |ui| { + ui.label(format!("State: {}", name.0)); + ui.label(format!("{:?}", vel)); + ui.label(format!("{:?}", input.get_last_frame())); + }); + } + + Ok(()) +} diff --git a/src/fighter/ecb.rs b/src/fighter/ecb.rs new file mode 100644 index 0000000..77842a6 --- /dev/null +++ b/src/fighter/ecb.rs @@ -0,0 +1,21 @@ +use bevy::prelude::*; + +/// Environment collision box: the diamond used to resolve a fighter against +/// the stage. Purely geometry — the collision response lives in +/// [`super::collision`]. +#[derive(Component, Debug, Clone, Copy)] +#[require(Transform)] +pub struct FighterECB { + pub vertical_half: f32, + pub horizontal_half: f32, +} + +impl FighterECB { + pub fn to_3d_lineloop(&self, trf: Transform) -> [Vec3; 4] { + let rel_up = trf.transform_point(Vec3::new(0.0, self.vertical_half, 0.0)); + let rel_down = trf.transform_point(Vec3::new(0.0, -self.vertical_half, 0.0)); + let rel_left = trf.transform_point(Vec3::new(-self.horizontal_half, 0.0, 0.0)); + let rel_right = trf.transform_point(Vec3::new(self.horizontal_half, 0.0, 0.0)); + [rel_up, rel_right, rel_down, rel_left] + } +} diff --git a/src/fighter/mod.rs b/src/fighter/mod.rs new file mode 100644 index 0000000..79a3bf4 --- /dev/null +++ b/src/fighter/mod.rs @@ -0,0 +1,86 @@ +//! Everything fighter-shaped: the components that describe a fighter, the +//! state machine that drives it, and the plugin that schedules it all. + +use bevy::prelude::*; +use bevy_egui::EguiPrimaryContextPass; +use bevy_ggrs::prelude::*; + +pub mod attributes; +pub mod collision; +pub mod debug; +pub mod ecb; +pub mod motion; +pub mod state; + +// The fighter components are re-exported so the rest of the crate can say +// `fighter::FighterVelocity` without caring which file it lives in. Named +// explicitly rather than globbed, so moving a type between submodules can't +// silently change what this module exports. +pub use attributes::FighterAttributes; +pub use ecb::FighterECB; +pub use motion::{FighterPreviousTranslation, FighterTranslation, FighterVelocity, Grounded}; + +use crate::schedule::GameplaySet; +use state::state_interrupt_system; + +/// Owns fighter simulation, the rollback registrations for fighter +/// components, and the fighter debug overlays. +#[derive(Default)] +pub struct FighterPlugin; + +impl Plugin for FighterPlugin { + fn build(&self, app: &mut App) { + app + // Every state gets a chance to interrupt itself, in priority order. + .add_systems( + GgrsSchedule, + ( + state_interrupt_system::, + state_interrupt_system::, + state_interrupt_system::, + state_interrupt_system::, + ) + .chain() + .in_set(GameplaySet::Interrupt), + ) + .add_systems( + GgrsSchedule, + ( + state::wait::wait_update, + state::walk::walk_update, + state::dash::dash_update, + state::run::run_update, + ) + .chain() + .in_set(GameplaySet::StateUpdate), + ) + .add_systems( + GgrsSchedule, + (motion::integrate_gravity, motion::apply_motion) + .chain() + .in_set(GameplaySet::Physics), + ) + .add_systems( + GgrsSchedule, + collision::collide_fighter_with_scene.in_set(GameplaySet::Collision), + ) + .add_observer(state::dash::on_insert_dash) + // Rollback registration lives next to the systems that write these + // components; a component simulated here but missing from this list + // is a desync waiting to happen. + .rollback_component_with_copy::() + .rollback_component_with_copy::() + .rollback_component_with_copy::() + .rollback_component_with_copy::() + .rollback_component_with_copy::() + .rollback_component_with_copy::() + .rollback_component_with_copy::() + .rollback_component_with_copy::() + .rollback_component_with_copy::() + .rollback_component_with_copy::() + .checksum_component::(state::dash::hash_dash_state) + // Debug views. + .add_systems(FixedPostUpdate, debug::debug_draw_ecb) + .add_systems(EguiPrimaryContextPass, debug::fighter_debug); + } +} diff --git a/src/fighter/motion.rs b/src/fighter/motion.rs new file mode 100644 index 0000000..78ae375 --- /dev/null +++ b/src/fighter/motion.rs @@ -0,0 +1,35 @@ +//! Where a fighter is and how it moves. States write [`FighterVelocity`]; +//! this module is what turns that into a position. + +use bevy::prelude::*; + +#[derive(Component, Clone, Copy)] +pub struct Grounded; + +#[derive(Component, Deref, DerefMut, Default, Debug, Clone, Copy)] +pub struct FighterVelocity(pub Vec2); + +#[derive(Component, Deref, DerefMut, Default, Clone, Copy)] +pub struct FighterPreviousTranslation(pub Vec2); + +#[derive(Component, Deref, DerefMut, Default, Clone, Copy)] +#[require(FighterPreviousTranslation)] +pub struct FighterTranslation(pub Vec2); + +pub fn integrate_gravity(_fighters: Query<&mut FighterVelocity>, _time: Res>) {} + +pub fn apply_motion( + fighters: Query<( + &FighterVelocity, + &mut FighterTranslation, + &mut FighterPreviousTranslation, + )>, + _time: Res>, +) { + for (velocity, mut translation, mut prev_translation) in fighters { + prev_translation.0 = translation.0; + translation.0 += velocity.0; + } +} + +pub fn fighter_movement() {} diff --git a/src/fighter/plugin.rs b/src/fighter/plugin.rs deleted file mode 100644 index 2e2dc80..0000000 --- a/src/fighter/plugin.rs +++ /dev/null @@ -1,48 +0,0 @@ -use bevy::prelude::*; -use bevy_egui::{EguiContexts, EguiPrimaryContextPass, egui}; -use crate::fighter::state::StateNameDebug; -use crate::fighter::{FighterVelocity, states}; -use crate::fighter::{ - apply_motion, collide_fighter_with_scene, integrate_gravity, state::state_interrupt_system, -}; -use crate::input::player::FighterInput; - -#[derive(Default)] -pub struct FighterPlugin; - -impl Plugin for FighterPlugin { - fn build(&self, app: &mut App) { - app.add_systems(FixedPostUpdate, super::debug_draw_ecb) - .add_systems( - FixedUpdate, - ( - state_interrupt_system::, - state_interrupt_system::, - state_interrupt_system::, - state_interrupt_system::, - states::wait::wait_update, - states::walk::walk_update, - states::dash::dash_update, - states::run::run_update, - integrate_gravity, - apply_motion, - collide_fighter_with_scene, - ) - .chain(), - ) - .add_observer(states::dash::on_insert_dash) - .add_systems(EguiPrimaryContextPass, fighter_debug); - } -} - -fn fighter_debug(mut contexts: EguiContexts, query: Query<(&FighterVelocity, &FighterInput, &StateNameDebug)>) -> Result { - for (vel, input, name) in query { - egui::Window::new("Hello").show(contexts.ctx_mut()?, |ui| { - ui.label(format!("State: {}", name.0)); - ui.label(format!("{:?}", vel)); - ui.label(format!("{:?}", input.get_last_frame())); - }); - } - - Ok(()) -} diff --git a/src/fighter/state/dash.rs b/src/fighter/state/dash.rs new file mode 100644 index 0000000..9786870 --- /dev/null +++ b/src/fighter/state/dash.rs @@ -0,0 +1,116 @@ +use std::hash::{Hash, Hasher}; + +use bevy::prelude::*; + +use super::{FighterState, FighterStateContext, FighterStateTransition, ground, run, wait, walk}; +use crate::fighter::{FighterAttributes, FighterVelocity}; +use crate::game_settings::GameSettings; +use crate::input::{ + FighterCommands::{SmashMoveLeft, SmashMoveRight}, + FighterInput, +}; + +pub struct DashStateCreator {} + +#[derive(Component, Debug, Clone, Copy)] +pub struct DashState { + frames_in_dash: u32, + direction: f32, +} + +pub fn hash_dash_state(dash_state: &DashState) -> u64 { + let mut hasher = bevy_ggrs::checksum_hasher(); + dash_state.direction.to_bits().hash(&mut hasher); + dash_state.frames_in_dash.hash(&mut hasher); + hasher.finish() +} + +impl DashState { + pub fn new(direction: f32) -> Self { + Self { + frames_in_dash: 0, + direction, + } + } +} + +impl FighterState for DashState { + fn check_interrupt( + &self, + state_context: &FighterStateContext, + ) -> Option { + if self.frames_in_dash > state_context.fighter_attribs.dash_duration { + // Dash finish shenanigans + run::check_input(state_context) + .or_else(|| walk::check_input(state_context)) + .or_else(|| wait::check_input(state_context)) + } else { + check_smash_input_with_dir(state_context) + .filter(|dir| *dir != self.direction) + .and_then(|dir| Some(FighterStateTransition::Dash(dir))) + } + } +} + +pub fn start(state_context: &mut FighterStateContext) { + state_context.input.clear_buffer(); +} + +fn check_smash_input_with_dir(state_context: &FighterStateContext) -> Option { + if state_context.input.has_command(SmashMoveLeft) { + return Some(-1.0); + } else if state_context.input.has_command(SmashMoveRight) { + return Some(1.0); + } else { + None + } +} + +pub fn check_input(state_context: &FighterStateContext) -> Option { + check_smash_input_with_dir(state_context).map(|dir| FighterStateTransition::Dash(dir)) +} + +pub fn dash_update( + dash_query: Query<( + &mut DashState, + &FighterAttributes, + &FighterInput, + &mut FighterVelocity, + )>, + game_settings: Res, +) { + for (mut dash, attributes, input, mut velocity) in dash_query { + dash.frames_in_dash += 1; + + let last_frame = input.get_last_frame(); + let (accel, target_vel) = attributes.get_accel_and_target_dashrun(&last_frame); + let accel = ground::compute_ground_accel( + accel, + target_vel, + velocity.x, + attributes, + game_settings.as_ref(), + ); + velocity.x += accel; + } +} + +pub fn on_insert_dash( + add: On, + query: Query< + ( + Entity, + &mut FighterInput, + &FighterAttributes, + &mut FighterVelocity, + ), + With, + >, +) { + for (entity, mut input, attribs, mut velocity) in query { + if entity == add.entity { + velocity.x = attribs.dash_initial_velocity * input.get_last_frame().movement.x.signum(); + input.clear_buffer(); + } + } +} diff --git a/src/fighter/states/grounded.rs b/src/fighter/state/ground.rs similarity index 57% rename from src/fighter/states/grounded.rs rename to src/fighter/state/ground.rs index f701698..d1cfeff 100644 --- a/src/fighter/states/grounded.rs +++ b/src/fighter/state/ground.rs @@ -1,6 +1,9 @@ -use bevy::log::info; +//! Ground movement math shared by every grounded state. Deliberately free of +//! ECS types so it stays cheap to reason about and to unit-test — it has to be +//! bit-for-bit deterministic for rollback. -use crate::{fighter::FighterAttributes, game_settings::GameSettings, input::player::InputFrame}; +use crate::fighter::FighterAttributes; +use crate::game_settings::GameSettings; pub fn apply_grounded_friction(friction: f32, ground_vel: f32) -> f32 { if friction.abs() > ground_vel.abs() { @@ -10,11 +13,15 @@ pub fn apply_grounded_friction(friction: f32, ground_vel: f32) -> f32 { } } -pub fn move_accelerate() { +pub fn move_accelerate() {} -} - -pub fn compute_ground_accel(accel: f32, target_vel: f32, gr_vel: f32, attribs: &FighterAttributes, game_settings: &GameSettings) -> f32 { +pub fn compute_ground_accel( + accel: f32, + target_vel: f32, + gr_vel: f32, + attribs: &FighterAttributes, + game_settings: &GameSettings, +) -> f32 { if target_vel == 0.0 { apply_grounded_friction(attribs.ground_friction, gr_vel) } else { @@ -46,24 +53,3 @@ pub fn compute_ground_accel(accel: f32, target_vel: f32, gr_vel: f32, attribs: & accel } } - - -impl FighterAttributes { - pub fn get_accel_and_target_dashrun(&self, input: &InputFrame) -> (f32, f32) { - let accel = input.movement.x * self.stick_dash_accel; - let accel = accel + input.movement.x.signum() * self.base_dash_accel; - let target_vel = input.movement.x * self.max_dash_vel; - - info!("accel {:?} target_vel {:?}", accel, target_vel); - - (accel, target_vel) - } - - pub fn get_accel_and_target_walk(&self, input: &InputFrame) -> (f32, f32) { - let accel = input.movement.x * self.stick_walk_accel; - let accel = accel + input.movement.x.signum() * self.base_walk_accel; - let target_vel = input.movement.x * self.max_walk_vel; - - (accel, target_vel) - } -} diff --git a/src/fighter/state.rs b/src/fighter/state/mod.rs similarity index 56% rename from src/fighter/state.rs rename to src/fighter/state/mod.rs index fd2e0d9..f607078 100644 --- a/src/fighter/state.rs +++ b/src/fighter/state/mod.rs @@ -1,18 +1,28 @@ -use bevy::{prelude::*}; +//! The fighter state machine: the [`FighterState`] trait every state +//! implements, the context they inspect, and the generic system that runs the +//! interrupt check. One state per submodule. -use crate::{fighter::{FighterAttributes, states}, game_settings::GameSettings, input::player::FighterInput}; +use bevy::prelude::*; + +use crate::fighter::FighterAttributes; +use crate::game_settings::GameSettings; +use crate::input::FighterInput; + +pub mod dash; +pub mod ground; +pub mod run; +pub mod wait; +pub mod walk; pub enum FighterStateTransition { Wait, Walk, - Dash, + Dash(f32), Run, } -pub trait FighterState : Component + Sized { - fn remove(&self, - state_context: &FighterStateContext, - commands: &mut Commands) { +pub trait FighterState: Component + Sized { + fn remove(&self, state_context: &FighterStateContext, commands: &mut Commands) { commands.entity(state_context.entity).remove::(); } fn check_interrupt( @@ -24,17 +34,21 @@ pub trait FighterState : Component + Sized { pub struct FighterStateContext<'a> { pub input: &'a mut FighterInput, pub fighter_attribs: &'a FighterAttributes, - pub game_settings: &'a Res<'a, GameSettings>, + pub game_settings: &'a GameSettings, pub entity: Entity, } -fn create_state(transition: FighterStateTransition, state_context: FighterStateContext, commands: &mut Commands) { +fn create_state( + transition: FighterStateTransition, + state_context: FighterStateContext, + commands: &mut Commands, +) { let mut ent_cmd = commands.entity(state_context.entity); match transition { - FighterStateTransition::Wait => ent_cmd.insert(states::wait::WaitState), - FighterStateTransition::Walk => ent_cmd.insert(states::walk::WalkState), - FighterStateTransition::Dash => ent_cmd.insert(states::dash::DashState::default()), - FighterStateTransition::Run => ent_cmd.insert(states::run::RunState), + FighterStateTransition::Wait => ent_cmd.insert(wait::WaitState), + FighterStateTransition::Walk => ent_cmd.insert(walk::WalkState), + FighterStateTransition::Dash(dir) => ent_cmd.insert(dash::DashState::new(dir)), + FighterStateTransition::Run => ent_cmd.insert(run::RunState), }; } @@ -42,7 +56,7 @@ fn get_state_debug_name(transition: &FighterStateTransition) -> &'static str { match transition { FighterStateTransition::Wait => "wait", FighterStateTransition::Walk => "walk", - FighterStateTransition::Dash => "dash", + FighterStateTransition::Dash(_) => "dash", FighterStateTransition::Run => "run", } } @@ -62,7 +76,6 @@ pub fn state_interrupt_system( mut commands: Commands, ) { for (ent, state, mut input, attribs) in query { - info!("{:?}", state); let state_context = FighterStateContext { input: &mut input, fighter_attribs: &attribs, @@ -71,7 +84,10 @@ pub fn state_interrupt_system( }; if let Some(new_state) = state.check_interrupt(&state_context) { commands.entity(ent).remove::(); - commands.entity(ent).insert(StateNameDebug(get_state_debug_name(&new_state))); + info!("State {:?} -> {}", state, get_state_debug_name(&new_state)); + commands + .entity(ent) + .insert(StateNameDebug(get_state_debug_name(&new_state))); create_state(new_state, state_context, &mut commands); } } diff --git a/src/fighter/state/run.rs b/src/fighter/state/run.rs new file mode 100644 index 0000000..244ffec --- /dev/null +++ b/src/fighter/state/run.rs @@ -0,0 +1,49 @@ +use bevy::prelude::*; + +use super::{FighterState, FighterStateContext, FighterStateTransition, ground, wait, walk}; +use crate::fighter::{FighterAttributes, FighterVelocity}; +use crate::game_settings::GameSettings; +use crate::input::FighterInput; + +#[derive(Component, Debug, Clone, Copy)] +pub struct RunState; + +impl FighterState for RunState { + fn check_interrupt( + &self, + state_context: &FighterStateContext, + ) -> Option { + if let Some(_) = check_input(state_context) { + None + } else { + walk::check_input(state_context).or_else(|| wait::check_input(state_context)) + } + } +} + +pub fn check_input(state_context: &FighterStateContext) -> Option { + let input_frame = state_context.input.get_last_frame(); + if input_frame.movement.x.abs() >= state_context.game_settings.input_common.run_stick_threshold + { + return Some(FighterStateTransition::Run); + } + None +} + +pub fn run_update( + fighters: Query<(&mut FighterVelocity, &FighterAttributes, &FighterInput), With>, + game_settings: Res, +) { + for (mut velocity, attributes, input) in fighters { + let last_frame = input.get_last_frame(); + let (accel, target_vel) = attributes.get_accel_and_target_dashrun(&last_frame); + let accel = ground::compute_ground_accel( + accel, + target_vel, + velocity.x, + attributes, + game_settings.as_ref(), + ); + velocity.x += accel; + } +} diff --git a/src/fighter/state/wait.rs b/src/fighter/state/wait.rs new file mode 100644 index 0000000..abdafa6 --- /dev/null +++ b/src/fighter/state/wait.rs @@ -0,0 +1,43 @@ +use bevy::prelude::*; + +use super::{FighterState, FighterStateContext, FighterStateTransition, ground, walk}; +use crate::fighter::{FighterAttributes, FighterVelocity}; +use crate::game_settings::GameSettings; + +#[derive(Component, Debug, Clone, Copy)] +pub struct WaitState; + +impl FighterState for WaitState { + fn check_interrupt( + &self, + state_context: &FighterStateContext, + ) -> Option { + walk::check_input(state_context) + } +} + +pub fn check_input(state_context: &FighterStateContext) -> Option { + let input_frame = state_context.input.get_last_frame(); + + if input_frame.movement.x.abs() < state_context.game_settings.input_common.stick_deadzone { + return Some(FighterStateTransition::Wait); + } + None +} + +pub fn wait_update( + query: Query<(&FighterAttributes, &mut FighterVelocity), With>, + game_settings: Res, +) { + for (attribs, mut velocity) in query { + let friction = if velocity.x.abs() > attribs.max_walk_vel { + attribs.ground_friction + * game_settings + .figher_common + .ground_friction_over_walk_speed_multiplier + } else { + attribs.ground_friction + }; + velocity.x += ground::apply_grounded_friction(friction, velocity.x); + } +} diff --git a/src/fighter/state/walk.rs b/src/fighter/state/walk.rs new file mode 100644 index 0000000..09a48df --- /dev/null +++ b/src/fighter/state/walk.rs @@ -0,0 +1,43 @@ +use bevy::prelude::*; + +use super::{FighterState, FighterStateContext, FighterStateTransition, dash, ground, wait}; +use crate::fighter::{FighterAttributes, FighterVelocity}; +use crate::game_settings::GameSettings; +use crate::input::FighterInput; + +#[derive(Component, Debug, Clone, Copy)] +pub struct WalkState; + +impl FighterState for WalkState { + fn check_interrupt( + &self, + state_context: &FighterStateContext, + ) -> Option { + wait::check_input(state_context).or_else(|| dash::check_input(state_context)) + } +} + +pub fn check_input(state_context: &FighterStateContext) -> Option { + let input_frame = state_context.input.get_last_frame(); + + if input_frame.movement.x.abs() >= state_context.game_settings.input_common.stick_deadzone { + return Some(FighterStateTransition::Walk); + } + + None +} + +pub fn walk_update( + fighters: Query<(&mut FighterVelocity, &FighterAttributes, &FighterInput), With>, + game_settings: Res, +) { + for (mut velocity, attributes, input) in fighters { + let last_frame = input.get_last_frame(); + let (accel, target_vel) = attributes.get_accel_and_target_walk(&last_frame); + + let accel = + ground::compute_ground_accel(accel, target_vel, velocity.x, attributes, &game_settings); + + velocity.x += accel; + } +} diff --git a/src/fighter/states.rs b/src/fighter/states.rs deleted file mode 100644 index 923c929..0000000 --- a/src/fighter/states.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod wait; -pub mod walk; -pub mod dash; -pub mod run; -pub mod grounded; \ No newline at end of file diff --git a/src/fighter/states/dash.rs b/src/fighter/states/dash.rs deleted file mode 100644 index b91980b..0000000 --- a/src/fighter/states/dash.rs +++ /dev/null @@ -1,116 +0,0 @@ -use crate::{ - fighter::{ - FighterAttributes, FighterVelocity, - state::{FighterState, FighterStateContext, FighterStateTransition}, - states::grounded, - }, - game_settings::GameSettings, - input::player::FighterInput, -}; -use bevy::prelude::*; - -pub struct DashStateCreator {} - -#[derive(Component, Debug)] -pub struct DashState { - frames_in_dash: u32, -} - -impl Default for DashState { - fn default() -> Self { - Self { frames_in_dash: 0 } - } -} - -impl FighterState for DashState { - fn check_interrupt( - &self, - state_context: &FighterStateContext, - ) -> Option { - if self.frames_in_dash > state_context.fighter_attribs.dash_duration { - // Dash finish shenanigans - super::run::check_input(state_context) - .or_else(|| super::walk::check_input(state_context)) - .or_else(|| super::wait::check_input(state_context)) - } else if let Some(last_frame) = state_context.input.get_last_frame() { - check_smash_input_with_dir(state_context) - .filter(|dir| *dir != last_frame.movement.x.signum()) - .and_then(|_| super::dash::check_input(state_context)) - } else { - None - } - } -} - -pub fn start(state_context: &mut FighterStateContext) { - state_context.input.clear_buffer(); -} - -fn check_smash_input_with_dir(state_context: &FighterStateContext) -> Option { - state_context.input.has_smash_x_movement( - state_context - .game_settings - .input_common - .smash_input_reset_axis_threshold, - state_context - .game_settings - .input_common - .smash_input_axis_threshold, - state_context - .game_settings - .input_common - .smash_input_frame_threshold, - ) -} - -pub fn dash_update( - dash_query: Query<( - &mut DashState, - &FighterAttributes, - &FighterInput, - &mut FighterVelocity, - )>, - game_settings: Res, -) { - for (mut dash, attributes, input, mut velocity) in dash_query { - dash.frames_in_dash += 1; - - if let Some(last_frame) = input.get_last_frame() { - let (accel, target_vel) = attributes.get_accel_and_target_dashrun(last_frame); - let accel = grounded::compute_ground_accel( - accel, - target_vel, - velocity.x, - attributes, - game_settings.as_ref(), - ); - velocity.x += accel; - } - } -} - -pub fn check_input(state_context: &FighterStateContext) -> Option { - check_smash_input_with_dir(state_context).map(|_| FighterStateTransition::Dash) -} - -pub fn on_insert_dash( - add: On, - query: Query< - ( - Entity, - &mut FighterInput, - &FighterAttributes, - &mut FighterVelocity, - ), - With, - >, -) { - for (entity, mut input, attribs, mut velocity) in query { - if entity == add.entity { - if let Some(last_frame) = input.get_last_frame() { - velocity.x = attribs.dash_initial_velocity * last_frame.movement.x.signum(); - } - input.clear_buffer(); - } - } -} diff --git a/src/fighter/states/run.rs b/src/fighter/states/run.rs deleted file mode 100644 index 02d65a4..0000000 --- a/src/fighter/states/run.rs +++ /dev/null @@ -1,44 +0,0 @@ -use crate::{ - fighter::{FighterAttributes, FighterVelocity, state::{FighterStateContext, FighterStateTransition}, states::grounded}, game_settings::GameSettings, input::player::FighterInput, state::FighterState, -}; -use bevy::prelude::*; - -#[derive(Component, Debug)] -pub struct RunState; - -pub fn check_input(state_context: &FighterStateContext) -> Option { - if let Some(input_frame) = state_context.input.get_last_frame() { - if input_frame.movement.x.abs() >= state_context.game_settings.input_common.run_stick_threshold { - return Some(FighterStateTransition::Run); - } - } - None -} - -impl FighterState for RunState { - fn check_interrupt( - &self, - state_context: &crate::fighter::state::FighterStateContext, - ) -> Option { - use super::*; - - if let Some(_) = check_input(state_context) { - None - } else { - walk::check_input(state_context).or_else(|| wait::check_input(state_context)) - } - } -} - -pub fn run_update( - fighters: Query<(&mut FighterVelocity, &FighterAttributes, &FighterInput), With>, - game_settings: Res, -) { - for ( mut velocity, attributes, input) in fighters { - if let Some(last_frame) = input.get_last_frame() { - let (accel, target_vel) = attributes.get_accel_and_target_dashrun(last_frame); - let accel = grounded::compute_ground_accel(accel, target_vel, velocity.x, attributes, game_settings.as_ref()); - velocity.x += accel; - } - } -} diff --git a/src/fighter/states/wait.rs b/src/fighter/states/wait.rs deleted file mode 100644 index bb5721b..0000000 --- a/src/fighter/states/wait.rs +++ /dev/null @@ -1,34 +0,0 @@ -use crate::{fighter::{state::{FighterState, FighterStateContext, FighterStateTransition}, states::{grounded, walk}}, game_settings::GameSettings}; -use crate::fighter::*; - -#[derive(Component, Debug)] -pub struct WaitState; - -impl FighterState for WaitState { - fn check_interrupt( - &self, - state_context: &FighterStateContext, - ) -> Option { - walk::check_input(state_context) - } -} - -pub fn check_input(state_context: &FighterStateContext) -> Option { - if let Some(input_frame) = state_context.input.get_last_frame() { - if input_frame.movement.x.abs() < state_context.game_settings.input_common.stick_deadzone { - return Some(FighterStateTransition::Wait); - } - } - None -} - -pub fn wait_update(query: Query<(&FighterAttributes, &mut FighterVelocity), With>, game_settings: Res) { - for (attribs, mut velocity) in query { - let friction = if velocity.x.abs() > attribs.max_walk_vel { - attribs.ground_friction * game_settings.figher_common.ground_friction_over_walk_speed_multiplier - } else { - attribs.ground_friction - }; - velocity.x += grounded::apply_grounded_friction(friction, velocity.x); - } -} \ No newline at end of file diff --git a/src/fighter/states/walk.rs b/src/fighter/states/walk.rs deleted file mode 100644 index 92d54d9..0000000 --- a/src/fighter/states/walk.rs +++ /dev/null @@ -1,46 +0,0 @@ -use crate::{ - fighter::{ - FighterAttributes, FighterVelocity, state::{FighterState, FighterStateContext, FighterStateTransition}, states::{dash, grounded, wait}, - }, game_settings::GameSettings, input::player::FighterInput, -}; -use bevy::prelude::*; - -#[derive(Component, Debug)] -pub struct WalkState; - -impl FighterState for WalkState { - fn check_interrupt( - &self, - state_context: &FighterStateContext, - ) -> Option { - wait::check_input(state_context).or_else(|| dash::check_input(state_context)) - } -} - -pub fn walk_update( - fighters: Query<( - &mut FighterVelocity, - &FighterAttributes, - &FighterInput, - ), With>, - game_settings: Res, -) { - for (mut velocity, attributes, input) in fighters { - if let Some(last_frame) = input.get_last_frame() { - let (accel, target_vel) = attributes.get_accel_and_target_walk(last_frame); - - let accel = grounded::compute_ground_accel(accel, target_vel, velocity.x, attributes, &game_settings); - - velocity.x += accel; - } - } -} - -pub fn check_input(state_context: &FighterStateContext) -> Option { - if let Some(input_frame) = state_context.input.get_last_frame() { - if input_frame.movement.x.abs() >= state_context.game_settings.input_common.stick_deadzone { - return Some(FighterStateTransition::Walk); - } - } - None -} diff --git a/src/game_settings.rs b/src/game_settings.rs index 855892f..1eff816 100644 --- a/src/game_settings.rs +++ b/src/game_settings.rs @@ -11,7 +11,8 @@ pub struct InputSettingsCommon { pub smash_input_reset_axis_threshold: f32, pub smash_input_axis_threshold: f32, pub run_stick_threshold: f32, - pub smash_input_frame_threshold: u32, + pub smash_input_frame_threshold: u8, + pub input_buffer_size: u8, } impl Default for InputSettingsCommon { @@ -22,7 +23,8 @@ impl Default for InputSettingsCommon { smash_input_reset_axis_threshold: 0.25, smash_input_axis_threshold: 0.8, run_stick_threshold: 0.625, - smash_input_frame_threshold: 2 + smash_input_frame_threshold: 2, + input_buffer_size: 5 } } } diff --git a/src/input.rs b/src/input.rs deleted file mode 100644 index 4139cea..0000000 --- a/src/input.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod player; -pub mod logic; -pub mod plugin; \ No newline at end of file diff --git a/src/input/buffer.rs b/src/input/buffer.rs new file mode 100644 index 0000000..6ab4fc1 --- /dev/null +++ b/src/input/buffer.rs @@ -0,0 +1,130 @@ +//! Per-fighter input state: the current and previous frame, plus the buffered +//! commands (smash inputs and friends) that states query when deciding whether +//! to interrupt. + +use std::ops::{Index, IndexMut}; + +use bevy::prelude::*; +use bevy_ggrs::{PlayerInputs, Rollback}; + +use crate::game_settings::GameSettings; +use crate::input::FighterInputFrame; +use crate::netcode::GGRSCfg; +use crate::player::Player; + +struct FighterInputBufferedActions {} + +pub enum FighterCommands { + SmashMoveLeft, + SmashMoveRight, +} + +/// Frames of life remaining for each buffered command. Indexed by +/// [`FighterCommands`] so adding a command is a two-line change here. +#[derive(Default, Clone, Copy)] +pub struct FighterCommandLifetimes { + smash_move_left: u8, + smash_move_right: u8, +} + +impl Index for FighterCommandLifetimes { + type Output = u8; + + fn index(&self, command: FighterCommands) -> &Self::Output { + match command { + FighterCommands::SmashMoveLeft => &self.smash_move_left, + FighterCommands::SmashMoveRight => &self.smash_move_right, + } + } +} + +impl IndexMut for FighterCommandLifetimes { + fn index_mut(&mut self, command: FighterCommands) -> &mut Self::Output { + match command { + FighterCommands::SmashMoveLeft => &mut self.smash_move_left, + FighterCommands::SmashMoveRight => &mut self.smash_move_right, + } + } +} + +#[derive(Component, Default, Clone, Copy)] +#[require(Rollback)] +pub struct FighterInput { + command_lifetimes: FighterCommandLifetimes, + prev_frame: FighterInputFrame, + current_frame: FighterInputFrame, + frames_in_smash_move_deadzone: u8, +} + +impl FighterInput { + pub fn get_last_frame(&self) -> FighterInputFrame { + self.current_frame + } + + pub fn push_input_frame(&mut self, frame: FighterInputFrame) { + self.prev_frame = self.current_frame; + self.current_frame = frame; + } + + pub fn clear_buffer(&mut self) { + self.command_lifetimes = FighterCommandLifetimes::default() + } + + pub fn clear_command(&mut self, command: FighterCommands) { + self.command_lifetimes[command] = 0; + } + + pub fn set_lifetime(&mut self, command: FighterCommands, lifetime: u8) { + self.command_lifetimes[command] = lifetime; + } + + pub fn has_command(&self, command: FighterCommands) -> bool { + self.command_lifetimes[command] > 0 + } +} + +/// Turns this frame's rolled-back inputs into per-fighter [`FighterInput`] +/// state. Runs in `GameplaySet::Input`, before any state looks at it. +pub fn postprocess_input( + query: Query<(&mut FighterInput, &Player)>, + inputs: Res>, + game_settings: Res, +) { + for (mut input, player) in query { + input.push_input_frame(inputs[player.handle].0); + + // Directional smash detection + let x_abs = input.current_frame.movement.x.abs(); + + let reset_deadzone = game_settings.input_common.smash_input_reset_axis_threshold; + let axis_threshold = game_settings.input_common.smash_input_axis_threshold; + let frame_threshold = game_settings.input_common.smash_input_frame_threshold; + + if x_abs < reset_deadzone { + input.frames_in_smash_move_deadzone = 0xFE; + } else { + let prev_x_abs = input.prev_frame.movement.x.abs(); + if prev_x_abs < reset_deadzone + || input.prev_frame.movement.x.signum() != input.current_frame.movement.x.signum() + { + input.frames_in_smash_move_deadzone = 0; + } else { + input.frames_in_smash_move_deadzone += 1; + } + } + + if x_abs >= axis_threshold && input.frames_in_smash_move_deadzone <= frame_threshold { + input.clear_command(FighterCommands::SmashMoveLeft); + input.clear_command(FighterCommands::SmashMoveRight); + + let command_to_set = if input.current_frame.movement.x > 0.0 { + FighterCommands::SmashMoveRight + } else { + FighterCommands::SmashMoveLeft + }; + input.set_lifetime(command_to_set, game_settings.input_common.input_buffer_size); + + if input.current_frame.movement.x > 0.0 {} + } + } +} diff --git a/src/input/control.rs b/src/input/control.rs new file mode 100644 index 0000000..377f24e --- /dev/null +++ b/src/input/control.rs @@ -0,0 +1,12 @@ +//! Which entity is driving which. Kept separate from the input pipeline +//! itself: this is a relationship between entities, not a source of input. + +use bevy::prelude::*; + +#[derive(Component, Debug)] +#[relationship(relationship_target = ControlledBy)] +pub struct Controls(pub Entity); + +#[derive(Component, Debug)] +#[relationship_target(relationship = Controls)] +pub struct ControlledBy(Entity); diff --git a/src/input/frame.rs b/src/input/frame.rs new file mode 100644 index 0000000..a711314 --- /dev/null +++ b/src/input/frame.rs @@ -0,0 +1,10 @@ +use bevy::prelude::*; +use serde::{Deserialize, Serialize}; + +/// One frame of a player's raw input. This is the type GGRS serializes and +/// sends over the wire, so keep it small and keep it `PartialEq`. +#[derive(Copy, Clone, Debug, PartialEq, Default, Serialize, Deserialize)] +pub struct FighterInputFrame { + pub movement: Vec2, + pub directional_attack: Vec2, +} diff --git a/src/input/keyboard.rs b/src/input/keyboard.rs new file mode 100644 index 0000000..826b38f --- /dev/null +++ b/src/input/keyboard.rs @@ -0,0 +1,55 @@ +//! Keyboard backend: samples the keys into the local input frames GGRS sends +//! over the wire. + +use bevy::platform::collections::HashMap; +use bevy::prelude::*; +use bevy_ggrs::{LocalInputs, LocalPlayers}; + +use crate::input::{BaseInputMap, FighterInputFrame, InputActionState, InputMapAction}; +use crate::netcode::GGRSCfg; + +pub fn preprocess_keyboard_input( + mut commands: Commands, + key: Res>, + input_map: Res, + local_players: Res, +) { + // Keyboard input is only for player 0 + + let mut local_inputs = HashMap::new(); + + for handle in &local_players.0 { + let mut input_frame = FighterInputFrame::default(); + for keyboard_elment in &input_map.keyboard { + let action_state = if key.just_pressed(keyboard_elment.key) { + InputActionState::JustPressed + } else if key.just_released(keyboard_elment.key) { + InputActionState::JustReleased + } else if key.pressed(keyboard_elment.key) { + InputActionState::Pressed + } else { + InputActionState::Released + }; + + let action_strength = match action_state { + InputActionState::JustPressed | InputActionState::Pressed => 1.0f32, + InputActionState::JustReleased | InputActionState::Released => 0.0f32, + }; + + match keyboard_elment.action { + InputMapAction::MovementXDir(sign) => { + input_frame.movement.x += (sign as f32) * action_strength + } + InputMapAction::MovementYDir(sign) => { + input_frame.movement.y += (sign as f32) * action_strength + } + InputMapAction::Jump => todo!(), + } + } + + input_frame.movement = input_frame.movement.normalize_or_zero(); + input_frame.directional_attack = input_frame.directional_attack.normalize_or_zero(); + local_inputs.insert(*handle, input_frame); + } + commands.insert_resource(LocalInputs::(local_inputs)); +} diff --git a/src/input/logic.rs b/src/input/logic.rs deleted file mode 100644 index 4778f95..0000000 --- a/src/input/logic.rs +++ /dev/null @@ -1,43 +0,0 @@ -use bevy::prelude::*; - -use crate::input::player::InputFrame; - -use super::player::{FighterInput, BaseInputMap, InputActionState}; - -pub fn postprocess_input(input_state: Query<&mut FighterInput>) { - for _player_input in input_state { - } -} -pub fn preprocess_keyboard_input(key: Res>, input_map: Res, mut input_state: Query<&mut FighterInput>) { - // Keyboard input is only for player 0 - for mut keeb_input in &mut input_state.iter_mut().take(1) { - let mut input_frame = InputFrame::default(); - for keyboard_elment in &input_map.keyboard { - let action_state = if key.just_pressed(keyboard_elment.key) { - InputActionState::JustPressed - } else if key.just_released(keyboard_elment.key) { - InputActionState::JustReleased - } else if key.pressed(keyboard_elment.key) { - InputActionState::Pressed - } else { - InputActionState::Released - }; - - let action_strength = match action_state { - InputActionState::JustPressed | InputActionState::Pressed => 1.0f32, - InputActionState::JustReleased | InputActionState::Released => 0.0f32, - }; - - match keyboard_elment.action { - super::player::InputMapAction::MovementXDir(sign) => input_frame.movement.x += (sign as f32) * action_strength, - super::player::InputMapAction::MovementYDir(sign) => input_frame.movement.y += (sign as f32) * action_strength, - super::player::InputMapAction::Jump => todo!(), - } - } - - input_frame.movement = input_frame.movement.normalize_or_zero(); - input_frame.directional_attack = input_frame.directional_attack.normalize_or_zero(); - keeb_input.push_input_frame(input_frame); - } - -} \ No newline at end of file diff --git a/src/input/map.rs b/src/input/map.rs new file mode 100644 index 0000000..f528d4b --- /dev/null +++ b/src/input/map.rs @@ -0,0 +1,31 @@ +//! What a physical button means. Device-agnostic: a binding names an +//! [`InputMapAction`], and each device backend (see [`super::keyboard`]) +//! decides how to read it. + +use bevy::prelude::*; + +#[derive(Debug)] +pub enum InputActionState { + JustPressed, + Pressed, + JustReleased, + Released, +} + +#[derive(Debug)] +pub enum InputMapAction { + MovementXDir(i32), + MovementYDir(i32), + Jump, +} + +#[derive(Debug)] +pub struct KeyboardInputMapElement { + pub key: KeyCode, + pub action: InputMapAction, +} + +#[derive(Resource)] +pub struct BaseInputMap { + pub keyboard: Vec, +} diff --git a/src/input/mod.rs b/src/input/mod.rs new file mode 100644 index 0000000..0bffea1 --- /dev/null +++ b/src/input/mod.rs @@ -0,0 +1,51 @@ +//! Input, from physical device to the buffered per-fighter state that the +//! state machine reads. +//! +//! The pipeline is: [`keyboard`] samples devices into [`FighterInputFrame`]s +//! that GGRS ships over the wire, then [`buffer::postprocess_input`] turns the +//! rolled-back frames into [`FighterInput`]. + +use bevy::prelude::*; +use bevy_ggrs::prelude::*; + +pub mod buffer; +pub mod control; +pub mod frame; +pub mod keyboard; +pub mod map; + +pub use buffer::{FighterCommands, FighterInput}; +pub use frame::FighterInputFrame; +pub use map::{BaseInputMap, InputActionState, InputMapAction, KeyboardInputMapElement}; + +use crate::schedule::GameplaySet; + +/// Owns the input map, the local-input collection that feeds GGRS, and the +/// per-fighter input state derived from the rolled-back inputs. +#[derive(Default)] +pub struct FighterInputPlugin; + +impl Plugin for FighterInputPlugin { + fn build(&self, app: &mut App) { + app.insert_resource(BaseInputMap { + keyboard: vec![ + // right + KeyboardInputMapElement { + key: KeyCode::KeyD, + action: InputMapAction::MovementXDir(1), + }, + // left + KeyboardInputMapElement { + key: KeyCode::KeyA, + action: InputMapAction::MovementXDir(-1), + }, + ], + }) + .add_systems(ReadInputs, keyboard::preprocess_keyboard_input) + .add_systems( + GgrsSchedule, + buffer::postprocess_input.in_set(GameplaySet::Input), + ) + .rollback_component_with_clone::(); + } +} diff --git a/src/input/player.rs b/src/input/player.rs deleted file mode 100644 index e8cf40b..0000000 --- a/src/input/player.rs +++ /dev/null @@ -1,99 +0,0 @@ -use std::collections::VecDeque; - -use bevy::prelude::*; - -#[derive(Default, Clone, Debug)] -pub struct InputFrame { - pub movement: Vec2, - pub directional_attack: Vec2 -} - -#[derive(Component)] -pub struct FighterInput { - input_buffer: VecDeque -} - -impl Default for FighterInput { - fn default() -> Self { - Self { input_buffer: vec!(InputFrame::default()).into() } - } -} - -impl FighterInput { - const INPUT_BUFFER_SIZE: usize = 5; - pub fn get_last_frame(&self) -> Option<&InputFrame> { - Some(self.input_buffer.back().unwrap()) - } - - pub fn push_input_frame(&mut self, frame: InputFrame) { - while self.input_buffer.len() >= Self::INPUT_BUFFER_SIZE { - self.input_buffer.pop_front(); - } - self.input_buffer.push_back(frame); - } - - pub fn clear_buffer(&mut self) { - info!("CLEAR"); - while self.input_buffer.len() > 1 { - self.input_buffer.pop_front(); - } - } - - pub fn has_smash_x_movement(&self, reset_deadzone: f32, threshold: f32, frame_threshold: u32) -> Option { - let iter = self.input_buffer.iter().zip(self.input_buffer.iter().skip(1)); - - let mut frames_in_deadzone = 0xFE; - for (prev, current) in iter { - let x_abs = current.movement.x.abs(); - if x_abs < reset_deadzone { - frames_in_deadzone = 0xFE; - } else { - let prev_x_abs = prev.movement.x.abs(); - if prev_x_abs < reset_deadzone || prev.movement.x.signum() != current.movement.x.signum() { - frames_in_deadzone = 0; - } else { - frames_in_deadzone += 1; - } - } - - if x_abs >= threshold && frames_in_deadzone <= frame_threshold { - return Some(x_abs.signum()); - } - } - - None - } -} - -#[derive(Debug)] -pub enum InputActionState { - JustPressed, - Pressed, - JustReleased, - Released -} - -#[derive(Debug)] -pub enum InputMapAction { - MovementXDir(i32), - MovementYDir(i32), - Jump -} -#[derive(Debug)] -pub struct KeyboardInputMapElement { - pub key: KeyCode, - pub action: InputMapAction -} - -#[derive(Resource)] -pub struct BaseInputMap { - pub keyboard: Vec -} - -#[derive(Component, Debug)] -#[relationship(relationship_target = ControlledBy)] -pub struct Controls(pub Entity); - -#[derive(Component, Debug)] -#[relationship_target(relationship = Controls)] -pub struct ControlledBy(Entity); \ No newline at end of file diff --git a/src/input/plugin.rs b/src/input/plugin.rs deleted file mode 100644 index 92fbc2d..0000000 --- a/src/input/plugin.rs +++ /dev/null @@ -1,27 +0,0 @@ -use bevy::prelude::*; -use crate::input::{ - logic::{postprocess_input, preprocess_keyboard_input}, player::{BaseInputMap, InputMapAction, KeyboardInputMapElement}, -}; - -#[derive(Default)] -pub struct FighterInputPlugin; - -impl Plugin for FighterInputPlugin { - fn build(&self, app: &mut App) { - app.insert_resource(BaseInputMap { - keyboard: vec![ - // right - KeyboardInputMapElement { - key: KeyCode::KeyD, - action: InputMapAction::MovementXDir(1), - }, - // left - KeyboardInputMapElement { - key: KeyCode::KeyA, - action: InputMapAction::MovementXDir(-1), - }, - ], - }) - .add_systems(FixedPreUpdate, (preprocess_keyboard_input, postprocess_input).chain()); - } -} diff --git a/src/lib.rs b/src/lib.rs index dbc4578..3466281 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,14 +22,29 @@ //! appears in `Add Component`. No registration code is needed. use bevy::prelude::*; +use clap::Parser; use jackdaw_runtime::prelude::*; -use crate::{fighter::{states::wait::WaitState, *}, game_settings::{FighterSettingsCommon, GameSettings, InputSettingsCommon}, stage::debug_draw_scene}; +use crate::{ + fighter::{ + FighterAttributes, FighterECB, FighterTranslation, FighterVelocity, + state::wait::WaitState, + }, + game_settings::{FighterSettingsCommon, GameSettings, InputSettingsCommon}, + input::FighterInput, + player::Player, + schedule::GameplaySchedulePlugin, +}; pub mod fighter; pub mod input; pub mod stage; pub mod game_settings; +pub mod netcode; +pub mod player; +pub mod schedule; +mod math; +mod args; /// Your game's Bevy plugin. The editor finds it by this name (override /// with `plugin = "..."` in jackdaw.toml) and runs it on Play; the @@ -41,6 +56,9 @@ pub fn setup(mut commands: Commands) { let _fighter = commands .spawn(( Name("Fighter".into()), + Player { + handle: 0, + }, FighterECB { vertical_half: 0.5, horizontal_half: 0.25, @@ -59,19 +77,27 @@ pub fn setup(mut commands: Commands) { dash_initial_velocity: 0.19, }, WaitState {}, - input::player::FighterInput::default() + FighterInput::default() )) .id(); } impl Plugin for GamePlugin { fn build(&self, app: &mut App) { - app.add_systems(Startup, stage::test_scene.spawn()) + // Each module owns its own systems, rollback registrations and + // resources; this plugin only wires them together and supplies the + // game-level content and configuration. + app.add_plugins(( + // Brings up GGRS (and with it `GgrsSchedule`), so it goes first. + netcode::FighterNetcodePlugin::default(), + GameplaySchedulePlugin, + input::FighterInputPlugin, + fighter::FighterPlugin, + stage::StagePlugin, + )) .add_systems(Startup, setup) .add_systems(Update, spin_cubes) - .add_systems(Update, debug_draw_scene) - .add_plugins(input::plugin::FighterInputPlugin) - .add_plugins(fighter::FighterPlugin) + .insert_resource(args::Args::parse()) .insert_resource(GameSettings { figher_common: FighterSettingsCommon { walk_speed_ease: 0.5, diff --git a/src/math.rs b/src/math.rs new file mode 100644 index 0000000..5ba5423 --- /dev/null +++ b/src/math.rs @@ -0,0 +1,33 @@ +use bevy::math::{Vec2, primitives::Segment2d}; + +pub trait SegmentIntersection { + fn segment_intersection(&self, rhs: &Segment2d) -> Option; +} + +impl SegmentIntersection for Segment2d { + fn segment_intersection(&self, rhs: &Segment2d) -> Option { + let p0 = self.point1(); + let p1 = self.point2(); + let p2 = rhs.point1(); + let p3 = rhs.point2(); + + let s1_x = p1.x - p0.x; + let s1_y = p1.y - p0.y; + let s2_x = p3.x - p2.x; + let s2_y = p3.y - p2.y; + + let denom = -s2_x * s1_y + s1_x * s2_y; + if denom.abs() < f32::EPSILON { + return None; // Parallel or collinear + } + + let s = (-s1_y * (p0.x - p2.x) + s1_x * (p0.y - p2.y)) / denom; + let t = ( s2_x * (p0.y - p2.y) - s2_y * (p0.x - p2.x)) / denom; + + if s >= 0.0 && s <= 1.0 && t >= 0.0 && t <= 1.0 { + Some(Vec2::new(p0.x + (t * s1_x), p0.y + (t * s1_y))) + } else { + None + } + } +} \ No newline at end of file diff --git a/src/netcode/debug.rs b/src/netcode/debug.rs new file mode 100644 index 0000000..bd9f095 --- /dev/null +++ b/src/netcode/debug.rs @@ -0,0 +1,30 @@ +//! Network stats overlay. + +use bevy::prelude::*; +use bevy_egui::prelude::*; +use bevy_ggrs::prelude::*; + +use crate::netcode::GGRSCfg; + +pub fn network_debug( + mut contexts: EguiContexts, + session: Option>>, +) -> Result { + if let Some(sess) = session { + match sess.as_ref() { + Session::P2P(s) => { + let num_players = s.num_players(); + for i in 0..num_players { + egui::Window::new(format!("Player {}", i)).show(contexts.ctx_mut()?, |ui| { + if let Ok(stats) = s.network_stats(i) { + ui.label(format!("{:?}", stats)); + } + }); + } + } + _ => {} + } + } + + Ok(()) +} diff --git a/src/netcode/mod.rs b/src/netcode/mod.rs new file mode 100644 index 0000000..e146860 --- /dev/null +++ b/src/netcode/mod.rs @@ -0,0 +1,31 @@ +//! Rollback networking: session setup and the engine-level rollback +//! registrations. Gameplay components are registered by the plugin that owns +//! them, not here. + +use bevy::prelude::*; +use bevy_egui::EguiPrimaryContextPass; +use bevy_ggrs::prelude::*; + +pub mod debug; +pub mod session; + +/// The GGRS configuration for this game — notably, what a frame of input is. +pub type GGRSCfg = GgrsConfig; + +#[derive(Default)] +pub struct FighterNetcodePlugin; + +impl Plugin for FighterNetcodePlugin { + fn build(&self, app: &mut App) { + let plugin = GgrsPlugin::::default(); + app.add_plugins(plugin) + .add_systems(Startup, session::netcode_setup) + // Engine-level components that every rolled-back entity may carry. + // Gameplay components are registered by the plugin that owns them. + .rollback_component_with_clone::() + .rollback_component_with_clone::() + .add_systems(EguiPrimaryContextPass, debug::network_debug) + .add_systems(Update, session::print_events_system) + .insert_resource(RollbackFrameRate(60)); + } +} diff --git a/src/netcode/session.rs b/src/netcode/session.rs new file mode 100644 index 0000000..1c0bb28 --- /dev/null +++ b/src/netcode/session.rs @@ -0,0 +1,48 @@ +//! Bringing up the GGRS session and reporting what it does. + +use bevy::prelude::*; +use bevy_ggrs::{ggrs::UdpNonBlockingSocket, prelude::*}; + +use crate::args::Args; +use crate::netcode::GGRSCfg; + +pub fn netcode_setup(mut commands: Commands, args: Res) -> Result { + let mut sess_build = SessionBuilder::::new() + .with_num_players(args.players)? + .with_check_distance(args.check_distance) + .with_input_delay(args.input_delay); // (optional) set input delay for the local player + + for i in 0..args.players { + sess_build = sess_build.add_player(PlayerType::Local, i)?; + } + + if args.synctest { + println!("Start synctest!"); + let sess = sess_build.start_synctest_session()?; + commands.insert_resource(Session::SyncTest(sess)); + } else { + println!("Start p2p!"); + let socket = UdpNonBlockingSocket::bind_to_port(6969)?; + let sess = sess_build.start_p2p_session(socket)?; + commands.insert_resource(Session::P2P(sess)); + } + + Ok(()) +} + +pub fn print_events_system(mut session: ResMut>) { + match session.as_mut() { + Session::P2P(s) => { + for event in s.events() { + match event { + GgrsEvent::Disconnected { .. } | GgrsEvent::NetworkInterrupted { .. } => { + warn!("GGRS event: {event:?}") + } + GgrsEvent::DesyncDetected { .. } => error!("GGRS event: {event:?}"), + _ => info!("GGRS event: {event:?}"), + } + } + } + _ => {} + } +} diff --git a/src/player.rs b/src/player.rs new file mode 100644 index 0000000..981fe8b --- /dev/null +++ b/src/player.rs @@ -0,0 +1,14 @@ +use bevy::prelude::*; +use bevy_ggrs::prelude::*; + +// #[require(Rollback)] is the idiomatic way to ensure an entity is always included in +// the rollback system. Whenever a Player is spawned, Rollback (and its RollbackId) will +// be added automatically — no need to add it manually in the spawn bundle. +/// Ties an entity to one of the session's input streams. Lives at the crate +/// root rather than in `netcode` because "which player is this" is a game +/// concept; `netcode` only decides where the inputs come from. +#[derive(Default, Component)] +#[require(Rollback)] +pub struct Player { + pub handle: usize, +} diff --git a/src/schedule.rs b/src/schedule.rs new file mode 100644 index 0000000..20f1427 --- /dev/null +++ b/src/schedule.rs @@ -0,0 +1,44 @@ +//! The gameplay frame pipeline. +//! +//! Every rollback-simulated system belongs to exactly one of these sets, and +//! the sets run in the order declared here. Plugins add their systems with +//! `.in_set(GameplaySet::X)` instead of being threaded into one central +//! `.chain()`, so a module can schedule its own work without any other module +//! knowing about it. + +use bevy::prelude::*; +use bevy_ggrs::prelude::*; + +#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)] +pub enum GameplaySet { + /// Turn this frame's netcode inputs into per-fighter input state. + Input, + /// Decide whether the current fighter state hands off to another one. + Interrupt, + /// Run the fighter state that survived the interrupt pass. + StateUpdate, + /// Integrate velocities into positions. + Physics, + /// Resolve the integrated positions against the stage. + Collision, +} + +/// Declares the order of [`GameplaySet`] inside [`GgrsSchedule`]. Added by +/// `GamePlugin` before any plugin that populates the sets. +pub struct GameplaySchedulePlugin; + +impl Plugin for GameplaySchedulePlugin { + fn build(&self, app: &mut App) { + app.configure_sets( + GgrsSchedule, + ( + GameplaySet::Input, + GameplaySet::Interrupt, + GameplaySet::StateUpdate, + GameplaySet::Physics, + GameplaySet::Collision, + ) + .chain(), + ); + } +} diff --git a/src/stage/debug.rs b/src/stage/debug.rs new file mode 100644 index 0000000..eddfe0b --- /dev/null +++ b/src/stage/debug.rs @@ -0,0 +1,16 @@ +use bevy::{color::palettes::css::RED, prelude::*}; + +use crate::stage::StageLine; + +pub fn debug_draw_scene(mut gizmos: Gizmos, planes: Query<&StageLine>) { + for plane in planes { + let from = plane.segment.point1(); + let to = plane.segment.point2(); + let from_3d = Vec3::new(from.x, from.y, 0.0); + let end_3d = Vec3::new(to.x, to.y, 0.0); + let normal_3d = Vec3::new(plane.normal.x, plane.normal.y, 0.0); + gizmos.line(from_3d, end_3d, RED); + let midpoint = (from_3d + end_3d) * 0.5; + gizmos.arrow(midpoint, midpoint + normal_3d, RED); + } +} diff --git a/src/stage/line.rs b/src/stage/line.rs new file mode 100644 index 0000000..e3a0d3d --- /dev/null +++ b/src/stage/line.rs @@ -0,0 +1,34 @@ +//! Stage collision geometry. + +use bevy::{math::InvalidDirectionError, prelude::*}; + +use crate::math::SegmentIntersection; + +#[derive(Component, Reflect, Clone, Default)] +#[reflect(Component)] +pub struct StageLine { + pub segment: Segment2d, + pub normal: Vec2, +} + +pub struct StagePlaneIntersectResult { + pub position: Vec2, +} + +impl StageLine { + pub fn new(start: Vec2, end: Vec2) -> Result { + let segment = Segment2d::new(start, end); + let normal = segment.left_normal().as_vec2(); + Ok(Self { segment, normal }) + } + + pub fn intersect_ray(&self, segment: Segment2d) -> Option { + self.segment + .segment_intersection(&segment) + .map(|position| StagePlaneIntersectResult { position }) + } + + pub fn project(&self, vec: Vec2) -> Vec2 { + vec - vec.project_onto_normalized(self.normal) + } +} diff --git a/src/stage/mod.rs b/src/stage/mod.rs index 2d03806..9f74a49 100644 --- a/src/stage/mod.rs +++ b/src/stage/mod.rs @@ -1,87 +1,21 @@ -use bevy::{color::palettes::css::{GREEN, RED}, prelude::*}; +//! The stage: its collision geometry, the scene that gets spawned, and how +//! both are drawn. -#[derive(Component, Reflect, Clone, Default)] -#[reflect(Component)] -pub struct StagePlane { - pub plane: Plane2d, - pub position: Vec2, - half_size: f32, - basis_x: Vec2, -} +use bevy::prelude::*; -pub struct StagePlaneIntersectResult { - pub position: Vec2 -} +pub mod debug; +pub mod line; +pub mod scene; -impl StagePlane { - pub fn new(normal: Dir2, half_size: f32, position: Vec2) -> Self { - let orthogonal = normal.perp(); - let basis_x = orthogonal * half_size; +pub use line::{StageLine, StagePlaneIntersectResult}; - Self { - plane: Plane2d::new(*normal), - position, - half_size, - basis_x, - } - } - pub fn intersect_ray(&self, ray: Ray2d) -> Option { - if let Some(result) = ray.plane_intersection_point(self.position, self.plane) { - return Some(StagePlaneIntersectResult { - position: result - }); - } - None - } +/// Owns the stage geometry: which scene gets spawned and how it is drawn. +#[derive(Default)] +pub struct StagePlugin; - pub fn project(&self, vec: Vec2) -> Vec2 { - vec - vec.project_onto_normalized(self.plane.normal.as_vec2()) +impl Plugin for StagePlugin { + fn build(&self, app: &mut App) { + app.add_systems(Startup, scene::test_scene.spawn()) + .add_systems(Update, debug::debug_draw_scene); } } - -pub fn debug_draw_scene(mut gizmos: Gizmos, planes: Query<&StagePlane>) { - for plane in planes { - let plane_pos_3d = Vec3::new(plane.position.x, plane.position.y, 0.0); - let plane_normal_3d = Vec3::new(plane.plane.normal.x, plane.plane.normal.y, 0.0); - let plane_basis_x_3d = Vec3::new(plane.basis_x.x, plane.basis_x.y, 0.0); - gizmos.rect( - Transform::from_translation(plane_pos_3d).looking_to(plane_normal_3d, Dir3::Z).to_isometry(), - Vec2::new(plane.half_size * 2.0, 1.0), - GREEN, - ); - gizmos.line(plane_pos_3d, plane_pos_3d + plane_basis_x_3d, RED); - } -} - -/// set up a simple 3D scene -pub fn test_scene() -> impl SceneList { - let up = Dir2::Y; - bsn_list! [ - ( - #Cube - Mesh3d(asset_value(Cuboid::new(5.0, 1.0, 1.0))) - MeshMaterial3d::(asset_value(Color::srgb_u8(124, 144, 255))) - Transform::from_xyz(0.0, 0.5, 0.0) - ), - ( - #StagePlane - template_value(StagePlane::new(up, 2.5, Vec2::new(0.0, 1.0))) - ), - ( - PointLight { - shadow_maps_enabled: true, - } - Transform::from_xyz(4.0, 8.0, 4.0) - ), - ( - Camera3d - template_value(Transform::from_xyz(0.0, 1.5, 9.0)) - /*FreeCamera { - sensitivity: 0.2, - friction: 25.0, - walk_speed: 3.0, - run_speed: 9.0, - }*/ - ) - ] -} diff --git a/src/stage/scene.rs b/src/stage/scene.rs new file mode 100644 index 0000000..0e2551c --- /dev/null +++ b/src/stage/scene.rs @@ -0,0 +1,37 @@ +//! The scene content that gets spawned at startup. + +use bevy::prelude::*; + +use crate::stage::StageLine; + +/// set up a simple 3D scene +pub fn test_scene() -> impl SceneList { + bsn_list! [ + ( + #Cube + Mesh3d(asset_value(Cuboid::new(5.0, 1.0, 1.0))) + MeshMaterial3d::(asset_value(Color::srgb_u8(124, 144, 255))) + Transform::from_xyz(0.0, 0.5, 0.0) + ), + ( + #StagePlane + template_value(StageLine::new(Vec2::new(-2.5, 1.0), Vec2::new(2.5, 1.0)).unwrap()) + ), + ( + PointLight { + shadow_maps_enabled: true, + } + Transform::from_xyz(4.0, 8.0, 4.0) + ), + ( + Camera3d + template_value(Transform::from_xyz(0.0, 1.5, 9.0)) + /*FreeCamera { + sensitivity: 0.2, + friction: 25.0, + walk_speed: 3.0, + run_speed: 9.0, + }*/ + ) + ] +}