Initial work on rollback and refactor
This commit is contained in:
Generated
+268
-5
@@ -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"
|
||||
|
||||
@@ -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]
|
||||
|
||||
+15
@@ -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
@@ -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() {}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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]
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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() {}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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 {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
fn remove(&self, state_context: &FighterStateContext, commands: &mut Commands) {
|
||||
commands.entity(state_context.entity).remove::<Self>();
|
||||
}
|
||||
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<T: FighterState + Component + std::fmt::Debug>(
|
||||
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<T: FighterState + Component + std::fmt::Debug>(
|
||||
};
|
||||
if let Some(new_state) = state.check_interrupt(&state_context) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
pub mod wait;
|
||||
pub mod walk;
|
||||
pub mod dash;
|
||||
pub mod run;
|
||||
pub mod grounded;
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
pub mod player;
|
||||
pub mod logic;
|
||||
pub mod plugin;
|
||||
@@ -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 {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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>,
|
||||
}
|
||||
@@ -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>();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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
@@ -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,
|
||||
|
||||
+33
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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<StagePlaneIntersectResult> {
|
||||
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::<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,
|
||||
}*/
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}*/
|
||||
)
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user