Initial work on rollback and refactor

This commit is contained in:
2026-08-12 12:27:35 +02:00
parent ad6c1e17ac
commit a21f24b2d9
45 changed files with 1451 additions and 717 deletions
Generated
+268 -5
View File
@@ -192,12 +192,56 @@ version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" 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]] [[package]]
name = "anstyle" name = "anstyle"
version = "1.0.14" version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" 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]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.104" version = "1.0.104"
@@ -1007,6 +1051,19 @@ dependencies = [
"smol_str", "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]] [[package]]
name = "bevy_gilrs" name = "bevy_gilrs"
version = "0.19.0" version = "0.19.0"
@@ -2072,6 +2129,15 @@ dependencies = [
"uuid", "uuid",
] ]
[[package]]
name = "bincode"
version = "1.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "bit-set" name = "bit-set"
version = "0.9.1" version = "0.9.1"
@@ -2087,6 +2153,42 @@ version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" 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]] [[package]]
name = "bitflags" name = "bitflags"
version = "1.3.2" version = "1.3.2"
@@ -2357,21 +2459,36 @@ dependencies = [
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.6.5" version = "4.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "301b56658598e48f3648647ac6fc887be7e7108eddfa4e9b63fcf3ec58c0cadf" checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
"clap_derive",
] ]
[[package]] [[package]]
name = "clap_builder" name = "clap_builder"
version = "4.6.5" version = "4.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94a65403d1a1bd28f7dc68eb8506e8874808ee5eecb59298de588e2e1407a078" checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889"
dependencies = [ dependencies = [
"anstream",
"anstyle", "anstyle",
"clap_lex", "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]] [[package]]
@@ -2420,6 +2537,12 @@ dependencies = [
"bytemuck", "bytemuck",
] ]
[[package]]
name = "colorchoice"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
[[package]] [[package]]
name = "combine" name = "combine"
version = "4.6.7" version = "4.6.7"
@@ -3281,7 +3404,11 @@ dependencies = [
"bevy", "bevy",
"bevy-inspector-egui", "bevy-inspector-egui",
"bevy_egui", "bevy_egui",
"bevy_ggrs",
"bitfields",
"clap",
"jackdaw_runtime", "jackdaw_runtime",
"serde",
] ]
[[package]] [[package]]
@@ -3310,6 +3437,17 @@ dependencies = [
"windows-link", "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]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.3.4" version = "0.3.4"
@@ -3335,6 +3473,32 @@ dependencies = [
"wasm-bindgen", "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]] [[package]]
name = "gilrs" name = "gilrs"
version = "0.11.2" version = "0.11.2"
@@ -3620,6 +3784,12 @@ dependencies = [
"stable_deref_trait", "stable_deref_trait",
] ]
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]] [[package]]
name = "hermit-abi" name = "hermit-abi"
version = "0.5.2" version = "0.5.2"
@@ -3908,6 +4078,15 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "instant"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
dependencies = [
"cfg-if",
]
[[package]] [[package]]
name = "inventory" name = "inventory"
version = "0.3.24" version = "0.3.24"
@@ -3928,6 +4107,12 @@ dependencies = [
"windows-sys 0.61.2", "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]] [[package]]
name = "itertools" name = "itertools"
version = "0.10.5" version = "0.10.5"
@@ -5061,6 +5246,12 @@ version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "once_cell_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]] [[package]]
name = "oorandom" name = "oorandom"
version = "11.1.5" version = "11.1.5"
@@ -5553,13 +5744,24 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "019b4b213425016d7d84a153c4c73afb0946fbb4840e4eece7ba8848b9d6da22" 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]] [[package]]
name = "rand" name = "rand"
version = "0.9.5" version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41"
dependencies = [ dependencies = [
"rand_chacha", "rand_chacha 0.9.0",
"rand_core 0.9.5", "rand_core 0.9.5",
] ]
@@ -5573,6 +5775,16 @@ dependencies = [
"rand_core 0.10.1", "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]] [[package]]
name = "rand_chacha" name = "rand_chacha"
version = "0.9.0" version = "0.9.0"
@@ -5583,6 +5795,15 @@ dependencies = [
"rand_core 0.9.5", "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]] [[package]]
name = "rand_core" name = "rand_core"
version = "0.9.5" version = "0.9.5"
@@ -5909,6 +6130,12 @@ dependencies = [
"tiny-skia", "tiny-skia",
] ]
[[package]]
name = "seahash"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
[[package]] [[package]]
name = "self_cell" name = "self_cell"
version = "1.3.0" version = "1.3.0"
@@ -6244,6 +6471,30 @@ dependencies = [
"precomputed-hash", "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]] [[package]]
name = "svg_fmt" name = "svg_fmt"
version = "0.4.5" version = "0.4.5"
@@ -6687,6 +6938,12 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "1.24.0" version = "1.24.0"
@@ -6716,6 +6973,12 @@ dependencies = [
"syn 2.0.119", "syn 2.0.119",
] ]
[[package]]
name = "varinteger"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ea29db9f94ff08bb619656b8120878f280526f71dc88b5262c958a510181812"
[[package]] [[package]]
name = "vec_map" name = "vec_map"
version = "0.8.2" version = "0.8.2"
+4
View File
@@ -24,6 +24,10 @@ avian3d = "0.7"
arrayvec = "0.7.8" arrayvec = "0.7.8"
bevy-inspector-egui = "0.37.0" bevy-inspector-egui = "0.37.0"
bevy_egui = "0.40.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. # Enable a small amount of optimization in the dev profile.
[profile.dev] [profile.dev]
+15
View File
@@ -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
}
-100
View File
@@ -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<Time<Fixed>>,
) {
}
pub fn apply_motion(
fighters: Query<(
&FighterVelocity,
&mut FighterTranslation,
&mut FighterPreviousTranslation,
)>,
_time: Res<Time<Fixed>>,
) {
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() {}
+39
View File
@@ -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)
}
}
+31
View File
@@ -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);
}
}
}
}
-16
View File
@@ -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,
}
+30
View File
@@ -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(())
}
+21
View File
@@ -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]
}
}
+86
View File
@@ -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::wait::WaitState>,
state_interrupt_system::<state::walk::WalkState>,
state_interrupt_system::<state::dash::DashState>,
state_interrupt_system::<state::run::RunState>,
)
.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::<FighterECB>()
.rollback_component_with_copy::<FighterVelocity>()
.rollback_component_with_copy::<FighterTranslation>()
.rollback_component_with_copy::<FighterPreviousTranslation>()
.rollback_component_with_copy::<FighterAttributes>()
.rollback_component_with_copy::<Grounded>()
.rollback_component_with_copy::<state::wait::WaitState>()
.rollback_component_with_copy::<state::walk::WalkState>()
.rollback_component_with_copy::<state::dash::DashState>()
.rollback_component_with_copy::<state::run::RunState>()
.checksum_component::<state::dash::DashState>(state::dash::hash_dash_state)
// Debug views.
.add_systems(FixedPostUpdate, debug::debug_draw_ecb)
.add_systems(EguiPrimaryContextPass, debug::fighter_debug);
}
}
+35
View File
@@ -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<Time<Fixed>>) {}
pub fn apply_motion(
fighters: Query<(
&FighterVelocity,
&mut FighterTranslation,
&mut FighterPreviousTranslation,
)>,
_time: Res<Time<Fixed>>,
) {
for (velocity, mut translation, mut prev_translation) in fighters {
prev_translation.0 = translation.0;
translation.0 += velocity.0;
}
}
pub fn fighter_movement() {}
-48
View File
@@ -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::<states::wait::WaitState>,
state_interrupt_system::<states::walk::WalkState>,
state_interrupt_system::<states::dash::DashState>,
state_interrupt_system::<states::run::RunState>,
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(())
}
+116
View File
@@ -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<FighterStateTransition> {
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<f32> {
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<FighterStateTransition> {
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<GameSettings>,
) {
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<Insert, DashState>,
query: Query<
(
Entity,
&mut FighterInput,
&FighterAttributes,
&mut FighterVelocity,
),
With<DashState>,
>,
) {
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();
}
}
}
@@ -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 { pub fn apply_grounded_friction(friction: f32, ground_vel: f32) -> f32 {
if friction.abs() > ground_vel.abs() { 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,
pub fn compute_ground_accel(accel: f32, target_vel: f32, gr_vel: f32, attribs: &FighterAttributes, game_settings: &GameSettings) -> f32 { target_vel: f32,
gr_vel: f32,
attribs: &FighterAttributes,
game_settings: &GameSettings,
) -> f32 {
if target_vel == 0.0 { if target_vel == 0.0 {
apply_grounded_friction(attribs.ground_friction, gr_vel) apply_grounded_friction(attribs.ground_friction, gr_vel)
} else { } else {
@@ -46,24 +53,3 @@ pub fn compute_ground_accel(accel: f32, target_vel: f32, gr_vel: f32, attribs: &
accel 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)
}
}
@@ -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 { pub enum FighterStateTransition {
Wait, Wait,
Walk, Walk,
Dash, Dash(f32),
Run, Run,
} }
pub trait FighterState : Component + Sized { pub trait FighterState: Component + Sized {
fn remove(&self, fn remove(&self, state_context: &FighterStateContext, commands: &mut Commands) {
state_context: &FighterStateContext,
commands: &mut Commands) {
commands.entity(state_context.entity).remove::<Self>(); commands.entity(state_context.entity).remove::<Self>();
} }
fn check_interrupt( fn check_interrupt(
@@ -24,17 +34,21 @@ pub trait FighterState : Component + Sized {
pub struct FighterStateContext<'a> { pub struct FighterStateContext<'a> {
pub input: &'a mut FighterInput, pub input: &'a mut FighterInput,
pub fighter_attribs: &'a FighterAttributes, pub fighter_attribs: &'a FighterAttributes,
pub game_settings: &'a Res<'a, GameSettings>, pub game_settings: &'a GameSettings,
pub entity: Entity, 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); let mut ent_cmd = commands.entity(state_context.entity);
match transition { match transition {
FighterStateTransition::Wait => ent_cmd.insert(states::wait::WaitState), FighterStateTransition::Wait => ent_cmd.insert(wait::WaitState),
FighterStateTransition::Walk => ent_cmd.insert(states::walk::WalkState), FighterStateTransition::Walk => ent_cmd.insert(walk::WalkState),
FighterStateTransition::Dash => ent_cmd.insert(states::dash::DashState::default()), FighterStateTransition::Dash(dir) => ent_cmd.insert(dash::DashState::new(dir)),
FighterStateTransition::Run => ent_cmd.insert(states::run::RunState), FighterStateTransition::Run => ent_cmd.insert(run::RunState),
}; };
} }
@@ -42,7 +56,7 @@ fn get_state_debug_name(transition: &FighterStateTransition) -> &'static str {
match transition { match transition {
FighterStateTransition::Wait => "wait", FighterStateTransition::Wait => "wait",
FighterStateTransition::Walk => "walk", FighterStateTransition::Walk => "walk",
FighterStateTransition::Dash => "dash", FighterStateTransition::Dash(_) => "dash",
FighterStateTransition::Run => "run", FighterStateTransition::Run => "run",
} }
} }
@@ -62,7 +76,6 @@ pub fn state_interrupt_system<T: FighterState + Component + std::fmt::Debug>(
mut commands: Commands, mut commands: Commands,
) { ) {
for (ent, state, mut input, attribs) in query { for (ent, state, mut input, attribs) in query {
info!("{:?}", state);
let state_context = FighterStateContext { let state_context = FighterStateContext {
input: &mut input, input: &mut input,
fighter_attribs: &attribs, fighter_attribs: &attribs,
@@ -71,7 +84,10 @@ pub fn state_interrupt_system<T: FighterState + Component + std::fmt::Debug>(
}; };
if let Some(new_state) = state.check_interrupt(&state_context) { if let Some(new_state) = state.check_interrupt(&state_context) {
commands.entity(ent).remove::<T>(); commands.entity(ent).remove::<T>();
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); create_state(new_state, state_context, &mut commands);
} }
} }
+49
View File
@@ -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<FighterStateTransition> {
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<FighterStateTransition> {
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<RunState>>,
game_settings: Res<GameSettings>,
) {
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;
}
}
+43
View File
@@ -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<FighterStateTransition> {
walk::check_input(state_context)
}
}
pub fn check_input(state_context: &FighterStateContext) -> Option<FighterStateTransition> {
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<WaitState>>,
game_settings: Res<GameSettings>,
) {
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);
}
}
+43
View File
@@ -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<FighterStateTransition> {
wait::check_input(state_context).or_else(|| dash::check_input(state_context))
}
}
pub fn check_input(state_context: &FighterStateContext) -> Option<FighterStateTransition> {
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<WalkState>>,
game_settings: Res<GameSettings>,
) {
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;
}
}
-5
View File
@@ -1,5 +0,0 @@
pub mod wait;
pub mod walk;
pub mod dash;
pub mod run;
pub mod grounded;
-116
View File
@@ -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<FighterStateTransition> {
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<f32> {
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<GameSettings>,
) {
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<FighterStateTransition> {
check_smash_input_with_dir(state_context).map(|_| FighterStateTransition::Dash)
}
pub fn on_insert_dash(
add: On<Insert, DashState>,
query: Query<
(
Entity,
&mut FighterInput,
&FighterAttributes,
&mut FighterVelocity,
),
With<DashState>,
>,
) {
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();
}
}
}
-44
View File
@@ -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<FighterStateTransition> {
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<crate::fighter::state::FighterStateTransition> {
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<RunState>>,
game_settings: Res<GameSettings>,
) {
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;
}
}
}
-34
View File
@@ -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<FighterStateTransition> {
walk::check_input(state_context)
}
}
pub fn check_input(state_context: &FighterStateContext) -> Option<FighterStateTransition> {
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<WaitState>>, game_settings: Res<GameSettings>) {
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);
}
}
-46
View File
@@ -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<FighterStateTransition> {
wait::check_input(state_context).or_else(|| dash::check_input(state_context))
}
}
pub fn walk_update(
fighters: Query<(
&mut FighterVelocity,
&FighterAttributes,
&FighterInput,
), With<WalkState>>,
game_settings: Res<GameSettings>,
) {
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<FighterStateTransition> {
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
}
+4 -2
View File
@@ -11,7 +11,8 @@ pub struct InputSettingsCommon {
pub smash_input_reset_axis_threshold: f32, pub smash_input_reset_axis_threshold: f32,
pub smash_input_axis_threshold: f32, pub smash_input_axis_threshold: f32,
pub run_stick_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 { impl Default for InputSettingsCommon {
@@ -22,7 +23,8 @@ impl Default for InputSettingsCommon {
smash_input_reset_axis_threshold: 0.25, smash_input_reset_axis_threshold: 0.25,
smash_input_axis_threshold: 0.8, smash_input_axis_threshold: 0.8,
run_stick_threshold: 0.625, run_stick_threshold: 0.625,
smash_input_frame_threshold: 2 smash_input_frame_threshold: 2,
input_buffer_size: 5
} }
} }
} }
-3
View File
@@ -1,3 +0,0 @@
pub mod player;
pub mod logic;
pub mod plugin;
+130
View File
@@ -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<FighterCommands> 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<FighterCommands> 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<PlayerInputs<GGRSCfg>>,
game_settings: Res<GameSettings>,
) {
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 {}
}
}
}
+12
View File
@@ -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);
+10
View File
@@ -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,
}
+55
View File
@@ -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<ButtonInput<KeyCode>>,
input_map: Res<BaseInputMap>,
local_players: Res<LocalPlayers>,
) {
// 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::<GGRSCfg>(local_inputs));
}
-43
View File
@@ -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<ButtonInput<KeyCode>>, input_map: Res<BaseInputMap>, 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);
}
}
+31
View File
@@ -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<KeyboardInputMapElement>,
}
+51
View File
@@ -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::<FighterInput>();
}
}
-99
View File
@@ -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<InputFrame>
}
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<f32> {
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<KeyboardInputMapElement>
}
#[derive(Component, Debug)]
#[relationship(relationship_target = ControlledBy)]
pub struct Controls(pub Entity);
#[derive(Component, Debug)]
#[relationship_target(relationship = Controls)]
pub struct ControlledBy(Entity);
-27
View File
@@ -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());
}
}
+32 -6
View File
@@ -22,14 +22,29 @@
//! appears in `Add Component`. No registration code is needed. //! appears in `Add Component`. No registration code is needed.
use bevy::prelude::*; use bevy::prelude::*;
use clap::Parser;
use jackdaw_runtime::prelude::*; 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 fighter;
pub mod input; pub mod input;
pub mod stage; pub mod stage;
pub mod game_settings; 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 /// Your game's Bevy plugin. The editor finds it by this name (override
/// with `plugin = "..."` in jackdaw.toml) and runs it on Play; the /// with `plugin = "..."` in jackdaw.toml) and runs it on Play; the
@@ -41,6 +56,9 @@ pub fn setup(mut commands: Commands) {
let _fighter = commands let _fighter = commands
.spawn(( .spawn((
Name("Fighter".into()), Name("Fighter".into()),
Player {
handle: 0,
},
FighterECB { FighterECB {
vertical_half: 0.5, vertical_half: 0.5,
horizontal_half: 0.25, horizontal_half: 0.25,
@@ -59,19 +77,27 @@ pub fn setup(mut commands: Commands) {
dash_initial_velocity: 0.19, dash_initial_velocity: 0.19,
}, },
WaitState {}, WaitState {},
input::player::FighterInput::default() FighterInput::default()
)) ))
.id(); .id();
} }
impl Plugin for GamePlugin { impl Plugin for GamePlugin {
fn build(&self, app: &mut App) { 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(Startup, setup)
.add_systems(Update, spin_cubes) .add_systems(Update, spin_cubes)
.add_systems(Update, debug_draw_scene) .insert_resource(args::Args::parse())
.add_plugins(input::plugin::FighterInputPlugin)
.add_plugins(fighter::FighterPlugin)
.insert_resource(GameSettings { .insert_resource(GameSettings {
figher_common: FighterSettingsCommon { figher_common: FighterSettingsCommon {
walk_speed_ease: 0.5, walk_speed_ease: 0.5,
+33
View File
@@ -0,0 +1,33 @@
use bevy::math::{Vec2, primitives::Segment2d};
pub trait SegmentIntersection {
fn segment_intersection(&self, rhs: &Segment2d) -> Option<Vec2>;
}
impl SegmentIntersection for Segment2d {
fn segment_intersection(&self, rhs: &Segment2d) -> Option<Vec2> {
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
}
}
}
+30
View File
@@ -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<Res<Session<GGRSCfg>>>,
) -> 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(())
}
+31
View File
@@ -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<crate::input::FighterInputFrame>;
#[derive(Default)]
pub struct FighterNetcodePlugin;
impl Plugin for FighterNetcodePlugin {
fn build(&self, app: &mut App) {
let plugin = GgrsPlugin::<GGRSCfg>::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::<Transform>()
.rollback_component_with_clone::<Name>()
.add_systems(EguiPrimaryContextPass, debug::network_debug)
.add_systems(Update, session::print_events_system)
.insert_resource(RollbackFrameRate(60));
}
}
+48
View File
@@ -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<Args>) -> Result {
let mut sess_build = SessionBuilder::<GGRSCfg>::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<Session<GGRSCfg>>) {
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:?}"),
}
}
}
_ => {}
}
}
+14
View File
@@ -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,
}
+44
View File
@@ -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(),
);
}
}
+16
View File
@@ -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);
}
}
+34
View File
@@ -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<Self, InvalidDirectionError> {
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<StagePlaneIntersectResult> {
self.segment
.segment_intersection(&segment)
.map(|position| StagePlaneIntersectResult { position })
}
pub fn project(&self, vec: Vec2) -> Vec2 {
vec - vec.project_onto_normalized(self.normal)
}
}
+14 -80
View File
@@ -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)] use bevy::prelude::*;
#[reflect(Component)]
pub struct StagePlane {
pub plane: Plane2d,
pub position: Vec2,
half_size: f32,
basis_x: Vec2,
}
pub struct StagePlaneIntersectResult { pub mod debug;
pub position: Vec2 pub mod line;
} pub mod scene;
impl StagePlane { pub use line::{StageLine, StagePlaneIntersectResult};
pub fn new(normal: Dir2, half_size: f32, position: Vec2) -> Self {
let orthogonal = normal.perp();
let basis_x = orthogonal * half_size;
Self { /// Owns the stage geometry: which scene gets spawned and how it is drawn.
plane: Plane2d::new(*normal), #[derive(Default)]
position, pub struct StagePlugin;
half_size,
basis_x,
}
}
pub fn intersect_ray(&self, ray: Ray2d) -> Option<StagePlaneIntersectResult> {
if let Some(result) = ray.plane_intersection_point(self.position, self.plane) {
return Some(StagePlaneIntersectResult {
position: result
});
}
None
}
pub fn project(&self, vec: Vec2) -> Vec2 { impl Plugin for StagePlugin {
vec - vec.project_onto_normalized(self.plane.normal.as_vec2()) 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::<StandardMaterial>(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,
}*/
)
]
}
+37
View File
@@ -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::<StandardMaterial>(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,
}*/
)
]
}