Add animation baking system
This commit is contained in:
@@ -1,2 +1,3 @@
|
|||||||
/target
|
/target
|
||||||
/.jackdaw
|
/.jackdaw
|
||||||
|
imported_assets
|
||||||
Generated
+22
-2
@@ -491,6 +491,12 @@ version = "1.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "be5eb007b7cacc6c660343e96f650fedf4b5a77512399eb952ca6642cf8d13f7"
|
checksum = "be5eb007b7cacc6c660343e96f650fedf4b5a77512399eb952ca6642cf8d13f7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64"
|
||||||
|
version = "0.13.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base64"
|
name = "base64"
|
||||||
version = "0.22.1"
|
version = "0.22.1"
|
||||||
@@ -1207,7 +1213,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "84e21ad83b7dc8d456491ea3d50057685d8c4cca8bc52a1b4a7161c86c369842"
|
checksum = "84e21ad83b7dc8d456491ea3d50057685d8c4cca8bc52a1b4a7161c86c369842"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-lock",
|
"async-lock",
|
||||||
"base64",
|
"base64 0.22.1",
|
||||||
"bevy_animation",
|
"bevy_animation",
|
||||||
"bevy_app",
|
"bevy_app",
|
||||||
"bevy_asset",
|
"bevy_asset",
|
||||||
@@ -3516,11 +3522,14 @@ dependencies = [
|
|||||||
"bevy_egui",
|
"bevy_egui",
|
||||||
"bevy_ggrs",
|
"bevy_ggrs",
|
||||||
"bevy_wind_waker_shader",
|
"bevy_wind_waker_shader",
|
||||||
|
"bincode",
|
||||||
"bitfields",
|
"bitfields",
|
||||||
"clap",
|
"clap",
|
||||||
"fixed",
|
"fixed",
|
||||||
"gc-adapter-neo",
|
"gc-adapter-neo",
|
||||||
|
"gltf",
|
||||||
"jackdaw_runtime",
|
"jackdaw_runtime",
|
||||||
|
"ron 0.12.2",
|
||||||
"serde",
|
"serde",
|
||||||
"strum",
|
"strum",
|
||||||
"strum_macros",
|
"strum_macros",
|
||||||
@@ -3733,10 +3742,13 @@ version = "1.4.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e3ce1918195723ce6ac74e80542c5a96a40c2b26162c1957a5cd70799b8cacf7"
|
checksum = "e3ce1918195723ce6ac74e80542c5a96a40c2b26162c1957a5cd70799b8cacf7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"base64 0.13.1",
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"gltf-json",
|
"gltf-json",
|
||||||
|
"image",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"urlencoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4164,6 +4176,8 @@ dependencies = [
|
|||||||
"num-traits",
|
"num-traits",
|
||||||
"png",
|
"png",
|
||||||
"tiff",
|
"tiff",
|
||||||
|
"zune-core",
|
||||||
|
"zune-jpeg",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6205,7 +6219,7 @@ version = "0.11.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "db09040cc89e461f1a265139777a2bde7f8d8c67c4936f700c63ce3e2904d468"
|
checksum = "db09040cc89e461f1a265139777a2bde7f8d8c67c4936f700c63ce3e2904d468"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64 0.22.1",
|
||||||
"bitflags 2.13.1",
|
"bitflags 2.13.1",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
@@ -7170,6 +7184,12 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "urlencoding"
|
||||||
|
version = "2.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "utf8_iter"
|
name = "utf8_iter"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
|
|||||||
+4
-1
@@ -15,7 +15,7 @@ authors = [""]
|
|||||||
[workspace]
|
[workspace]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bevy = { version = "0.19", features = ["dynamic_linking", "free_camera"] }
|
bevy = { version = "0.19", features = ["asset_processor", "dynamic_linking", "free_camera"] }
|
||||||
# Loads the scenes you author in the editor (assets/*.bsn) and builds
|
# Loads the scenes you author in the editor (assets/*.bsn) and builds
|
||||||
# physics colliders from them. An ordinary dependency; remove it if you
|
# physics colliders from them. An ordinary dependency; remove it if you
|
||||||
# stop using authored scenes.
|
# stop using authored scenes.
|
||||||
@@ -26,6 +26,9 @@ 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" }
|
bevy_ggrs = { git = "https://github.com/gschup/bevy_ggrs" }
|
||||||
serde = "1.0.229"
|
serde = "1.0.229"
|
||||||
|
bincode = "1.3.3"
|
||||||
|
gltf = "1.4.1"
|
||||||
|
ron = "0.12.0"
|
||||||
bitfields = "3.0.0"
|
bitfields = "3.0.0"
|
||||||
clap = { version = "4.6.6", features = ["derive"] }
|
clap = { version = "4.6.6", features = ["derive"] }
|
||||||
bevy_wind_waker_shader = { git = "https://github.com/lavaeater/bevy_wind_waker_shader.git" }
|
bevy_wind_waker_shader = { git = "https://github.com/lavaeater/bevy_wind_waker_shader.git" }
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
(
|
(
|
||||||
id: TestFighter,
|
id: TestFighter,
|
||||||
model_path: "fighters/test/test-fighter-normal.glb",
|
model_path: "fighters/test/test-fighter-normal.glb",
|
||||||
|
baked_animation_path: "fighters/test/test.fighterbake",
|
||||||
attributes: (
|
attributes: (
|
||||||
base_walk_accel: "0.1",
|
base_walk_accel: "0.1",
|
||||||
stick_walk_accel: "0.16",
|
stick_walk_accel: "0.16",
|
||||||
max_walk_vel: "0.7",
|
max_walk_vel: "0.7",
|
||||||
ground_friction: "0.02",
|
ground_friction: "0.09",
|
||||||
dash_acceleration_duration: 13,
|
dash_acceleration_duration: 13,
|
||||||
dash_duration: 24,
|
dash_duration: 24,
|
||||||
base_dash_accel: "0.02",
|
base_dash_accel: "0.02",
|
||||||
@@ -32,6 +33,14 @@
|
|||||||
air_dodge_velocity: "2.790",
|
air_dodge_velocity: "2.790",
|
||||||
air_dodge_decay: "0.9"
|
air_dodge_decay: "0.9"
|
||||||
),
|
),
|
||||||
|
camera: (
|
||||||
|
vertical_origin_offset: "10.0",
|
||||||
|
forward_extent: "25.5",
|
||||||
|
backward_extent: "2.5",
|
||||||
|
upward_extent: "15.0",
|
||||||
|
downward_extent: "5.0",
|
||||||
|
visibility_radius: "5.0",
|
||||||
|
),
|
||||||
animations: {
|
animations: {
|
||||||
Wait: "PlyPurin5K_Share_ACTION_Wait",
|
Wait: "PlyPurin5K_Share_ACTION_Wait",
|
||||||
Walk: "PlyPurin5K_Share_ACTION_WalkFast",
|
Walk: "PlyPurin5K_Share_ACTION_WalkFast",
|
||||||
@@ -43,6 +52,7 @@
|
|||||||
Landing: "PlyPurin5K_Share_ACTION_Landing",
|
Landing: "PlyPurin5K_Share_ACTION_Landing",
|
||||||
AirDodge: "PlyPurin5K_Share_ACTION_EscapeAir",
|
AirDodge: "PlyPurin5K_Share_ACTION_EscapeAir",
|
||||||
DoubleJump: "PlyPurin5K_Share_ACTION_JumpAerialF1",
|
DoubleJump: "PlyPurin5K_Share_ACTION_JumpAerialF1",
|
||||||
Fall: "PlyPurin5K_Share_ACTION_Fall"
|
Fall: "PlyPurin5K_Share_ACTION_Fall",
|
||||||
|
Turn: "PlyPurin5K_Share_ACTION_Turn",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
(
|
||||||
|
manifest_path: "fighters/test/test.fighter.ron",
|
||||||
|
)
|
||||||
@@ -1,6 +1,25 @@
|
|||||||
(
|
(
|
||||||
id: TestStage,
|
id: TestStage,
|
||||||
model_path: "stages/battlefield.glb#Scene0",
|
model_path: "stages/battlefield.glb#Scene0",
|
||||||
|
camera: (
|
||||||
|
left: -75.0,
|
||||||
|
right: 75.0,
|
||||||
|
bottom: -30.0,
|
||||||
|
top: 70.0,
|
||||||
|
origin: (0.0, 15.0),
|
||||||
|
vertical_fov_degrees: 30.0,
|
||||||
|
min_depth: 150.0,
|
||||||
|
max_depth: 400.0,
|
||||||
|
subject_scale: 1.0,
|
||||||
|
tracking_smoothness: 3.0,
|
||||||
|
vertical_tilt_degrees: 0.0,
|
||||||
|
horizontal_pan_coefficient: 0.0,
|
||||||
|
vertical_pan_coefficient: 0.0,
|
||||||
|
max_horizontal_pan_degrees: 17.5,
|
||||||
|
max_upward_pan_degrees: 5.0,
|
||||||
|
max_downward_pan_degrees: 7.0,
|
||||||
|
max_downward_expansion: 40.0,
|
||||||
|
),
|
||||||
polygons: [
|
polygons: [
|
||||||
(
|
(
|
||||||
poly_type: Platform,
|
poly_type: Platform,
|
||||||
|
|||||||
+11
-7
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
|
||||||
use crate::stage::manifest::StageCameraProfile;
|
use crate::{fighter::manifest::FighterManifest, stage::manifest::StageCameraProfile};
|
||||||
|
|
||||||
const EMPTY_FRAME_HALF_SIZE: f32 = 40.0;
|
const EMPTY_FRAME_HALF_SIZE: f32 = 40.0;
|
||||||
const SUBJECT_COUNT_SCALES: [f32; 4] = [1.5, 1.32, 1.16, 1.0];
|
const SUBJECT_COUNT_SCALES: [f32; 4] = [1.5, 1.32, 1.16, 1.0];
|
||||||
@@ -208,7 +208,7 @@ fn update_match_camera(
|
|||||||
fighters: Query<
|
fighters: Query<
|
||||||
(
|
(
|
||||||
&crate::fighter::motion::FighterTranslation,
|
&crate::fighter::motion::FighterTranslation,
|
||||||
&crate::fighter::FighterCameraProfile,
|
&crate::fighter::Fighter,
|
||||||
&crate::fighter::FighterFacingDirection,
|
&crate::fighter::FighterFacingDirection,
|
||||||
),
|
),
|
||||||
With<crate::player::Player>,
|
With<crate::player::Player>,
|
||||||
@@ -219,11 +219,13 @@ fn update_match_camera(
|
|||||||
&mut MatchCamera,
|
&mut MatchCamera,
|
||||||
&StageCameraProfile,
|
&StageCameraProfile,
|
||||||
)>,
|
)>,
|
||||||
|
manifests: Res<Assets<FighterManifest>>
|
||||||
) {
|
) {
|
||||||
let subjects = fighters.iter().map(|(translation, profile, facing)| {
|
let subjects = fighters.iter().map(|(translation, fighter, facing)| {
|
||||||
|
let manifest = manifests.get(&fighter.manifest).expect("Fighter manifest should be valid");
|
||||||
fighter_subject_bounds(
|
fighter_subject_bounds(
|
||||||
Vec2::new(translation.x.to_num(), translation.y.to_num()),
|
Vec2::new(translation.x.to_num(), translation.y.to_num()),
|
||||||
*profile,
|
manifest.camera,
|
||||||
*facing,
|
*facing,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
@@ -296,12 +298,13 @@ fn debug_draw_camera(
|
|||||||
fighters: Query<
|
fighters: Query<
|
||||||
(
|
(
|
||||||
&crate::fighter::motion::FighterTranslation,
|
&crate::fighter::motion::FighterTranslation,
|
||||||
&crate::fighter::FighterCameraProfile,
|
&crate::fighter::Fighter,
|
||||||
&crate::fighter::FighterFacingDirection,
|
&crate::fighter::FighterFacingDirection,
|
||||||
),
|
),
|
||||||
With<crate::player::Player>,
|
With<crate::player::Player>,
|
||||||
>,
|
>,
|
||||||
cameras: Query<(&MatchCamera, &StageCameraProfile)>,
|
cameras: Query<(&MatchCamera, &StageCameraProfile)>,
|
||||||
|
manifests: Res<Assets<FighterManifest>>,
|
||||||
) {
|
) {
|
||||||
for (camera, profile) in &cameras {
|
for (camera, profile) in &cameras {
|
||||||
draw_rectangle(
|
draw_rectangle(
|
||||||
@@ -315,10 +318,11 @@ fn debug_draw_camera(
|
|||||||
Color::srgb(1.0, 0.5, 0.0),
|
Color::srgb(1.0, 0.5, 0.0),
|
||||||
);
|
);
|
||||||
draw_rectangle(&mut gizmos, camera.frame, Color::srgb(1.0, 1.0, 0.0));
|
draw_rectangle(&mut gizmos, camera.frame, Color::srgb(1.0, 1.0, 0.0));
|
||||||
for (translation, fighter_profile, facing) in &fighters {
|
for (translation, fighter, facing) in &fighters {
|
||||||
|
let manifest = manifests.get(&fighter.manifest).expect("Fighter manifest should be valid");
|
||||||
let subject = fighter_subject_bounds(
|
let subject = fighter_subject_bounds(
|
||||||
Vec2::new(translation.x.to_num(), translation.y.to_num()),
|
Vec2::new(translation.x.to_num(), translation.y.to_num()),
|
||||||
*fighter_profile,
|
manifest.camera,
|
||||||
*facing,
|
*facing,
|
||||||
);
|
);
|
||||||
draw_rectangle(
|
draw_rectangle(
|
||||||
|
|||||||
@@ -15,6 +15,37 @@ impl FighterAnimationPlayerLink {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The simulation-owned animation clock. Rendering may interpolate freely, but
|
||||||
|
/// gameplay samples baked data only with this rollback-tracked value.
|
||||||
|
#[derive(Component, Clone, Copy, Debug, Hash, PartialEq, Eq)]
|
||||||
|
pub struct FighterAnimationFrame {
|
||||||
|
pub kind: AnimKind,
|
||||||
|
pub frame: u32,
|
||||||
|
pub repeat: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FighterAnimationFrame {
|
||||||
|
pub const fn new(kind: AnimKind, repeat: bool) -> Self {
|
||||||
|
Self { kind, frame: 0, repeat }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset(&mut self, kind: AnimKind, repeat: bool) {
|
||||||
|
self.kind = kind;
|
||||||
|
self.frame = 0;
|
||||||
|
self.repeat = repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn advance(&mut self) {
|
||||||
|
self.frame = self.frame.saturating_add(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn advance_fighter_animation_frames(mut fighters: Query<&mut FighterAnimationFrame>) {
|
||||||
|
for mut animation in &mut fighters {
|
||||||
|
animation.advance();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn apply_fighter_translation_to_visuals(
|
pub fn apply_fighter_translation_to_visuals(
|
||||||
query: Query<(&FighterTranslation, &FighterECB, &mut Transform)>,
|
query: Query<(&FighterTranslation, &FighterECB, &mut Transform)>,
|
||||||
) {
|
) {
|
||||||
@@ -47,11 +78,16 @@ pub fn setup_fighter_animation_player(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn apply_animation(children: Query<(&mut Transform, &FighterFacingDirection), With<FighterVisual>>) {
|
pub fn apply_animation(children: Query<(&mut Transform, &FighterFacingDirection, &FighterAnimationFrame), With<FighterVisual>>) {
|
||||||
for (mut trf, facing_direction) in children {
|
for (mut trf, facing_direction, frame) in children {
|
||||||
// Melee model scale is in decimeters
|
// Melee model scale is in decimeters
|
||||||
trf.rotation = Quat::IDENTITY;
|
trf.rotation = Quat::IDENTITY;
|
||||||
trf.rotate_local_y(std::f32::consts::PI * 0.5 * facing_direction.to_sign().to_num::<f32>());
|
if let AnimKind::Turn = frame.kind {
|
||||||
|
trf.rotate_local_y(std::f32::consts::PI * -0.5 * facing_direction.to_sign().to_num::<f32>());
|
||||||
|
} else {
|
||||||
|
trf.rotate_local_y(std::f32::consts::PI * 0.5 * facing_direction.to_sign().to_num::<f32>());
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,4 +128,5 @@ pub enum AnimKind {
|
|||||||
Landing,
|
Landing,
|
||||||
AirDodge,
|
AirDodge,
|
||||||
Fall,
|
Fall,
|
||||||
|
Turn,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use crate::{input::FighterInputFrame, math::int::FGi32};
|
|||||||
/// Per-fighter tuning values. These are character data, not global rules —
|
/// Per-fighter tuning values. These are character data, not global rules —
|
||||||
/// anything that applies to every fighter belongs in
|
/// anything that applies to every fighter belongs in
|
||||||
/// [`crate::game_settings::FighterSettingsCommon`] instead.
|
/// [`crate::game_settings::FighterSettingsCommon`] instead.
|
||||||
#[derive(Asset, Component, Clone, Copy, Reflect, Serialize, Deserialize)]
|
#[derive(Asset, Clone, Copy, Reflect, Serialize, Deserialize)]
|
||||||
#[reflect(opaque)]
|
#[reflect(opaque)]
|
||||||
pub struct FighterAttributes {
|
pub struct FighterAttributes {
|
||||||
pub base_walk_accel: FGi32,
|
pub base_walk_accel: FGi32,
|
||||||
|
|||||||
@@ -0,0 +1,528 @@
|
|||||||
|
//! Deterministic, fighter-local skeletal animation samples for gameplay.
|
||||||
|
//!
|
||||||
|
//! Rendering continues to use Bevy's floating-point animation player. Gameplay
|
||||||
|
//! reads this asset instead, so rollback never depends on render-time sampling.
|
||||||
|
|
||||||
|
use std::{collections::HashMap, io, path::Path};
|
||||||
|
|
||||||
|
use bevy::{
|
||||||
|
asset::{
|
||||||
|
AssetLoader, AssetPath, AsyncWriteExt, LoadContext,
|
||||||
|
io::{Reader, Writer},
|
||||||
|
processor::LoadTransformAndSave,
|
||||||
|
saver::{AssetSaver, SavedAsset},
|
||||||
|
transformer::{AssetTransformer, TransformedAsset},
|
||||||
|
},
|
||||||
|
prelude::*,
|
||||||
|
reflect::TypePath,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
fighter::{animation::AnimKind, manifest::FighterManifest},
|
||||||
|
math::int::{FGWide, FGi32},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const BAKED_ANIMATION_FPS: u32 = 60;
|
||||||
|
|
||||||
|
/// Parent transforms are composed after
|
||||||
|
/// quantization, which makes every runtime sample platform-independent.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct FixedMat4 {
|
||||||
|
pub cols: [[FGi32; 4]; 4],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FixedMat4 {
|
||||||
|
fn from_mat4(matrix: Mat4) -> io::Result<Self> {
|
||||||
|
let values = matrix.to_cols_array();
|
||||||
|
let mut cols = [[FGi32::ZERO; 4]; 4];
|
||||||
|
for (index, value) in values.into_iter().enumerate() {
|
||||||
|
cols[index / 4][index % 4] = FGi32::checked_from_num(value).ok_or_else(|| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
"animation transform overflows Q16.16",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
Ok(Self { cols })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mul(self, rhs: Self) -> Self {
|
||||||
|
let mut cols = [[FGi32::ZERO; 4]; 4];
|
||||||
|
for column in 0..4 {
|
||||||
|
for row in 0..4 {
|
||||||
|
let mut value = FGWide::ZERO;
|
||||||
|
for k in 0..4 {
|
||||||
|
value +=
|
||||||
|
FGWide::from_num(self.cols[k][row]) * FGWide::from_num(rhs.cols[column][k]);
|
||||||
|
}
|
||||||
|
cols[column][row] = FGi32::from_num(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self { cols }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct BakedAnimationClip {
|
||||||
|
/// One model-space matrix per stored node, for each 60 Hz simulation frame.
|
||||||
|
pub frames: Vec<Vec<FixedMat4>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Asset, TypePath, Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct BakedFighterAnimations {
|
||||||
|
pub frame_rate: u32,
|
||||||
|
/// Indexed exactly like every frame in each clip.
|
||||||
|
pub bone_names: Vec<String>,
|
||||||
|
clips: Vec<BakedAnimationClip>,
|
||||||
|
clip_indices: HashMap<AnimKind, usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BakedFighterAnimations {
|
||||||
|
pub fn sample(&self, kind: AnimKind, frame: u32, bone: &str) -> Option<FixedMat4> {
|
||||||
|
let bone_index = self.bone_names.iter().position(|name| name == bone)?;
|
||||||
|
let frames = &self.clip(kind)?.frames;
|
||||||
|
let pose = frames.get(frame as usize).or_else(|| frames.last())?;
|
||||||
|
pose.get(bone_index).copied()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn frame_count(&self, kind: AnimKind) -> Option<u32> {
|
||||||
|
self.clip(kind).map(|clip| clip.frames.len() as u32)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clip(&self, kind: AnimKind) -> Option<&BakedAnimationClip> {
|
||||||
|
self.clip_indices
|
||||||
|
.get(&kind)
|
||||||
|
.and_then(|index| self.clips.get(*index))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct FighterBakeDeclaration {
|
||||||
|
manifest_path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Asset, TypePath)]
|
||||||
|
struct FighterBakeSource {
|
||||||
|
model_bytes: Vec<u8>,
|
||||||
|
animations: HashMap<AnimKind, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, TypePath)]
|
||||||
|
struct FighterBakeSourceLoader;
|
||||||
|
|
||||||
|
impl AssetLoader for FighterBakeSourceLoader {
|
||||||
|
type Asset = FighterBakeSource;
|
||||||
|
type Settings = ();
|
||||||
|
type Error = io::Error;
|
||||||
|
|
||||||
|
async fn load(
|
||||||
|
&self,
|
||||||
|
reader: &mut dyn Reader,
|
||||||
|
_: &Self::Settings,
|
||||||
|
load_context: &mut LoadContext<'_>,
|
||||||
|
) -> Result<Self::Asset, Self::Error> {
|
||||||
|
let mut declaration = Vec::new();
|
||||||
|
reader.read_to_end(&mut declaration).await?;
|
||||||
|
let declaration: FighterBakeDeclaration = ron::de::from_bytes(&declaration)
|
||||||
|
.map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
|
||||||
|
|
||||||
|
let manifest_bytes = load_context
|
||||||
|
.read_asset_bytes(&declaration.manifest_path)
|
||||||
|
.await
|
||||||
|
.map_err(|error| io::Error::other(error.to_string()))?;
|
||||||
|
let manifest: FighterManifest = ron::de::from_bytes(&manifest_bytes)
|
||||||
|
.map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
|
||||||
|
|
||||||
|
// The processor's AssetServer reads processed assets. Reading this
|
||||||
|
// source dependency through it would wait for a processed GLB while
|
||||||
|
// this processor is still producing the first processed asset.
|
||||||
|
let model_bytes = std::fs::read(Path::new("assets").join(&manifest.model_path))?;
|
||||||
|
Ok(FighterBakeSource {
|
||||||
|
model_bytes,
|
||||||
|
animations: manifest.animations,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extensions(&self) -> &[&str] {
|
||||||
|
&["fighterbake"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, TypePath)]
|
||||||
|
struct BakedFighterAnimationsLoader;
|
||||||
|
|
||||||
|
impl AssetLoader for BakedFighterAnimationsLoader {
|
||||||
|
type Asset = BakedFighterAnimations;
|
||||||
|
type Settings = ();
|
||||||
|
type Error = io::Error;
|
||||||
|
|
||||||
|
async fn load(
|
||||||
|
&self,
|
||||||
|
reader: &mut dyn Reader,
|
||||||
|
_: &Self::Settings,
|
||||||
|
_: &mut LoadContext<'_>,
|
||||||
|
) -> Result<Self::Asset, Self::Error> {
|
||||||
|
let mut bytes = Vec::new();
|
||||||
|
reader.read_to_end(&mut bytes).await?;
|
||||||
|
bincode::deserialize(&bytes)
|
||||||
|
.map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extensions(&self) -> &[&str] {
|
||||||
|
&["fighterbake"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, TypePath)]
|
||||||
|
struct FighterBakeTransformer;
|
||||||
|
|
||||||
|
impl AssetTransformer for FighterBakeTransformer {
|
||||||
|
type AssetInput = FighterBakeSource;
|
||||||
|
type AssetOutput = BakedFighterAnimations;
|
||||||
|
type Settings = ();
|
||||||
|
type Error = io::Error;
|
||||||
|
|
||||||
|
async fn transform<'a>(
|
||||||
|
&'a self,
|
||||||
|
source: TransformedAsset<Self::AssetInput>,
|
||||||
|
_: &'a Self::Settings,
|
||||||
|
) -> Result<TransformedAsset<Self::AssetOutput>, Self::Error> {
|
||||||
|
let baked = bake_gltf(&source.model_bytes, &source.animations)?;
|
||||||
|
Ok(source.replace_asset(baked))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, TypePath)]
|
||||||
|
struct BakedFighterAnimationsSaver;
|
||||||
|
|
||||||
|
impl AssetSaver for BakedFighterAnimationsSaver {
|
||||||
|
type Asset = BakedFighterAnimations;
|
||||||
|
type Settings = ();
|
||||||
|
type OutputLoader = BakedFighterAnimationsLoader;
|
||||||
|
type Error = io::Error;
|
||||||
|
|
||||||
|
async fn save(
|
||||||
|
&self,
|
||||||
|
writer: &mut Writer,
|
||||||
|
asset: SavedAsset<'_, '_, Self::Asset>,
|
||||||
|
_: &Self::Settings,
|
||||||
|
_: AssetPath<'_>,
|
||||||
|
) -> Result<(), Self::Error> {
|
||||||
|
let bytes = bincode::serialize(&*asset)
|
||||||
|
.map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
|
||||||
|
writer.write_all(&bytes).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type FighterBakeProcessor = LoadTransformAndSave<
|
||||||
|
FighterBakeSourceLoader,
|
||||||
|
FighterBakeTransformer,
|
||||||
|
BakedFighterAnimationsSaver,
|
||||||
|
>;
|
||||||
|
|
||||||
|
pub struct BakedAnimationPlugin;
|
||||||
|
|
||||||
|
impl Plugin for BakedAnimationPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.init_asset::<BakedFighterAnimations>()
|
||||||
|
.register_asset_loader(FighterBakeSourceLoader)
|
||||||
|
.register_asset_loader(BakedFighterAnimationsLoader)
|
||||||
|
.register_asset_processor(FighterBakeProcessor::new(
|
||||||
|
FighterBakeTransformer,
|
||||||
|
BakedFighterAnimationsSaver,
|
||||||
|
))
|
||||||
|
.set_default_asset_processor::<FighterBakeProcessor>("fighterbake");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bake_gltf(
|
||||||
|
bytes: &[u8],
|
||||||
|
animations: &HashMap<AnimKind, String>,
|
||||||
|
) -> io::Result<BakedFighterAnimations> {
|
||||||
|
let (document, buffers, _) = gltf::import_slice(bytes)
|
||||||
|
.map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
|
||||||
|
let nodes: Vec<_> = document.nodes().collect();
|
||||||
|
if nodes.is_empty() {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
"fighter model has no nodes",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let mut parents = vec![None; nodes.len()];
|
||||||
|
for node in &nodes {
|
||||||
|
for child in node.children() {
|
||||||
|
parents[child.index()] = Some(node.index());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let names = nodes
|
||||||
|
.iter()
|
||||||
|
.map(|node| {
|
||||||
|
node.name().map(str::to_owned).ok_or_else(|| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
format!("node {} has no name", node.index()),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<io::Result<Vec<_>>>()?;
|
||||||
|
|
||||||
|
let mut pending = HashMap::<&str, Vec<AnimKind>>::new();
|
||||||
|
for (kind, name) in animations {
|
||||||
|
pending.entry(name).or_default().push(*kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut clips = Vec::with_capacity(pending.len());
|
||||||
|
let mut clip_indices = HashMap::with_capacity(animations.len());
|
||||||
|
for animation in document.animations() {
|
||||||
|
let name = animation.name().ok_or_else(|| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
format!("animation {} has no name", animation.index()),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let Some(kinds) = pending.remove(name) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let channels = animation
|
||||||
|
.channels()
|
||||||
|
.map(|channel| read_channel(channel, &buffers))
|
||||||
|
.collect::<io::Result<Vec<_>>>()?;
|
||||||
|
let duration = channels
|
||||||
|
.iter()
|
||||||
|
.filter_map(Channel::duration)
|
||||||
|
.fold(0.0_f32, f32::max);
|
||||||
|
let frame_count = (duration * BAKED_ANIMATION_FPS as f32).ceil() as usize + 1;
|
||||||
|
let mut frames = Vec::with_capacity(frame_count);
|
||||||
|
for frame in 0..frame_count {
|
||||||
|
let time = frame as f32 / BAKED_ANIMATION_FPS as f32;
|
||||||
|
let local = nodes
|
||||||
|
.iter()
|
||||||
|
.map(|node| local_transform(node.clone(), &channels, time))
|
||||||
|
.collect::<io::Result<Vec<_>>>()?;
|
||||||
|
let mut model = vec![None; nodes.len()];
|
||||||
|
for index in 0..nodes.len() {
|
||||||
|
resolve_model_transform(index, &parents, &local, &mut model)?;
|
||||||
|
}
|
||||||
|
frames.push(model.into_iter().map(Option::unwrap).collect());
|
||||||
|
}
|
||||||
|
let index = clips.len();
|
||||||
|
clips.push(BakedAnimationClip { frames });
|
||||||
|
for kind in kinds {
|
||||||
|
clip_indices.insert(kind, index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if clips.is_empty() {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
"fighter model has no animations",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if !pending.is_empty() {
|
||||||
|
let mut missing = pending.keys().copied().collect::<Vec<_>>();
|
||||||
|
missing.sort_unstable();
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
format!(
|
||||||
|
"fighter model has no animations named {}",
|
||||||
|
missing.join(", ")
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(BakedFighterAnimations {
|
||||||
|
frame_rate: BAKED_ANIMATION_FPS,
|
||||||
|
bone_names: names,
|
||||||
|
clips,
|
||||||
|
clip_indices,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_model_transform(
|
||||||
|
index: usize,
|
||||||
|
parents: &[Option<usize>],
|
||||||
|
local: &[FixedMat4],
|
||||||
|
model: &mut [Option<FixedMat4>],
|
||||||
|
) -> io::Result<FixedMat4> {
|
||||||
|
if let Some(value) = model[index] {
|
||||||
|
return Ok(value);
|
||||||
|
}
|
||||||
|
let value = match parents[index] {
|
||||||
|
Some(parent) => resolve_model_transform(parent, parents, local, model)?.mul(local[index]),
|
||||||
|
None => local[index],
|
||||||
|
};
|
||||||
|
model[index] = Some(value);
|
||||||
|
Ok(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct Channel {
|
||||||
|
node: usize,
|
||||||
|
property: gltf::animation::Property,
|
||||||
|
interpolation: gltf::animation::Interpolation,
|
||||||
|
times: Vec<f32>,
|
||||||
|
values: ChannelValues,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Channel {
|
||||||
|
fn duration(&self) -> Option<f32> {
|
||||||
|
self.times.last().copied()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
enum ChannelValues {
|
||||||
|
Vec3(Vec<[f32; 3]>),
|
||||||
|
Rotation(Vec<[f32; 4]>),
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_channel(
|
||||||
|
channel: gltf::animation::Channel<'_>,
|
||||||
|
buffers: &[gltf::buffer::Data],
|
||||||
|
) -> io::Result<Channel> {
|
||||||
|
let reader = channel.reader(|buffer| Some(&buffers[buffer.index()].0));
|
||||||
|
let times = reader
|
||||||
|
.read_inputs()
|
||||||
|
.ok_or_else(|| {
|
||||||
|
io::Error::new(io::ErrorKind::InvalidData, "animation channel has no input")
|
||||||
|
})?
|
||||||
|
.collect();
|
||||||
|
let values = match reader.read_outputs().ok_or_else(|| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
"animation channel has no output",
|
||||||
|
)
|
||||||
|
})? {
|
||||||
|
gltf::animation::util::ReadOutputs::Translations(values)
|
||||||
|
| gltf::animation::util::ReadOutputs::Scales(values) => {
|
||||||
|
ChannelValues::Vec3(values.collect())
|
||||||
|
}
|
||||||
|
gltf::animation::util::ReadOutputs::Rotations(values) => {
|
||||||
|
ChannelValues::Rotation(values.into_f32().collect())
|
||||||
|
}
|
||||||
|
gltf::animation::util::ReadOutputs::MorphTargetWeights(_) => {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
"morph target animation is not supported for bone baking",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if channel.sampler().interpolation() == gltf::animation::Interpolation::CubicSpline {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
"cubic spline animation is not supported for bone baking",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(Channel {
|
||||||
|
node: channel.target().node().index(),
|
||||||
|
property: channel.target().property(),
|
||||||
|
interpolation: channel.sampler().interpolation(),
|
||||||
|
times,
|
||||||
|
values,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn local_transform(node: gltf::Node<'_>, channels: &[Channel], time: f32) -> io::Result<FixedMat4> {
|
||||||
|
let (mut translation, mut rotation, mut scale) = match node.transform() {
|
||||||
|
gltf::scene::Transform::Decomposed {
|
||||||
|
translation,
|
||||||
|
rotation,
|
||||||
|
scale,
|
||||||
|
} => (
|
||||||
|
Vec3::from(translation),
|
||||||
|
Quat::from_array(rotation),
|
||||||
|
Vec3::from(scale),
|
||||||
|
),
|
||||||
|
gltf::scene::Transform::Matrix { matrix } => {
|
||||||
|
return FixedMat4::from_mat4(Mat4::from_cols_array_2d(&matrix));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
for channel in channels
|
||||||
|
.iter()
|
||||||
|
.filter(|channel| channel.node == node.index())
|
||||||
|
{
|
||||||
|
match (&channel.values, channel.property) {
|
||||||
|
(ChannelValues::Vec3(values), gltf::animation::Property::Translation) => {
|
||||||
|
translation = Vec3::from(sample_vec3(channel, values, time))
|
||||||
|
}
|
||||||
|
(ChannelValues::Vec3(values), gltf::animation::Property::Scale) => {
|
||||||
|
scale = Vec3::from(sample_vec3(channel, values, time))
|
||||||
|
}
|
||||||
|
(ChannelValues::Rotation(values), gltf::animation::Property::Rotation) => {
|
||||||
|
rotation = Quat::from_array(sample_rotation(channel, values, time))
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
"animation property does not match its values",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FixedMat4::from_mat4(Mat4::from_scale_rotation_translation(
|
||||||
|
scale,
|
||||||
|
rotation,
|
||||||
|
translation,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sample_index(channel: &Channel, time: f32) -> (usize, usize, f32) {
|
||||||
|
let next = channel.times.partition_point(|key| *key <= time);
|
||||||
|
if next == 0 {
|
||||||
|
return (0, 0, 0.0);
|
||||||
|
}
|
||||||
|
if next == channel.times.len() {
|
||||||
|
let last = next - 1;
|
||||||
|
return (last, last, 0.0);
|
||||||
|
}
|
||||||
|
let previous = next - 1;
|
||||||
|
let factor = (time - channel.times[previous]) / (channel.times[next] - channel.times[previous]);
|
||||||
|
(previous, next, factor)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sample_vec3(channel: &Channel, values: &[[f32; 3]], time: f32) -> [f32; 3] {
|
||||||
|
let (from, to, factor) = sample_index(channel, time);
|
||||||
|
if channel.interpolation == gltf::animation::Interpolation::Step {
|
||||||
|
return values[from];
|
||||||
|
}
|
||||||
|
Vec3::from(values[from])
|
||||||
|
.lerp(Vec3::from(values[to]), factor)
|
||||||
|
.to_array()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sample_rotation(channel: &Channel, values: &[[f32; 4]], time: f32) -> [f32; 4] {
|
||||||
|
let (from, to, factor) = sample_index(channel, time);
|
||||||
|
if channel.interpolation == gltf::animation::Interpolation::Step {
|
||||||
|
return values[from];
|
||||||
|
}
|
||||||
|
Quat::from_array(values[from])
|
||||||
|
.slerp(Quat::from_array(values[to]), factor)
|
||||||
|
.to_array()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn configured_fighter_clips_bake_to_fixed_point_frames() {
|
||||||
|
let manifest: FighterManifest =
|
||||||
|
ron::from_str(include_str!("../../assets/fighters/test/test.fighter.ron")).unwrap();
|
||||||
|
let baked = bake_gltf(
|
||||||
|
include_bytes!("../../assets/fighters/test/test-fighter-normal.glb"),
|
||||||
|
&manifest.animations,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(baked.frame_rate, BAKED_ANIMATION_FPS);
|
||||||
|
assert!(baked.frame_count(AnimKind::Wait).unwrap() > 1);
|
||||||
|
assert_eq!(baked.clip_indices.len(), manifest.animations.len());
|
||||||
|
assert_eq!(
|
||||||
|
baked.clip_indices[&AnimKind::JumpSquat],
|
||||||
|
baked.clip_indices[&AnimKind::Landing]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
baked.clip(AnimKind::Wait).unwrap().frames[0].len(),
|
||||||
|
baked.bone_names.len()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+30
-5
@@ -4,10 +4,15 @@ use bevy::color::palettes::css::ORANGE;
|
|||||||
use bevy::{color::palettes::css::YELLOW, prelude::*};
|
use bevy::{color::palettes::css::YELLOW, prelude::*};
|
||||||
use bevy_egui::{EguiContexts, egui};
|
use bevy_egui::{EguiContexts, egui};
|
||||||
|
|
||||||
|
use crate::fighter::animation::FighterAnimationFrame;
|
||||||
|
use crate::fighter::baked_animation::BakedFighterAnimations;
|
||||||
use crate::fighter::ecb::FighterPreviousECB;
|
use crate::fighter::ecb::FighterPreviousECB;
|
||||||
|
use crate::fighter::manifest::{FighterManifest, FighterManifestRegistry};
|
||||||
use crate::fighter::state::StateNameDebug;
|
use crate::fighter::state::StateNameDebug;
|
||||||
use crate::fighter::{FighterECB, FighterPreviousTranslation, FighterTranslation, FighterVelocity};
|
use crate::fighter::visual::FighterAnimations;
|
||||||
|
use crate::fighter::{Fighter, FighterECB, FighterFacingDirection, FighterPreviousTranslation, FighterTranslation, FighterVelocity};
|
||||||
use crate::input::FighterInput;
|
use crate::input::FighterInput;
|
||||||
|
use crate::match_loading::MatchPlayer;
|
||||||
|
|
||||||
pub fn debug_draw_ecb(
|
pub fn debug_draw_ecb(
|
||||||
ecbs: Query<(
|
ecbs: Query<(
|
||||||
@@ -30,22 +35,42 @@ pub fn debug_draw_ecb(
|
|||||||
gizmos.lineloop(prev_transformed, ORANGE);
|
gizmos.lineloop(prev_transformed, ORANGE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fighter_debug(
|
pub fn fighter_debug(
|
||||||
mut contexts: EguiContexts,
|
mut contexts: EguiContexts,
|
||||||
query: Query<(&FighterVelocity, &FighterInput, &StateNameDebug)>,
|
query: Query<(&FighterVelocity, &FighterInput, &StateNameDebug)>,
|
||||||
) -> Result {
|
) -> Result {
|
||||||
for (vel, input, name) in query {
|
for (i, (vel, input, name)) in query.iter().enumerate() {
|
||||||
egui::Window::new("Hello").show(contexts.ctx_mut()?, |ui| {
|
egui::Window::new(format!("Player {}", i)).show(contexts.ctx_mut()?, |ui| {
|
||||||
ui.label(format!("State: {}", name.0));
|
ui.label(format!("State: {}", name.0));
|
||||||
ui.label(format!("{:?}", vel));
|
ui.label(format!("{:?}", vel));
|
||||||
ui.label(format!("{:?}", input.get_last_frame()));
|
ui.label(format!("{:?}", input.get_last_frame()));
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn animation_debug(
|
||||||
|
query: Query<(
|
||||||
|
&GlobalTransform,
|
||||||
|
&FighterAnimationFrame,
|
||||||
|
&FighterAnimations,
|
||||||
|
)>,
|
||||||
|
baked_anims: Res<Assets<BakedFighterAnimations>>,
|
||||||
|
mut gizmos: Gizmos,
|
||||||
|
) {
|
||||||
|
for (global_transform, frame, anims) in query {
|
||||||
|
let baked: &BakedFighterAnimations = baked_anims.get(&anims.baked).expect("Baked anims should be in");
|
||||||
|
for bone in &baked.bone_names {
|
||||||
|
let out = anims.sample_bone(&baked_anims, *frame, bone);
|
||||||
|
if let Some(out) = out {
|
||||||
|
let pos = global_transform.transform_point(Vec3::new(out.cols[3][0].to_num(), out.cols[3][1].to_num(), out.cols[3][2].to_num()));
|
||||||
|
gizmos.cross(pos, 0.5, bevy::color::palettes::css::RED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn update_config(
|
pub fn update_config(
|
||||||
mut config_store: ResMut<GizmoConfigStore>,
|
mut config_store: ResMut<GizmoConfigStore>,
|
||||||
keyboard: Res<ButtonInput<KeyCode>>,
|
keyboard: Res<ButtonInput<KeyCode>>,
|
||||||
|
|||||||
+10
-10
@@ -36,29 +36,29 @@ pub fn snapshot_fighter_ecb(fighters: Query<(&FighterECB, &mut FighterPreviousEC
|
|||||||
|
|
||||||
impl FighterECB {
|
impl FighterECB {
|
||||||
pub fn to_3d_lineloop(&self, trf: Transform) -> [Vec3; 4] {
|
pub fn to_3d_lineloop(&self, trf: Transform) -> [Vec3; 4] {
|
||||||
let rel_up = trf.transform_point(Vec3::new(0.0, self.vertical_half.to_num(), 0.0));
|
let rel_up = trf.transform_point(Vec3::new(0.0, (self.vertical_half + self.vertical_half).to_num(), 0.0));
|
||||||
let rel_down = trf.transform_point(Vec3::new(0.0, (-self.vertical_half).to_num(), 0.0));
|
let rel_down = trf.transform_point(Vec3::new(0.0, 0.0, 0.0));
|
||||||
let rel_left = trf.transform_point(Vec3::new((-self.horizontal_half).to_num(), 0.0, 0.0));
|
let rel_left = trf.transform_point(Vec3::new((-self.horizontal_half).to_num(), self.vertical_half.to_num(), 0.0));
|
||||||
let rel_right = trf.transform_point(Vec3::new(self.horizontal_half.to_num(), 0.0, 0.0));
|
let rel_right = trf.transform_point(Vec3::new(self.horizontal_half.to_num(), self.vertical_half.to_num(), 0.0));
|
||||||
[rel_up, rel_right, rel_down, rel_left]
|
[rel_up, rel_right, rel_down, rel_left]
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_bottom_point(&self) -> FGVec2 {
|
pub fn get_bottom_point(&self) -> FGVec2 {
|
||||||
FGVec2::new(FGi32::ZERO, -self.vertical_half)
|
FGVec2::new(FGi32::ZERO, FGi32::ZERO)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FighterPreviousECB {
|
impl FighterPreviousECB {
|
||||||
pub fn to_3d_lineloop(&self, trf: Transform) -> [Vec3; 4] {
|
pub fn to_3d_lineloop(&self, trf: Transform) -> [Vec3; 4] {
|
||||||
let rel_up = trf.transform_point(Vec3::new(0.0, self.vertical_half.to_num(), 0.0));
|
let rel_up = trf.transform_point(Vec3::new(0.0, (self.vertical_half + self.vertical_half).to_num(), 0.0));
|
||||||
let rel_down = trf.transform_point(Vec3::new(0.0, (-self.vertical_half).to_num(), 0.0));
|
let rel_down = trf.transform_point(Vec3::new(0.0, 0.0, 0.0));
|
||||||
let rel_left = trf.transform_point(Vec3::new((-self.horizontal_half).to_num(), 0.0, 0.0));
|
let rel_left = trf.transform_point(Vec3::new((-self.horizontal_half).to_num(), self.vertical_half.to_num(), 0.0));
|
||||||
let rel_right = trf.transform_point(Vec3::new(self.horizontal_half.to_num(), 0.0, 0.0));
|
let rel_right = trf.transform_point(Vec3::new(self.horizontal_half.to_num(), self.vertical_half.to_num(), 0.0));
|
||||||
[rel_up, rel_right, rel_down, rel_left]
|
[rel_up, rel_right, rel_down, rel_left]
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_bottom_point(&self) -> FGVec2 {
|
pub fn get_bottom_point(&self) -> FGVec2 {
|
||||||
FGVec2::new(FGi32::ZERO, -self.vertical_half)
|
FGVec2::new(FGi32::ZERO, FGi32::ZERO)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ pub enum FighterId {
|
|||||||
pub struct FighterManifest {
|
pub struct FighterManifest {
|
||||||
pub id: FighterId,
|
pub id: FighterId,
|
||||||
pub model_path: String,
|
pub model_path: String,
|
||||||
|
pub baked_animation_path: String,
|
||||||
pub attributes: FighterAttributes,
|
pub attributes: FighterAttributes,
|
||||||
pub camera: FighterCameraProfile,
|
pub camera: FighterCameraProfile,
|
||||||
pub animations: HashMap<AnimKind, String>,
|
pub animations: HashMap<AnimKind, String>,
|
||||||
@@ -87,6 +88,13 @@ pub fn prepare_fighter_manifests(
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if manifest.baked_animation_path.is_empty() {
|
||||||
|
fail(
|
||||||
|
&mut next_state,
|
||||||
|
&format!("{:?} has an empty baked animation path", manifest.id),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if !manifest.animations.contains_key(&AnimKind::Wait) {
|
if !manifest.animations.contains_key(&AnimKind::Wait) {
|
||||||
fail(
|
fail(
|
||||||
&mut next_state,
|
&mut next_state,
|
||||||
|
|||||||
+32
-14
@@ -7,6 +7,7 @@ use bevy_ggrs::prelude::*;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
pub mod animation;
|
pub mod animation;
|
||||||
|
pub mod baked_animation;
|
||||||
pub mod attributes;
|
pub mod attributes;
|
||||||
pub mod collision;
|
pub mod collision;
|
||||||
pub mod debug;
|
pub mod debug;
|
||||||
@@ -27,15 +28,8 @@ pub use motion::{FighterPreviousTranslation, FighterTranslation, FighterVelocity
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
fighter::{
|
fighter::{
|
||||||
animation::animation_init,
|
animation::animation_init, manifest::FighterManifest, state::{FighterState, fall::FallState}, visual::FighterAnimations,
|
||||||
state::{FighterState, fall::FallState},
|
}, input::FighterInput, math::{int::FGi32, vec::FGVec2}, player::Player, schedule::GameplaySet, stage::line::StageCollision,
|
||||||
visual::FighterAnimations,
|
|
||||||
},
|
|
||||||
input::FighterInput,
|
|
||||||
math::{int::FGi32, vec::FGVec2},
|
|
||||||
player::Player,
|
|
||||||
schedule::GameplaySet,
|
|
||||||
stage::line::StageCollision,
|
|
||||||
};
|
};
|
||||||
use state::state_interrupt_system;
|
use state::state_interrupt_system;
|
||||||
|
|
||||||
@@ -49,7 +43,17 @@ pub enum FighterFacingDirection {
|
|||||||
Left,
|
Left,
|
||||||
Right,
|
Right,
|
||||||
}
|
}
|
||||||
#[derive(Component, Copy, Clone, Debug, Reflect, Serialize, Deserialize)]
|
|
||||||
|
impl FighterFacingDirection {
|
||||||
|
fn reverse(&self) -> Self {
|
||||||
|
match self {
|
||||||
|
FighterFacingDirection::Left => FighterFacingDirection::Right,
|
||||||
|
FighterFacingDirection::Right => FighterFacingDirection::Left,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Reflect, Serialize, Deserialize)]
|
||||||
#[reflect(opaque)]
|
#[reflect(opaque)]
|
||||||
pub struct FighterCameraProfile {
|
pub struct FighterCameraProfile {
|
||||||
pub vertical_origin_offset: FGi32,
|
pub vertical_origin_offset: FGi32,
|
||||||
@@ -84,6 +88,12 @@ impl FighterFacingDirection {
|
|||||||
#[require(Transform)]
|
#[require(Transform)]
|
||||||
pub struct FighterVisual;
|
pub struct FighterVisual;
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct Fighter {
|
||||||
|
id: FighterId,
|
||||||
|
pub manifest: Handle<FighterManifest>
|
||||||
|
}
|
||||||
|
|
||||||
impl Plugin for FighterPlugin {
|
impl Plugin for FighterPlugin {
|
||||||
fn build(&self, app: &mut App) {
|
fn build(&self, app: &mut App) {
|
||||||
app.add_systems(
|
app.add_systems(
|
||||||
@@ -95,6 +105,7 @@ impl Plugin for FighterPlugin {
|
|||||||
GgrsSchedule,
|
GgrsSchedule,
|
||||||
(
|
(
|
||||||
ecb::snapshot_fighter_ecb.before(state_interrupt_system),
|
ecb::snapshot_fighter_ecb.before(state_interrupt_system),
|
||||||
|
animation::advance_fighter_animation_frames.before(state_interrupt_system),
|
||||||
state::state_interrupt_system,
|
state::state_interrupt_system,
|
||||||
)
|
)
|
||||||
.in_set(GameplaySet::Interrupt),
|
.in_set(GameplaySet::Interrupt),
|
||||||
@@ -130,15 +141,19 @@ impl Plugin for FighterPlugin {
|
|||||||
.rollback_component_with_copy::<FighterVelocity>()
|
.rollback_component_with_copy::<FighterVelocity>()
|
||||||
.rollback_component_with_copy::<FighterTranslation>()
|
.rollback_component_with_copy::<FighterTranslation>()
|
||||||
.rollback_component_with_copy::<FighterPreviousTranslation>()
|
.rollback_component_with_copy::<FighterPreviousTranslation>()
|
||||||
.rollback_component_with_copy::<FighterAttributes>()
|
.rollback_component_with_copy::<animation::FighterAnimationFrame>()
|
||||||
|
.checksum_component_with_hash::<animation::FighterAnimationFrame>()
|
||||||
.rollback_component_with_copy::<Grounded>()
|
.rollback_component_with_copy::<Grounded>()
|
||||||
|
.rollback_component_with_copy::<FighterFacingDirection>()
|
||||||
.rollback_resource_with_reflect::<StageCollision>()
|
.rollback_resource_with_reflect::<StageCollision>()
|
||||||
// Debug views.
|
// Debug views.
|
||||||
.add_systems(FixedPostUpdate, debug::debug_draw_ecb)
|
.add_systems(FixedPostUpdate, debug::debug_draw_ecb)
|
||||||
.add_systems(EguiPrimaryContextPass, debug::fighter_debug)
|
.add_systems(EguiPrimaryContextPass, debug::fighter_debug)
|
||||||
|
.add_systems(FixedPostUpdate, debug::animation_debug)
|
||||||
.add_systems(EguiPrimaryContextPass, debug::update_config)
|
.add_systems(EguiPrimaryContextPass, debug::update_config)
|
||||||
.rollback_component_with_clone::<FighterState>()
|
.rollback_component_with_clone::<FighterState>()
|
||||||
.checksum_component_with_hash::<FighterState>()
|
.checksum_component_with_hash::<FighterState>()
|
||||||
|
.checksum_component_with_hash::<FighterFacingDirection>()
|
||||||
.register_type::<FighterFacingDirection>();
|
.register_type::<FighterFacingDirection>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -147,8 +162,8 @@ pub fn spawn_fighter(
|
|||||||
commands: &mut Commands,
|
commands: &mut Commands,
|
||||||
player_handle: usize,
|
player_handle: usize,
|
||||||
spawn_position: FGVec2,
|
spawn_position: FGVec2,
|
||||||
|
manifest: (Handle<FighterManifest>, &FighterManifest),
|
||||||
attributes: FighterAttributes,
|
attributes: FighterAttributes,
|
||||||
camera_profile: FighterCameraProfile,
|
|
||||||
animations: FighterAnimations,
|
animations: FighterAnimations,
|
||||||
visual_root: WorldAssetRoot,
|
visual_root: WorldAssetRoot,
|
||||||
) {
|
) {
|
||||||
@@ -157,6 +172,10 @@ pub fn spawn_fighter(
|
|||||||
Player {
|
Player {
|
||||||
handle: player_handle,
|
handle: player_handle,
|
||||||
},
|
},
|
||||||
|
Fighter {
|
||||||
|
id: manifest.1.id,
|
||||||
|
manifest: manifest.0
|
||||||
|
},
|
||||||
FighterFacingDirection::Right,
|
FighterFacingDirection::Right,
|
||||||
FighterECB {
|
FighterECB {
|
||||||
vertical_half: FGi32::lit("5.0"),
|
vertical_half: FGi32::lit("5.0"),
|
||||||
@@ -164,11 +183,10 @@ pub fn spawn_fighter(
|
|||||||
},
|
},
|
||||||
FighterVelocity::default(),
|
FighterVelocity::default(),
|
||||||
FighterTranslation(spawn_position),
|
FighterTranslation(spawn_position),
|
||||||
attributes,
|
|
||||||
camera_profile,
|
|
||||||
visual_root,
|
visual_root,
|
||||||
animations,
|
animations,
|
||||||
FighterState::Fall(FallState),
|
FighterState::Fall(FallState),
|
||||||
|
animation::FighterAnimationFrame::new(animation::AnimKind::Wait, true),
|
||||||
FighterVisual,
|
FighterVisual,
|
||||||
FighterInput::default(),
|
FighterInput::default(),
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -7,7 +7,11 @@ pub fn integrate_gravity(velocity: &mut FighterVelocity, attributes: &FighterAtt
|
|||||||
velocity.y = velocity.y.max(-attributes.terminal_velocity);
|
velocity.y = velocity.y.max(-attributes.terminal_velocity);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn apply_air_drift(velocity: &mut FighterVelocity, attribs: &FighterAttributes, input: &FighterInput) {
|
pub fn apply_air_drift(
|
||||||
|
velocity: &mut FighterVelocity,
|
||||||
|
attribs: &FighterAttributes,
|
||||||
|
input: &FighterInput,
|
||||||
|
) {
|
||||||
if input.get_last_frame().movement.x.is_zero() {
|
if input.get_last_frame().movement.x.is_zero() {
|
||||||
if velocity.x.abs() < attribs.air_friction {
|
if velocity.x.abs() < attribs.air_friction {
|
||||||
velocity.x = FGi32::ZERO;
|
velocity.x = FGi32::ZERO;
|
||||||
@@ -16,9 +20,12 @@ pub fn apply_air_drift(velocity: &mut FighterVelocity, attribs: &FighterAttribut
|
|||||||
velocity.x += (-velocity_sign) * attribs.air_friction;
|
velocity.x += (-velocity_sign) * attribs.air_friction;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let target_velocity = input.get_last_frame().movement.x.signum() * attribs.max_air_horizontal_velocity;
|
let target_velocity =
|
||||||
let acceleration = input.get_last_frame().movement.x.signum() * attribs.air_acceleration_base;
|
input.get_last_frame().movement.x.signum() * attribs.max_air_horizontal_velocity;
|
||||||
let mut acceleration = acceleration + input.get_last_frame().movement.x * attribs.air_acceleration_stick;
|
let acceleration =
|
||||||
|
input.get_last_frame().movement.x.signum() * attribs.air_acceleration_base;
|
||||||
|
let mut acceleration =
|
||||||
|
acceleration + input.get_last_frame().movement.x * attribs.air_acceleration_stick;
|
||||||
let velocity_x = velocity.x;
|
let velocity_x = velocity.x;
|
||||||
if !(velocity_x * acceleration < FGi32::ZERO) {
|
if !(velocity_x * acceleration < FGi32::ZERO) {
|
||||||
if acceleration > FGi32::ZERO {
|
if acceleration > FGi32::ZERO {
|
||||||
@@ -46,13 +53,11 @@ pub fn apply_air_drift(velocity: &mut FighterVelocity, attribs: &FighterAttribut
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub fn apply_air_motion(
|
pub fn apply_air_motion(
|
||||||
velocity: &FighterVelocity,
|
velocity: &FighterVelocity,
|
||||||
translation: &mut FighterTranslation,
|
translation: &mut FighterTranslation,
|
||||||
prev_translation: &mut FighterPreviousTranslation,
|
prev_translation: &mut FighterPreviousTranslation,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
// Vertical velocity should be 0 on the ground
|
// Vertical velocity should be 0 on the ground
|
||||||
prev_translation.0 = translation.0;
|
prev_translation.0 = translation.0;
|
||||||
translation.0 += velocity.0;
|
translation.0 += velocity.0;
|
||||||
|
|||||||
@@ -14,7 +14,9 @@ use crate::{
|
|||||||
wait::WaitState,
|
wait::WaitState,
|
||||||
walk,
|
walk,
|
||||||
},
|
},
|
||||||
}, input, math::vec::FGVec2,
|
},
|
||||||
|
input,
|
||||||
|
math::vec::FGVec2,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Hash)]
|
#[derive(Debug, Clone, Hash)]
|
||||||
@@ -26,8 +28,8 @@ pub struct AirDodgeState {
|
|||||||
impl FighterStateImpl for AirDodgeState {
|
impl FighterStateImpl for AirDodgeState {
|
||||||
const NAME: &'static str = "AirDodge";
|
const NAME: &'static str = "AirDodge";
|
||||||
|
|
||||||
fn check_interrupt(&self, state_context: &FighterStateContext) -> Option<FighterState> {
|
fn check_interrupt(&self, state_context: &mut FighterStateContext) -> Option<FighterState> {
|
||||||
if self.duration_counter >= state_context.fighter_attribs.air_dodge_duration {
|
if self.duration_counter >= state_context.fighter_manifest.attributes.air_dodge_duration {
|
||||||
Some(FighterState::Fall(FallState))
|
Some(FighterState::Fall(FallState))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@@ -37,7 +39,7 @@ impl FighterStateImpl for AirDodgeState {
|
|||||||
fn update(&mut self, state_context: &mut FighterStateContext) {
|
fn update(&mut self, state_context: &mut FighterStateContext) {
|
||||||
self.duration_counter += 1;
|
self.duration_counter += 1;
|
||||||
|
|
||||||
state_context.velocity.0 *= state_context.fighter_attribs.air_dodge_decay;
|
state_context.velocity.0 *= state_context.fighter_manifest.attributes.air_dodge_decay;
|
||||||
|
|
||||||
air::apply_air_motion(
|
air::apply_air_motion(
|
||||||
state_context.velocity,
|
state_context.velocity,
|
||||||
@@ -66,13 +68,10 @@ impl FighterStateImpl for AirDodgeState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn on_enter(&mut self, state_context: &mut FighterStateContext) {
|
fn on_enter(&mut self, state_context: &mut FighterStateContext) {
|
||||||
state_context.animation_transitions.play(
|
state_context.play_animation(AnimKind::AirDodge, false);
|
||||||
state_context.animation_player,
|
|
||||||
state_context.animations.clips[&AnimKind::AirDodge],
|
|
||||||
Duration::ZERO,
|
|
||||||
);
|
|
||||||
|
|
||||||
state_context.velocity.0 = self.direction * state_context.fighter_attribs.air_dodge_velocity;
|
state_context.velocity.0 =
|
||||||
|
self.direction * state_context.fighter_manifest.attributes.air_dodge_velocity;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+29
-21
@@ -4,11 +4,11 @@ use std::time::Duration;
|
|||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
|
||||||
use super::{FighterStateContext, FighterStateImpl, ground, run, wait, walk};
|
use super::{FighterStateContext, FighterStateImpl, ground, run, wait, walk};
|
||||||
|
use crate::fighter::FighterFacingDirection;
|
||||||
use crate::fighter::animation::AnimKind;
|
use crate::fighter::animation::AnimKind;
|
||||||
use crate::fighter::state::FighterState;
|
use crate::fighter::state::FighterState;
|
||||||
use crate::fighter::state::fall::FallState;
|
use crate::fighter::state::fall::FallState;
|
||||||
use crate::fighter::state::ground::{GroundedMotionResult, GroundedStateCommon};
|
use crate::fighter::state::ground::{GroundedMotionResult, GroundedStateCommon};
|
||||||
use crate::fighter::FighterFacingDirection;
|
|
||||||
use crate::input::FighterCommands::{SmashMoveLeft, SmashMoveRight};
|
use crate::input::FighterCommands::{SmashMoveLeft, SmashMoveRight};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -37,9 +37,9 @@ impl DashState {
|
|||||||
|
|
||||||
impl FighterStateImpl for DashState {
|
impl FighterStateImpl for DashState {
|
||||||
const NAME: &'static str = "Dash";
|
const NAME: &'static str = "Dash";
|
||||||
fn check_interrupt(&self, ctx: &FighterStateContext) -> Option<FighterState> {
|
fn check_interrupt(&self, ctx: &mut FighterStateContext) -> Option<FighterState> {
|
||||||
let ground = &self.ground_common;
|
let ground = &self.ground_common;
|
||||||
let attributes = ctx.fighter_attribs;
|
let attributes = ctx.fighter_manifest.attributes;
|
||||||
|
|
||||||
// A reverse dash can interrupt before IASA
|
// A reverse dash can interrupt before IASA
|
||||||
if let Some(direction) =
|
if let Some(direction) =
|
||||||
@@ -70,42 +70,48 @@ impl FighterStateImpl for DashState {
|
|||||||
|
|
||||||
fn on_enter(&mut self, state_context: &mut FighterStateContext) {
|
fn on_enter(&mut self, state_context: &mut FighterStateContext) {
|
||||||
*state_context.facing_direction = self.direction;
|
*state_context.facing_direction = self.direction;
|
||||||
state_context.velocity.x =
|
state_context.velocity.x = state_context
|
||||||
state_context.fighter_attribs.dash_initial_velocity * self.direction.to_sign();
|
.fighter_manifest
|
||||||
|
.attributes
|
||||||
|
.dash_initial_velocity
|
||||||
|
* self.direction.to_sign();
|
||||||
|
|
||||||
state_context.animation_transitions.play(
|
state_context.play_animation(AnimKind::Dash, false);
|
||||||
&mut state_context.animation_player,
|
|
||||||
state_context.animations.clips[&AnimKind::Dash],
|
|
||||||
Duration::ZERO,
|
|
||||||
);
|
|
||||||
|
|
||||||
match state_context.facing_direction {
|
match state_context.facing_direction {
|
||||||
FighterFacingDirection::Left => {
|
FighterFacingDirection::Left => {
|
||||||
state_context.input.clear_command(crate::input::FighterCommands::SmashMoveLeft);
|
state_context
|
||||||
},
|
.input
|
||||||
|
.clear_command(crate::input::FighterCommands::SmashMoveLeft);
|
||||||
|
}
|
||||||
FighterFacingDirection::Right => {
|
FighterFacingDirection::Right => {
|
||||||
state_context.input.clear_command(crate::input::FighterCommands::SmashMoveRight);
|
state_context
|
||||||
},
|
.input
|
||||||
|
.clear_command(crate::input::FighterCommands::SmashMoveRight);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(&mut self, state_context: &mut FighterStateContext) {
|
fn update(&mut self, state_context: &mut FighterStateContext) {
|
||||||
self.frames_in_dash += 1;
|
self.frames_in_dash += 1;
|
||||||
|
|
||||||
if self.frames_in_dash < state_context.fighter_attribs.dash_acceleration_duration {
|
if self.frames_in_dash
|
||||||
|
< state_context
|
||||||
}
|
.fighter_manifest
|
||||||
|
.attributes
|
||||||
|
.dash_acceleration_duration
|
||||||
|
{}
|
||||||
|
|
||||||
let last_frame = state_context.input.get_last_frame();
|
let last_frame = state_context.input.get_last_frame();
|
||||||
let (accel, target_vel) = state_context
|
let (accel, target_vel) = state_context
|
||||||
.fighter_attribs
|
.fighter_manifest
|
||||||
|
.attributes
|
||||||
.get_accel_and_target_dashrun(&last_frame, self.direction.to_sign());
|
.get_accel_and_target_dashrun(&last_frame, self.direction.to_sign());
|
||||||
let accel = ground::compute_ground_accel(
|
let accel = ground::compute_ground_accel(
|
||||||
accel,
|
accel,
|
||||||
target_vel,
|
target_vel,
|
||||||
state_context.velocity.x,
|
state_context.velocity.x,
|
||||||
state_context.fighter_attribs,
|
&state_context.fighter_manifest.attributes,
|
||||||
state_context.game_settings,
|
state_context.game_settings,
|
||||||
);
|
);
|
||||||
state_context.velocity.x += accel;
|
state_context.velocity.x += accel;
|
||||||
@@ -131,7 +137,9 @@ impl FighterStateImpl for DashState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_smash_input_with_dir(state_context: &FighterStateContext) -> Option<FighterFacingDirection> {
|
fn check_smash_input_with_dir(
|
||||||
|
state_context: &FighterStateContext,
|
||||||
|
) -> Option<FighterFacingDirection> {
|
||||||
if state_context.input.has_command(SmashMoveLeft) {
|
if state_context.input.has_command(SmashMoveLeft) {
|
||||||
return Some(FighterFacingDirection::Left);
|
return Some(FighterFacingDirection::Left);
|
||||||
} else if state_context.input.has_command(SmashMoveRight) {
|
} else if state_context.input.has_command(SmashMoveRight) {
|
||||||
|
|||||||
@@ -10,23 +10,22 @@ pub struct FallState;
|
|||||||
|
|
||||||
impl FighterStateImpl for FallState {
|
impl FighterStateImpl for FallState {
|
||||||
const NAME: &'static str = "Fall";
|
const NAME: &'static str = "Fall";
|
||||||
fn check_interrupt(&self, state_context: &FighterStateContext) -> Option<FighterState> {
|
fn check_interrupt(&self, state_context: &mut FighterStateContext) -> Option<FighterState> {
|
||||||
jump::check_input_double_jump(state_context)
|
jump::check_input_double_jump(state_context)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_enter(&mut self, state_context: &mut super::FighterStateContext) {
|
fn on_enter(&mut self, state_context: &mut super::FighterStateContext) {
|
||||||
state_context.animation_transitions.play(
|
state_context.play_animation(AnimKind::Fall, true);
|
||||||
&mut state_context.animation_player,
|
|
||||||
state_context.animations.clips[&AnimKind::Fall],
|
|
||||||
std::time::Duration::ZERO,
|
|
||||||
).repeat();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(&mut self, state_context: &mut super::FighterStateContext) {
|
fn update(&mut self, state_context: &mut super::FighterStateContext) {
|
||||||
air::integrate_gravity(state_context.velocity, state_context.fighter_attribs);
|
air::integrate_gravity(
|
||||||
|
state_context.velocity,
|
||||||
|
&state_context.fighter_manifest.attributes,
|
||||||
|
);
|
||||||
air::apply_air_drift(
|
air::apply_air_drift(
|
||||||
state_context.velocity,
|
state_context.velocity,
|
||||||
state_context.fighter_attribs,
|
&state_context.fighter_manifest.attributes,
|
||||||
state_context.input,
|
state_context.input,
|
||||||
);
|
);
|
||||||
air::apply_air_motion(
|
air::apply_air_motion(
|
||||||
|
|||||||
@@ -2,16 +2,14 @@
|
|||||||
//! ECS types so it stays cheap to reason about and to unit-test — it has to be
|
//! ECS types so it stays cheap to reason about and to unit-test — it has to be
|
||||||
//! bit-for-bit deterministic for rollback.
|
//! bit-for-bit deterministic for rollback.
|
||||||
|
|
||||||
use crate::fighter::state::{FighterState, FighterStateContext};
|
use crate::fighter::state::{FighterState, FighterStateContext, dash, turn, wait, walk};
|
||||||
use crate::fighter::{
|
use crate::fighter::{
|
||||||
FighterAttributes, FighterPreviousTranslation, FighterTranslation, FighterVelocity,
|
FighterAttributes, FighterPreviousTranslation, FighterTranslation, FighterVelocity,
|
||||||
};
|
};
|
||||||
use crate::game_settings::GameSettings;
|
use crate::game_settings::GameSettings;
|
||||||
use crate::math::int::FGi32;
|
use crate::math::int::FGi32;
|
||||||
use crate::math::vec::FGVec2;
|
use crate::math::vec::FGVec2;
|
||||||
use crate::stage::line::{
|
use crate::stage::line::{StageLineID, StagePolyLineSegmentType};
|
||||||
StageLineID, StagePolyLineSegmentType,
|
|
||||||
};
|
|
||||||
|
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use fixed::traits::Fixed;
|
use fixed::traits::Fixed;
|
||||||
@@ -149,3 +147,20 @@ pub fn grounded_movement_common_interrupts(
|
|||||||
) -> Option<FighterState> {
|
) -> Option<FighterState> {
|
||||||
super::jump::check_input(state_context, ground_common)
|
super::jump::check_input(state_context, ground_common)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn grounded_movement_standstill_common_interrupts(
|
||||||
|
state_context: &FighterStateContext,
|
||||||
|
ground_common: &GroundedStateCommon,
|
||||||
|
) -> Option<FighterState> {
|
||||||
|
if let Some(state) = dash::check_input(state_context, &ground_common) {
|
||||||
|
Some(state)
|
||||||
|
} else if let Some(state) = turn::check_input(state_context, &ground_common) {
|
||||||
|
Some(state)
|
||||||
|
} else if let Some(state) = walk::check_input(state_context, &ground_common) {
|
||||||
|
Some(state)
|
||||||
|
} else if let Some(state) = wait::check_input(state_context, &ground_common) {
|
||||||
|
Some(state)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+67
-51
@@ -47,10 +47,11 @@ impl JumpSquatState {
|
|||||||
impl FighterStateImpl for JumpSquatState {
|
impl FighterStateImpl for JumpSquatState {
|
||||||
const NAME: &'static str = "JumpSquat";
|
const NAME: &'static str = "JumpSquat";
|
||||||
|
|
||||||
fn check_interrupt(&self, state_context: &FighterStateContext) -> Option<FighterState> {
|
fn check_interrupt(&self, state_context: &mut FighterStateContext) -> Option<FighterState> {
|
||||||
// Special handling for wavedash from jump squat
|
// Special handling for wavedash from jump squat
|
||||||
if state_context.input.has_command(FighterCommands::Shield) {
|
if state_context.input.has_command(FighterCommands::Shield) {
|
||||||
// If aiming sideways and in the deadzone, do a perfect wavedash
|
// If aiming sideways and in the deadzone, do a perfect wavedash
|
||||||
|
// TODO: Improve this so we don't have to go through the airborne state first
|
||||||
let mut wavedash_dir = state_context
|
let mut wavedash_dir = state_context
|
||||||
.input
|
.input
|
||||||
.get_last_frame()
|
.get_last_frame()
|
||||||
@@ -71,7 +72,9 @@ impl FighterStateImpl for JumpSquatState {
|
|||||||
duration_counter: 0,
|
duration_counter: 0,
|
||||||
direction: wavedash_dir.normalize_or_zero(),
|
direction: wavedash_dir.normalize_or_zero(),
|
||||||
}))
|
}))
|
||||||
} else if self.duration_counter >= state_context.fighter_attribs.jumpsquat_duration {
|
} else if self.duration_counter
|
||||||
|
>= state_context.fighter_manifest.attributes.jumpsquat_duration
|
||||||
|
{
|
||||||
info!("Jump {}", state_context.input.get_last_frame().jump);
|
info!("Jump {}", state_context.input.get_last_frame().jump);
|
||||||
let jump_type = if state_context.input.get_last_frame().jump {
|
let jump_type = if state_context.input.get_last_frame().jump {
|
||||||
JumpType::LongJump
|
JumpType::LongJump
|
||||||
@@ -87,16 +90,17 @@ impl FighterStateImpl for JumpSquatState {
|
|||||||
fn update(&mut self, state_context: &mut FighterStateContext) {
|
fn update(&mut self, state_context: &mut FighterStateContext) {
|
||||||
self.duration_counter += 1;
|
self.duration_counter += 1;
|
||||||
|
|
||||||
let friction =
|
let friction = if state_context.velocity.x.abs()
|
||||||
if state_context.velocity.x.abs() > state_context.fighter_attribs.max_walk_vel {
|
> state_context.fighter_manifest.attributes.max_walk_vel
|
||||||
state_context.fighter_attribs.ground_friction
|
{
|
||||||
* state_context
|
state_context.fighter_manifest.attributes.ground_friction
|
||||||
.game_settings
|
* state_context
|
||||||
.fighter_common
|
.game_settings
|
||||||
.ground_friction_over_walk_speed_multiplier
|
.fighter_common
|
||||||
} else {
|
.ground_friction_over_walk_speed_multiplier
|
||||||
state_context.fighter_attribs.ground_friction
|
} else {
|
||||||
};
|
state_context.fighter_manifest.attributes.ground_friction
|
||||||
|
};
|
||||||
|
|
||||||
let ground_velocity = state_context.velocity.x;
|
let ground_velocity = state_context.velocity.x;
|
||||||
|
|
||||||
@@ -123,18 +127,14 @@ impl FighterStateImpl for JumpSquatState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn on_enter(&mut self, state_context: &mut FighterStateContext) {
|
fn on_enter(&mut self, state_context: &mut FighterStateContext) {
|
||||||
state_context.animation_transitions.play(
|
state_context.play_animation(AnimKind::JumpSquat, false);
|
||||||
state_context.animation_player,
|
|
||||||
state_context.animations.clips[&AnimKind::JumpSquat],
|
|
||||||
Duration::ZERO,
|
|
||||||
);
|
|
||||||
state_context.input.clear_command(FighterCommands::Jump);
|
state_context.input.clear_command(FighterCommands::Jump);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FighterStateImpl for JumpState {
|
impl FighterStateImpl for JumpState {
|
||||||
const NAME: &'static str = "Jump";
|
const NAME: &'static str = "Jump";
|
||||||
fn check_interrupt(&self, state_context: &FighterStateContext) -> Option<FighterState> {
|
fn check_interrupt(&self, state_context: &mut FighterStateContext) -> Option<FighterState> {
|
||||||
if state_context.input.has_command(FighterCommands::Jump) {
|
if state_context.input.has_command(FighterCommands::Jump) {
|
||||||
Some(FighterState::Jump(JumpState {
|
Some(FighterState::Jump(JumpState {
|
||||||
jump_type: JumpType::DoubleJump,
|
jump_type: JumpType::DoubleJump,
|
||||||
@@ -145,10 +145,13 @@ impl FighterStateImpl for JumpState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn update(&mut self, state_context: &mut FighterStateContext) {
|
fn update(&mut self, state_context: &mut FighterStateContext) {
|
||||||
air::integrate_gravity(state_context.velocity, state_context.fighter_attribs);
|
air::integrate_gravity(
|
||||||
|
state_context.velocity,
|
||||||
|
&state_context.fighter_manifest.attributes,
|
||||||
|
);
|
||||||
air::apply_air_drift(
|
air::apply_air_drift(
|
||||||
state_context.velocity,
|
state_context.velocity,
|
||||||
state_context.fighter_attribs,
|
&state_context.fighter_manifest.attributes,
|
||||||
state_context.input,
|
state_context.input,
|
||||||
);
|
);
|
||||||
air::apply_air_motion(
|
air::apply_air_motion(
|
||||||
@@ -168,51 +171,64 @@ impl FighterStateImpl for JumpState {
|
|||||||
if movement_stick_x.is_zero()
|
if movement_stick_x.is_zero()
|
||||||
|| movement_stick_x.signum() == state_context.facing_direction.to_sign()
|
|| movement_stick_x.signum() == state_context.facing_direction.to_sign()
|
||||||
{
|
{
|
||||||
state_context.animation_transitions.play(
|
state_context.play_animation(AnimKind::JumpForward, false);
|
||||||
state_context.animation_player,
|
|
||||||
state_context.animations.clips[&AnimKind::JumpForward],
|
|
||||||
Duration::ZERO,
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
state_context.animation_transitions.play(
|
state_context.play_animation(AnimKind::JumpBack, false);
|
||||||
state_context.animation_player,
|
|
||||||
state_context.animations.clips[&AnimKind::JumpBack],
|
|
||||||
Duration::ZERO,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
JumpType::DoubleJump => {
|
JumpType::DoubleJump => {
|
||||||
state_context.animation_transitions.play(
|
state_context.play_animation(AnimKind::DoubleJump, false);
|
||||||
state_context.animation_player,
|
|
||||||
state_context.animations.clips[&AnimKind::DoubleJump],
|
|
||||||
Duration::ZERO,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let vertical_vel = match self.jump_type {
|
let vertical_vel = match self.jump_type {
|
||||||
JumpType::ShortHop => state_context.fighter_attribs.short_hop_vertical_velocity,
|
JumpType::ShortHop => {
|
||||||
JumpType::LongJump => state_context.fighter_attribs.full_jump_vertical_velocity,
|
state_context
|
||||||
|
.fighter_manifest
|
||||||
|
.attributes
|
||||||
|
.short_hop_vertical_velocity
|
||||||
|
}
|
||||||
|
JumpType::LongJump => {
|
||||||
|
state_context
|
||||||
|
.fighter_manifest
|
||||||
|
.attributes
|
||||||
|
.full_jump_vertical_velocity
|
||||||
|
}
|
||||||
JumpType::DoubleJump => {
|
JumpType::DoubleJump => {
|
||||||
state_context.fighter_attribs.full_jump_vertical_velocity
|
state_context
|
||||||
* state_context.fighter_attribs.air_jump_multiplier
|
.fighter_manifest
|
||||||
|
.attributes
|
||||||
|
.full_jump_vertical_velocity
|
||||||
|
* state_context
|
||||||
|
.fighter_manifest
|
||||||
|
.attributes
|
||||||
|
.air_jump_multiplier
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
match self.jump_type {
|
match self.jump_type {
|
||||||
JumpType::ShortHop | JumpType::LongJump => {
|
JumpType::ShortHop | JumpType::LongJump => {
|
||||||
state_context.velocity.x +=
|
state_context.velocity.x += state_context
|
||||||
state_context.fighter_attribs.jump_horizontal_velocity * movement_stick_x;
|
.fighter_manifest
|
||||||
|
.attributes
|
||||||
|
.jump_horizontal_velocity
|
||||||
|
* movement_stick_x;
|
||||||
}
|
}
|
||||||
JumpType::DoubleJump => {
|
JumpType::DoubleJump => {
|
||||||
state_context.velocity.x =
|
state_context.velocity.x = state_context
|
||||||
state_context.fighter_attribs.air_jump_horizontal_velocity * movement_stick_x;
|
.fighter_manifest
|
||||||
|
.attributes
|
||||||
|
.air_jump_horizontal_velocity
|
||||||
|
* movement_stick_x;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
state_context.velocity.y = vertical_vel;
|
state_context.velocity.y = vertical_vel;
|
||||||
state_context.velocity.x +=
|
state_context.velocity.x += state_context
|
||||||
state_context.fighter_attribs.jump_horizontal_velocity * movement_stick_x;
|
.fighter_manifest
|
||||||
|
.attributes
|
||||||
|
.jump_horizontal_velocity
|
||||||
|
* movement_stick_x;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_collision_interrupt(
|
fn check_collision_interrupt(
|
||||||
@@ -245,13 +261,13 @@ pub fn check_input(
|
|||||||
.then(|| FighterState::JumpSquat(JumpSquatState::create(ground_common.clone())))
|
.then(|| FighterState::JumpSquat(JumpSquatState::create(ground_common.clone())))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn check_input_double_jump(
|
pub fn check_input_double_jump(state_context: &FighterStateContext) -> Option<FighterState> {
|
||||||
state_context: &FighterStateContext
|
|
||||||
) -> Option<FighterState> {
|
|
||||||
state_context
|
state_context
|
||||||
.input
|
.input
|
||||||
.has_command(FighterCommands::Jump)
|
.has_command(FighterCommands::Jump)
|
||||||
.then(|| FighterState::Jump(JumpState{
|
.then(|| {
|
||||||
jump_type: JumpType::DoubleJump
|
FighterState::Jump(JumpState {
|
||||||
}))
|
jump_type: JumpType::DoubleJump,
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
+27
-29
@@ -3,15 +3,18 @@ use std::time::Duration;
|
|||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
|
||||||
use crate::fighter::{
|
use crate::fighter::{
|
||||||
animation::AnimKind,
|
animation::AnimKind,
|
||||||
state::{
|
state::{
|
||||||
FighterState, FighterStateContext, FighterStateImpl, dash,
|
FighterState, FighterStateContext, FighterStateImpl, dash,
|
||||||
fall::FallState,
|
fall::FallState,
|
||||||
ground::{self, GroundedMotionResult, GroundedStateCommon},
|
ground::{
|
||||||
wait::WaitState,
|
self, GroundedMotionResult, GroundedStateCommon,
|
||||||
walk,
|
grounded_movement_standstill_common_interrupts,
|
||||||
},
|
},
|
||||||
};
|
wait::WaitState,
|
||||||
|
walk,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Hash)]
|
#[derive(Debug, Clone, Hash)]
|
||||||
pub struct LandingState {
|
pub struct LandingState {
|
||||||
@@ -31,12 +34,10 @@ impl LandingState {
|
|||||||
impl FighterStateImpl for LandingState {
|
impl FighterStateImpl for LandingState {
|
||||||
const NAME: &'static str = "Landing";
|
const NAME: &'static str = "Landing";
|
||||||
|
|
||||||
fn check_interrupt(&self, state_context: &FighterStateContext) -> Option<FighterState> {
|
fn check_interrupt(&self, state_context: &mut FighterStateContext) -> Option<FighterState> {
|
||||||
if self.duration_counter >= state_context.fighter_attribs.landing_duration {
|
if state_context.is_current_animation_finished() {
|
||||||
Some(FighterState::Wait(WaitState {
|
grounded_movement_standstill_common_interrupts(state_context, &self.grounded_common)
|
||||||
grounded_common: self.grounded_common.clone(),
|
} else if self.duration_counter >= state_context.fighter_manifest.attributes.landing_iasa {
|
||||||
}))
|
|
||||||
} else if self.duration_counter >= state_context.fighter_attribs.landing_iasa {
|
|
||||||
dash::check_input(state_context, &self.grounded_common).or_else(|| {
|
dash::check_input(state_context, &self.grounded_common).or_else(|| {
|
||||||
ground::grounded_movement_common_interrupts(state_context, &self.grounded_common)
|
ground::grounded_movement_common_interrupts(state_context, &self.grounded_common)
|
||||||
.or_else(|| walk::check_input(state_context, &self.grounded_common))
|
.or_else(|| walk::check_input(state_context, &self.grounded_common))
|
||||||
@@ -49,16 +50,17 @@ impl FighterStateImpl for LandingState {
|
|||||||
fn update(&mut self, state_context: &mut FighterStateContext) {
|
fn update(&mut self, state_context: &mut FighterStateContext) {
|
||||||
self.duration_counter += 1;
|
self.duration_counter += 1;
|
||||||
|
|
||||||
let friction =
|
let friction = if state_context.velocity.x.abs()
|
||||||
if state_context.velocity.x.abs() > state_context.fighter_attribs.max_walk_vel {
|
> state_context.fighter_manifest.attributes.max_walk_vel
|
||||||
state_context.fighter_attribs.ground_friction
|
{
|
||||||
* state_context
|
state_context.fighter_manifest.attributes.ground_friction
|
||||||
.game_settings
|
* state_context
|
||||||
.fighter_common
|
.game_settings
|
||||||
.ground_friction_over_walk_speed_multiplier
|
.fighter_common
|
||||||
} else {
|
.ground_friction_over_walk_speed_multiplier
|
||||||
state_context.fighter_attribs.ground_friction
|
} else {
|
||||||
};
|
state_context.fighter_manifest.attributes.ground_friction
|
||||||
|
};
|
||||||
|
|
||||||
let ground_velocity = state_context.velocity.x;
|
let ground_velocity = state_context.velocity.x;
|
||||||
|
|
||||||
@@ -85,10 +87,6 @@ impl FighterStateImpl for LandingState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn on_enter(&mut self, state_context: &mut FighterStateContext) {
|
fn on_enter(&mut self, state_context: &mut FighterStateContext) {
|
||||||
state_context.animation_transitions.play(
|
state_context.play_animation(AnimKind::Landing, false);
|
||||||
state_context.animation_player,
|
|
||||||
state_context.animations.clips[&AnimKind::Landing],
|
|
||||||
Duration::ZERO,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+79
-11
@@ -5,26 +5,30 @@
|
|||||||
use bevy::ecs::query::QueryData;
|
use bevy::ecs::query::QueryData;
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
|
||||||
use crate::fighter::animation::FighterAnimationPlayerLink;
|
use crate::fighter::animation::{AnimKind, FighterAnimationFrame, FighterAnimationPlayerLink};
|
||||||
|
use crate::fighter::baked_animation::{BakedFighterAnimations, FixedMat4};
|
||||||
use crate::fighter::ecb::FighterPreviousECB;
|
use crate::fighter::ecb::FighterPreviousECB;
|
||||||
|
use crate::fighter::manifest::FighterManifest;
|
||||||
use crate::fighter::visual::FighterAnimations;
|
use crate::fighter::visual::FighterAnimations;
|
||||||
use crate::fighter::{
|
use crate::fighter::{
|
||||||
FighterAttributes, FighterECB, FighterFacingDirection, FighterPreviousTranslation, FighterTranslation, FighterVelocity,
|
Fighter, FighterAttributes, FighterECB, FighterFacingDirection, FighterPreviousTranslation,
|
||||||
|
FighterTranslation, FighterVelocity,
|
||||||
};
|
};
|
||||||
use crate::game_settings::GameSettings;
|
use crate::game_settings::GameSettings;
|
||||||
use crate::input::FighterInput;
|
use crate::input::FighterInput;
|
||||||
use crate::stage::line::StageCollision;
|
use crate::stage::line::StageCollision;
|
||||||
|
|
||||||
pub mod air;
|
pub mod air;
|
||||||
|
pub mod air_dodge;
|
||||||
pub mod dash;
|
pub mod dash;
|
||||||
pub mod fall;
|
pub mod fall;
|
||||||
pub mod ground;
|
pub mod ground;
|
||||||
pub mod jump;
|
pub mod jump;
|
||||||
|
pub mod land;
|
||||||
pub mod run;
|
pub mod run;
|
||||||
|
pub mod turn;
|
||||||
pub mod wait;
|
pub mod wait;
|
||||||
pub mod walk;
|
pub mod walk;
|
||||||
pub mod land;
|
|
||||||
pub mod air_dodge;
|
|
||||||
|
|
||||||
macro_rules! fighter_states {
|
macro_rules! fighter_states {
|
||||||
($($variant:ident => $ty:ty),* $(,)?) => {
|
($($variant:ident => $ty:ty),* $(,)?) => {
|
||||||
@@ -35,7 +39,7 @@ macro_rules! fighter_states {
|
|||||||
pub fn name(&self) -> &'static str {
|
pub fn name(&self) -> &'static str {
|
||||||
match self { $(Self::$variant(_) => <$ty>::NAME),* }
|
match self { $(Self::$variant(_) => <$ty>::NAME),* }
|
||||||
}
|
}
|
||||||
pub fn check_interrupt(&self, ctx: &FighterStateContext) -> Option<FighterState> {
|
pub fn check_interrupt(&self, ctx: &mut FighterStateContext) -> Option<FighterState> {
|
||||||
match self { $(Self::$variant(s) => s.check_interrupt(ctx)),* }
|
match self { $(Self::$variant(s) => s.check_interrupt(ctx)),* }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,7 +63,7 @@ macro_rules! fighter_states {
|
|||||||
|
|
||||||
pub trait FighterStateImpl: Sized {
|
pub trait FighterStateImpl: Sized {
|
||||||
const NAME: &'static str;
|
const NAME: &'static str;
|
||||||
fn check_interrupt(&self, state_context: &FighterStateContext) -> Option<FighterState>;
|
fn check_interrupt(&self, state_context: &mut FighterStateContext) -> Option<FighterState>;
|
||||||
|
|
||||||
fn on_enter(&mut self, _state_context: &mut FighterStateContext) {}
|
fn on_enter(&mut self, _state_context: &mut FighterStateContext) {}
|
||||||
fn update(&mut self, _state_context: &mut FighterStateContext);
|
fn update(&mut self, _state_context: &mut FighterStateContext);
|
||||||
@@ -79,13 +83,49 @@ pub struct FighterStateContext<'a> {
|
|||||||
pub velocity: &'a mut FighterVelocity,
|
pub velocity: &'a mut FighterVelocity,
|
||||||
pub ecb: &'a mut FighterECB,
|
pub ecb: &'a mut FighterECB,
|
||||||
pub prev_ecb: &'a mut FighterPreviousECB,
|
pub prev_ecb: &'a mut FighterPreviousECB,
|
||||||
pub fighter_attribs: &'a FighterAttributes,
|
|
||||||
pub facing_direction: &'a mut FighterFacingDirection,
|
pub facing_direction: &'a mut FighterFacingDirection,
|
||||||
pub game_settings: &'a GameSettings,
|
pub game_settings: &'a GameSettings,
|
||||||
pub stage_collision: &'a StageCollision,
|
pub stage_collision: &'a StageCollision,
|
||||||
pub animations: &'a FighterAnimations,
|
pub animations: &'a FighterAnimations,
|
||||||
pub animation_player: &'a mut AnimationPlayer,
|
pub animation_player: &'a mut AnimationPlayer,
|
||||||
pub animation_transitions: &'a mut AnimationTransitions,
|
pub animation_transitions: &'a mut AnimationTransitions,
|
||||||
|
pub animation_frame: &'a mut FighterAnimationFrame,
|
||||||
|
pub baked_animations: &'a Assets<BakedFighterAnimations>,
|
||||||
|
pub fighter_manifest: &'a FighterManifest,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FighterStateContext<'_> {
|
||||||
|
pub fn play_animation(&mut self, kind: AnimKind, repeat: bool) {
|
||||||
|
let active = self.animation_transitions.play(
|
||||||
|
self.animation_player,
|
||||||
|
self.animations.clips[&kind],
|
||||||
|
std::time::Duration::ZERO,
|
||||||
|
);
|
||||||
|
if repeat {
|
||||||
|
active.repeat();
|
||||||
|
}
|
||||||
|
self.animation_frame.reset(kind, repeat);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_current_animation_frame(&self) -> u32 {
|
||||||
|
self.animation_frame.frame
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_current_animation_finished(&self) -> bool {
|
||||||
|
let anims_data = self
|
||||||
|
.baked_animations
|
||||||
|
.get(&self.animations.baked)
|
||||||
|
.expect("Animation should exist");
|
||||||
|
anims_data
|
||||||
|
.frame_count(self.animation_frame.kind)
|
||||||
|
.expect("Current animation should have baked frames")
|
||||||
|
< self.animation_frame.frame
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sample_bone(&self, bone: &str) -> Option<FixedMat4> {
|
||||||
|
self.animations
|
||||||
|
.sample_bone(self.baked_animations, *self.animation_frame, bone)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fighter_states! {
|
fighter_states! {
|
||||||
@@ -97,7 +137,8 @@ fighter_states! {
|
|||||||
JumpSquat => jump::JumpSquatState,
|
JumpSquat => jump::JumpSquatState,
|
||||||
Jump => jump::JumpState,
|
Jump => jump::JumpState,
|
||||||
Land => land::LandingState,
|
Land => land::LandingState,
|
||||||
AirDodge => air_dodge::AirDodgeState
|
AirDodge => air_dodge::AirDodgeState,
|
||||||
|
Turn => turn::TurnState
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(QueryData)]
|
#[derive(QueryData)]
|
||||||
@@ -110,7 +151,8 @@ pub struct FighterFrameQuery {
|
|||||||
pub ecb: &'static mut FighterECB,
|
pub ecb: &'static mut FighterECB,
|
||||||
pub prev_ecb: &'static mut FighterPreviousECB,
|
pub prev_ecb: &'static mut FighterPreviousECB,
|
||||||
pub facing_direction: &'static mut FighterFacingDirection,
|
pub facing_direction: &'static mut FighterFacingDirection,
|
||||||
pub attributes: &'static FighterAttributes,
|
pub animation_frame: &'static mut FighterAnimationFrame,
|
||||||
|
pub fighter: &'static Fighter,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'w, 's> FighterFrameQueryItem<'w, 's> {
|
impl<'w, 's> FighterFrameQueryItem<'w, 's> {
|
||||||
@@ -121,6 +163,8 @@ impl<'w, 's> FighterFrameQueryItem<'w, 's> {
|
|||||||
animations: &'a FighterAnimations,
|
animations: &'a FighterAnimations,
|
||||||
animation_player: &'a mut AnimationPlayer,
|
animation_player: &'a mut AnimationPlayer,
|
||||||
animation_transitions: &'a mut AnimationTransitions,
|
animation_transitions: &'a mut AnimationTransitions,
|
||||||
|
baked_animations: &'a Assets<BakedFighterAnimations>,
|
||||||
|
manifest: &'a FighterManifest,
|
||||||
) -> FighterStateContext<'a> {
|
) -> FighterStateContext<'a> {
|
||||||
FighterStateContext {
|
FighterStateContext {
|
||||||
translation: &mut self.translation,
|
translation: &mut self.translation,
|
||||||
@@ -130,12 +174,14 @@ impl<'w, 's> FighterFrameQueryItem<'w, 's> {
|
|||||||
ecb: &mut self.ecb,
|
ecb: &mut self.ecb,
|
||||||
prev_ecb: &mut self.prev_ecb,
|
prev_ecb: &mut self.prev_ecb,
|
||||||
facing_direction: &mut self.facing_direction,
|
facing_direction: &mut self.facing_direction,
|
||||||
fighter_attribs: self.attributes,
|
|
||||||
game_settings,
|
game_settings,
|
||||||
stage_collision,
|
stage_collision,
|
||||||
animations,
|
animations,
|
||||||
animation_player,
|
animation_player,
|
||||||
animation_transitions,
|
animation_transitions,
|
||||||
|
animation_frame: &mut self.animation_frame,
|
||||||
|
baked_animations,
|
||||||
|
fighter_manifest: manifest,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -148,6 +194,7 @@ pub struct StatefulFighterQuery {
|
|||||||
pub frame: FighterFrameQuery,
|
pub frame: FighterFrameQuery,
|
||||||
pub animations: &'static FighterAnimations,
|
pub animations: &'static FighterAnimations,
|
||||||
pub animation_player_link: &'static FighterAnimationPlayerLink,
|
pub animation_player_link: &'static FighterAnimationPlayerLink,
|
||||||
|
pub fighter: &'static Fighter,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
@@ -164,6 +211,8 @@ pub fn state_update_system(
|
|||||||
mut animation_players: Query<(&mut AnimationPlayer, &mut AnimationTransitions)>,
|
mut animation_players: Query<(&mut AnimationPlayer, &mut AnimationTransitions)>,
|
||||||
stage_collision: Res<StageCollision>,
|
stage_collision: Res<StageCollision>,
|
||||||
game_settings: Res<GameSettings>,
|
game_settings: Res<GameSettings>,
|
||||||
|
baked_animations: Res<Assets<BakedFighterAnimations>>,
|
||||||
|
manifests: Res<Assets<FighterManifest>>,
|
||||||
) {
|
) {
|
||||||
for fighter in &mut query {
|
for fighter in &mut query {
|
||||||
let StatefulFighterQueryItem {
|
let StatefulFighterQueryItem {
|
||||||
@@ -178,12 +227,17 @@ pub fn state_update_system(
|
|||||||
else {
|
else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut update_ctx = frame.context(
|
let mut update_ctx = frame.context(
|
||||||
&game_settings,
|
&game_settings,
|
||||||
&stage_collision,
|
&stage_collision,
|
||||||
animations,
|
animations,
|
||||||
&mut animation_player,
|
&mut animation_player,
|
||||||
&mut animation_transitions,
|
&mut animation_transitions,
|
||||||
|
&baked_animations,
|
||||||
|
&manifests
|
||||||
|
.get(&(fighter.fighter.manifest))
|
||||||
|
.expect("Fighter manifest should be valid"),
|
||||||
);
|
);
|
||||||
state.update(&mut update_ctx);
|
state.update(&mut update_ctx);
|
||||||
}
|
}
|
||||||
@@ -194,6 +248,8 @@ pub fn state_interrupt_system(
|
|||||||
mut animation_players: Query<(&mut AnimationPlayer, &mut AnimationTransitions)>,
|
mut animation_players: Query<(&mut AnimationPlayer, &mut AnimationTransitions)>,
|
||||||
stage_collision: Res<StageCollision>,
|
stage_collision: Res<StageCollision>,
|
||||||
game_settings: Res<GameSettings>,
|
game_settings: Res<GameSettings>,
|
||||||
|
baked_animations: Res<Assets<BakedFighterAnimations>>,
|
||||||
|
fighter_manifests: Res<Assets<FighterManifest>>,
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
) {
|
) {
|
||||||
for fighter in &mut query {
|
for fighter in &mut query {
|
||||||
@@ -203,6 +259,7 @@ pub fn state_interrupt_system(
|
|||||||
mut frame,
|
mut frame,
|
||||||
animations,
|
animations,
|
||||||
animation_player_link,
|
animation_player_link,
|
||||||
|
fighter,
|
||||||
} = fighter;
|
} = fighter;
|
||||||
let Ok((mut animation_player, mut animation_transitions)) =
|
let Ok((mut animation_player, mut animation_transitions)) =
|
||||||
animation_players.get_mut(animation_player_link.player())
|
animation_players.get_mut(animation_player_link.player())
|
||||||
@@ -215,8 +272,12 @@ pub fn state_interrupt_system(
|
|||||||
animations,
|
animations,
|
||||||
&mut animation_player,
|
&mut animation_player,
|
||||||
&mut animation_transitions,
|
&mut animation_transitions,
|
||||||
|
&baked_animations,
|
||||||
|
fighter_manifests
|
||||||
|
.get(&fighter.manifest)
|
||||||
|
.expect("Fighter manifest should be valid"),
|
||||||
);
|
);
|
||||||
if let Some(new_state) = state.check_interrupt(&state_context) {
|
if let Some(new_state) = state.check_interrupt(&mut state_context) {
|
||||||
let mut new_state = new_state;
|
let mut new_state = new_state;
|
||||||
let old_state_name = state.name();
|
let old_state_name = state.name();
|
||||||
let new_state_name = new_state.name();
|
let new_state_name = new_state.name();
|
||||||
@@ -234,6 +295,8 @@ pub fn state_collision_interrupt_system(
|
|||||||
mut animation_players: Query<(&mut AnimationPlayer, &mut AnimationTransitions)>,
|
mut animation_players: Query<(&mut AnimationPlayer, &mut AnimationTransitions)>,
|
||||||
stage_collision: Res<StageCollision>,
|
stage_collision: Res<StageCollision>,
|
||||||
game_settings: Res<GameSettings>,
|
game_settings: Res<GameSettings>,
|
||||||
|
baked_animations: Res<Assets<BakedFighterAnimations>>,
|
||||||
|
fighter_manifests: Res<Assets<FighterManifest>>,
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
) {
|
) {
|
||||||
for fighter in &mut query {
|
for fighter in &mut query {
|
||||||
@@ -243,6 +306,7 @@ pub fn state_collision_interrupt_system(
|
|||||||
mut frame,
|
mut frame,
|
||||||
animations,
|
animations,
|
||||||
animation_player_link,
|
animation_player_link,
|
||||||
|
fighter,
|
||||||
} = fighter;
|
} = fighter;
|
||||||
let Ok((mut animation_player, mut animation_transitions)) =
|
let Ok((mut animation_player, mut animation_transitions)) =
|
||||||
animation_players.get_mut(animation_player_link.player())
|
animation_players.get_mut(animation_player_link.player())
|
||||||
@@ -255,6 +319,10 @@ pub fn state_collision_interrupt_system(
|
|||||||
animations,
|
animations,
|
||||||
&mut animation_player,
|
&mut animation_player,
|
||||||
&mut animation_transitions,
|
&mut animation_transitions,
|
||||||
|
&baked_animations,
|
||||||
|
fighter_manifests
|
||||||
|
.get(&fighter.manifest)
|
||||||
|
.expect("Fighter manifest should be valid"),
|
||||||
);
|
);
|
||||||
if let Some(new_state) = state.check_collision_interrupt(&mut state_context) {
|
if let Some(new_state) = state.check_collision_interrupt(&mut state_context) {
|
||||||
let mut new_state = new_state;
|
let mut new_state = new_state;
|
||||||
|
|||||||
@@ -16,8 +16,10 @@ pub struct RunState {
|
|||||||
|
|
||||||
impl FighterStateImpl for RunState {
|
impl FighterStateImpl for RunState {
|
||||||
const NAME: &'static str = "Run";
|
const NAME: &'static str = "Run";
|
||||||
fn check_interrupt(&self, state_context: &FighterStateContext) -> Option<FighterState> {
|
fn check_interrupt(&self, state_context: &mut FighterStateContext) -> Option<FighterState> {
|
||||||
if let Some(state) = ground::grounded_movement_common_interrupts(state_context, &self.grounded_common) {
|
if let Some(state) =
|
||||||
|
ground::grounded_movement_common_interrupts(state_context, &self.grounded_common)
|
||||||
|
{
|
||||||
return Some(state);
|
return Some(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,26 +35,20 @@ impl FighterStateImpl for RunState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn on_enter(&mut self, state_context: &mut super::FighterStateContext) {
|
fn on_enter(&mut self, state_context: &mut super::FighterStateContext) {
|
||||||
state_context
|
state_context.play_animation(AnimKind::Run, true);
|
||||||
.animation_transitions
|
|
||||||
.play(
|
|
||||||
&mut state_context.animation_player,
|
|
||||||
state_context.animations.clips[&AnimKind::Run],
|
|
||||||
Duration::ZERO,
|
|
||||||
)
|
|
||||||
.repeat();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(&mut self, state_context: &mut super::FighterStateContext) {
|
fn update(&mut self, state_context: &mut super::FighterStateContext) {
|
||||||
let last_frame = state_context.input.get_last_frame();
|
let last_frame = state_context.input.get_last_frame();
|
||||||
let (accel, target_vel) = state_context
|
let (accel, target_vel) = state_context
|
||||||
.fighter_attribs
|
.fighter_manifest
|
||||||
|
.attributes
|
||||||
.get_accel_and_target_dashrun(&last_frame, FGi32::ONE);
|
.get_accel_and_target_dashrun(&last_frame, FGi32::ONE);
|
||||||
let accel = ground::compute_ground_accel(
|
let accel = ground::compute_ground_accel(
|
||||||
accel,
|
accel,
|
||||||
target_vel,
|
target_vel,
|
||||||
state_context.velocity.x,
|
state_context.velocity.x,
|
||||||
state_context.fighter_attribs,
|
&state_context.fighter_manifest.attributes,
|
||||||
state_context.game_settings,
|
state_context.game_settings,
|
||||||
);
|
);
|
||||||
state_context.velocity.x += accel;
|
state_context.velocity.x += accel;
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
use super::{FighterStateContext, FighterStateImpl};
|
||||||
|
use crate::fighter::animation::AnimKind;
|
||||||
|
use crate::fighter::collision;
|
||||||
|
use crate::fighter::state::fall::FallState;
|
||||||
|
use crate::fighter::state::ground::{self, GroundedMotionResult, GroundedStateCommon};
|
||||||
|
use crate::fighter::state::land::LandingState;
|
||||||
|
use crate::fighter::state::{FighterState, air, dash, jump, wait, walk};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Hash)]
|
||||||
|
pub struct TurnState {
|
||||||
|
grounded_common: GroundedStateCommon,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FighterStateImpl for TurnState {
|
||||||
|
const NAME: &'static str = "Turn";
|
||||||
|
fn check_interrupt(&self, state_context: &mut FighterStateContext) -> Option<FighterState> {
|
||||||
|
// State chain that results in the turn completing
|
||||||
|
if let Some(state) = jump::check_input(state_context, &self.grounded_common) {
|
||||||
|
Some(state)
|
||||||
|
} else if state_context.is_current_animation_finished() {
|
||||||
|
if let Some(state) = dash::check_input(state_context, &self.grounded_common) {
|
||||||
|
Some(state)
|
||||||
|
} else if let Some(state) = walk::check_input(state_context, &self.grounded_common) {
|
||||||
|
Some(state)
|
||||||
|
} else if let Some(state) = wait::check_input(state_context, &self.grounded_common) {
|
||||||
|
Some(state)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_enter(&mut self, state_context: &mut super::FighterStateContext) {
|
||||||
|
state_context.play_animation(AnimKind::Turn, false);
|
||||||
|
*state_context.facing_direction = state_context.facing_direction.reverse();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, state_context: &mut super::FighterStateContext) {
|
||||||
|
let friction = if state_context.velocity.x.abs()
|
||||||
|
> state_context.fighter_manifest.attributes.max_walk_vel
|
||||||
|
{
|
||||||
|
state_context.fighter_manifest.attributes.ground_friction
|
||||||
|
* state_context
|
||||||
|
.game_settings
|
||||||
|
.fighter_common
|
||||||
|
.ground_friction_over_walk_speed_multiplier
|
||||||
|
} else {
|
||||||
|
state_context.fighter_manifest.attributes.ground_friction
|
||||||
|
};
|
||||||
|
|
||||||
|
let ground_velocity = state_context.velocity.x;
|
||||||
|
|
||||||
|
state_context.velocity.x += ground::apply_grounded_friction(friction, ground_velocity);
|
||||||
|
|
||||||
|
ground::apply_grounded_motion(
|
||||||
|
state_context.velocity,
|
||||||
|
state_context.translation,
|
||||||
|
state_context.prev_translation,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_collision_interrupt(
|
||||||
|
&mut self,
|
||||||
|
state_context: &mut FighterStateContext,
|
||||||
|
) -> Option<FighterState> {
|
||||||
|
if let GroundedMotionResult::InAir =
|
||||||
|
ground::collide_with_stage_grounded(state_context, &mut self.grounded_common, false)
|
||||||
|
{
|
||||||
|
Some(FighterState::Fall(FallState))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn check_input(
|
||||||
|
state_context: &FighterStateContext,
|
||||||
|
grounded_common: &GroundedStateCommon,
|
||||||
|
) -> Option<FighterState> {
|
||||||
|
let input_frame = state_context.input.get_last_frame();
|
||||||
|
|
||||||
|
if !input_frame.movement.x.is_zero()
|
||||||
|
&& input_frame.movement.x.signum() != state_context.facing_direction.to_sign()
|
||||||
|
{
|
||||||
|
return Some(FighterState::Turn(TurnState {
|
||||||
|
grounded_common: grounded_common.clone(),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
+31
-24
@@ -8,7 +8,7 @@ use crate::fighter::state::fall::FallState;
|
|||||||
use crate::fighter::state::ground::{
|
use crate::fighter::state::ground::{
|
||||||
GroundedMotionResult, GroundedStateCommon, grounded_movement_common_interrupts,
|
GroundedMotionResult, GroundedStateCommon, grounded_movement_common_interrupts,
|
||||||
};
|
};
|
||||||
use crate::fighter::state::{FighterState, dash};
|
use crate::fighter::state::{FighterState, dash, turn};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Hash)]
|
#[derive(Debug, Clone, Hash)]
|
||||||
pub struct WaitState {
|
pub struct WaitState {
|
||||||
@@ -17,35 +17,42 @@ pub struct WaitState {
|
|||||||
|
|
||||||
impl FighterStateImpl for WaitState {
|
impl FighterStateImpl for WaitState {
|
||||||
const NAME: &'static str = "Wait";
|
const NAME: &'static str = "Wait";
|
||||||
fn check_interrupt(&self, state_context: &FighterStateContext) -> Option<FighterState> {
|
fn check_interrupt(&self, state_context: &mut FighterStateContext) -> Option<FighterState> {
|
||||||
dash::check_input(state_context, &self.grounded_common).or_else(|| {
|
if let Some(state) = ground::grounded_movement_standstill_common_interrupts(
|
||||||
grounded_movement_common_interrupts(state_context, &self.grounded_common)
|
state_context,
|
||||||
.or_else(|| walk::check_input(state_context, &self.grounded_common))
|
&self.grounded_common,
|
||||||
})
|
) {
|
||||||
|
// Prevent transitioning into myself
|
||||||
|
if let FighterState::Wait(_) = state {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(state)
|
||||||
|
}
|
||||||
|
} else if let Some(state) =
|
||||||
|
ground::grounded_movement_common_interrupts(state_context, &self.grounded_common)
|
||||||
|
{
|
||||||
|
Some(state)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_enter(&mut self, state_context: &mut super::FighterStateContext) {
|
fn on_enter(&mut self, state_context: &mut super::FighterStateContext) {
|
||||||
state_context
|
state_context.play_animation(AnimKind::Wait, true);
|
||||||
.animation_transitions
|
|
||||||
.play(
|
|
||||||
&mut state_context.animation_player,
|
|
||||||
state_context.animations.clips[&AnimKind::Wait],
|
|
||||||
Duration::ZERO,
|
|
||||||
)
|
|
||||||
.repeat();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(&mut self, state_context: &mut super::FighterStateContext) {
|
fn update(&mut self, state_context: &mut super::FighterStateContext) {
|
||||||
let friction =
|
let friction = if state_context.velocity.x.abs()
|
||||||
if state_context.velocity.x.abs() > state_context.fighter_attribs.max_walk_vel {
|
> state_context.fighter_manifest.attributes.max_walk_vel
|
||||||
state_context.fighter_attribs.ground_friction
|
{
|
||||||
* state_context
|
state_context.fighter_manifest.attributes.ground_friction
|
||||||
.game_settings
|
* state_context
|
||||||
.fighter_common
|
.game_settings
|
||||||
.ground_friction_over_walk_speed_multiplier
|
.fighter_common
|
||||||
} else {
|
.ground_friction_over_walk_speed_multiplier
|
||||||
state_context.fighter_attribs.ground_friction
|
} else {
|
||||||
};
|
state_context.fighter_manifest.attributes.ground_friction
|
||||||
|
};
|
||||||
|
|
||||||
let ground_velocity = state_context.velocity.x;
|
let ground_velocity = state_context.velocity.x;
|
||||||
|
|
||||||
|
|||||||
+10
-14
@@ -4,9 +4,9 @@ use bevy::prelude::*;
|
|||||||
|
|
||||||
use super::{FighterStateContext, FighterStateImpl, dash, ground, wait};
|
use super::{FighterStateContext, FighterStateImpl, dash, ground, wait};
|
||||||
use crate::fighter::animation::AnimKind;
|
use crate::fighter::animation::AnimKind;
|
||||||
|
use crate::fighter::state::FighterState;
|
||||||
use crate::fighter::state::fall::FallState;
|
use crate::fighter::state::fall::FallState;
|
||||||
use crate::fighter::state::ground::{GroundedMotionResult, GroundedStateCommon};
|
use crate::fighter::state::ground::{GroundedMotionResult, GroundedStateCommon};
|
||||||
use crate::fighter::state::FighterState;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Hash)]
|
#[derive(Debug, Clone, Hash)]
|
||||||
pub struct WalkState {
|
pub struct WalkState {
|
||||||
@@ -15,8 +15,10 @@ pub struct WalkState {
|
|||||||
|
|
||||||
impl FighterStateImpl for WalkState {
|
impl FighterStateImpl for WalkState {
|
||||||
const NAME: &'static str = "Walk";
|
const NAME: &'static str = "Walk";
|
||||||
fn check_interrupt(&self, state_context: &FighterStateContext) -> Option<FighterState> {
|
fn check_interrupt(&self, state_context: &mut FighterStateContext) -> Option<FighterState> {
|
||||||
if let Some(state) = ground::grounded_movement_common_interrupts(state_context, &self.grounded_common) {
|
if let Some(state) =
|
||||||
|
ground::grounded_movement_common_interrupts(state_context, &self.grounded_common)
|
||||||
|
{
|
||||||
return Some(state);
|
return Some(state);
|
||||||
}
|
}
|
||||||
wait::check_input(state_context, &self.grounded_common)
|
wait::check_input(state_context, &self.grounded_common)
|
||||||
@@ -24,27 +26,21 @@ impl FighterStateImpl for WalkState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn on_enter(&mut self, state_context: &mut FighterStateContext) {
|
fn on_enter(&mut self, state_context: &mut FighterStateContext) {
|
||||||
state_context
|
state_context.play_animation(AnimKind::Walk, true);
|
||||||
.animation_transitions
|
|
||||||
.play(
|
|
||||||
&mut state_context.animation_player,
|
|
||||||
state_context.animations.clips[&AnimKind::Walk],
|
|
||||||
Duration::ZERO,
|
|
||||||
)
|
|
||||||
.repeat();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(&mut self, state_context: &mut FighterStateContext) {
|
fn update(&mut self, state_context: &mut FighterStateContext) {
|
||||||
let last_frame = state_context.input.get_last_frame();
|
let last_frame = state_context.input.get_last_frame();
|
||||||
let (accel, target_vel) = state_context
|
let (accel, target_vel) = state_context
|
||||||
.fighter_attribs
|
.fighter_manifest
|
||||||
|
.attributes
|
||||||
.get_accel_and_target_walk(&last_frame);
|
.get_accel_and_target_walk(&last_frame);
|
||||||
|
|
||||||
let accel = ground::compute_ground_accel(
|
let accel = ground::compute_ground_accel(
|
||||||
accel,
|
accel,
|
||||||
target_vel,
|
target_vel,
|
||||||
state_context.velocity.x,
|
state_context.velocity.x,
|
||||||
state_context.fighter_attribs,
|
&state_context.fighter_manifest.attributes,
|
||||||
&state_context.game_settings,
|
&state_context.game_settings,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -77,7 +73,7 @@ pub fn check_input(
|
|||||||
) -> Option<FighterState> {
|
) -> Option<FighterState> {
|
||||||
let input_frame = state_context.input.get_last_frame();
|
let input_frame = state_context.input.get_last_frame();
|
||||||
|
|
||||||
if input_frame.movement.x.abs() >= state_context.game_settings.input_common.stick_deadzone {
|
if !input_frame.movement.x.is_zero() {
|
||||||
return Some(FighterState::Walk(WalkState {
|
return Some(FighterState::Walk(WalkState {
|
||||||
grounded_common: ground_state_common.clone(),
|
grounded_common: ground_state_common.clone(),
|
||||||
}));
|
}));
|
||||||
|
|||||||
+25
-1
@@ -1,10 +1,34 @@
|
|||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use crate::fighter::animation::AnimKind;
|
use crate::fighter::animation::{AnimKind, FighterAnimationFrame};
|
||||||
|
use crate::fighter::baked_animation::BakedFighterAnimations;
|
||||||
|
|
||||||
#[derive(Component, Clone)]
|
#[derive(Component, Clone)]
|
||||||
pub struct FighterAnimations {
|
pub struct FighterAnimations {
|
||||||
pub graph: Handle<AnimationGraph>,
|
pub graph: Handle<AnimationGraph>,
|
||||||
pub clips: HashMap<AnimKind, AnimationNodeIndex>,
|
pub clips: HashMap<AnimKind, AnimationNodeIndex>,
|
||||||
|
/// Gameplay samples this fixed-point asset; the graph remains visual-only.
|
||||||
|
pub baked: Handle<BakedFighterAnimations>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FighterAnimations {
|
||||||
|
pub fn sample_bone(
|
||||||
|
&self,
|
||||||
|
baked_assets: &Assets<BakedFighterAnimations>,
|
||||||
|
frame: FighterAnimationFrame,
|
||||||
|
bone: &str,
|
||||||
|
) -> Option<crate::fighter::baked_animation::FixedMat4> {
|
||||||
|
let baked = baked_assets.get(&self.baked)?;
|
||||||
|
let frame_count = baked.frame_count(frame.kind)?;
|
||||||
|
if frame_count == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let frame_index = if frame.repeat {
|
||||||
|
frame.frame % frame_count
|
||||||
|
} else {
|
||||||
|
frame.frame
|
||||||
|
};
|
||||||
|
baked.sample(frame.kind, frame_index, bone)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-1
@@ -21,7 +21,7 @@
|
|||||||
//! save, click Rebuild in jackdaw (or run `jd build`) and it
|
//! save, click Rebuild in jackdaw (or run `jd build`) and it
|
||||||
//! appears in `Add Component`. No registration code is needed.
|
//! appears in `Add Component`. No registration code is needed.
|
||||||
|
|
||||||
use bevy::prelude::*;
|
use bevy::{asset::processor::{AssetProcessor, ProcessorState}, prelude::*, tasks::block_on};
|
||||||
use bevy_asset_loader::prelude::*;
|
use bevy_asset_loader::prelude::*;
|
||||||
use bevy_wind_waker_shader::prelude::*;
|
use bevy_wind_waker_shader::prelude::*;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
@@ -46,6 +46,7 @@ pub mod stage;
|
|||||||
#[derive(States, Debug, Clone, Copy, Eq, PartialEq, Hash, Default)]
|
#[derive(States, Debug, Clone, Copy, Eq, PartialEq, Hash, Default)]
|
||||||
pub enum AppState {
|
pub enum AppState {
|
||||||
#[default]
|
#[default]
|
||||||
|
WaitForAssetProcessing,
|
||||||
LoadCommonAssets,
|
LoadCommonAssets,
|
||||||
LoadFighterManifests,
|
LoadFighterManifests,
|
||||||
PrepareFighterManifests,
|
PrepareFighterManifests,
|
||||||
@@ -78,6 +79,7 @@ impl Plugin for GamePlugin {
|
|||||||
GameplaySchedulePlugin,
|
GameplaySchedulePlugin,
|
||||||
input::FighterInputPlugin,
|
input::FighterInputPlugin,
|
||||||
fighter::FighterPlugin,
|
fighter::FighterPlugin,
|
||||||
|
fighter::baked_animation::BakedAnimationPlugin,
|
||||||
stage::StagePlugin,
|
stage::StagePlugin,
|
||||||
MatchCameraPlugin,
|
MatchCameraPlugin,
|
||||||
bevy_common_assets::ron::RonAssetPlugin::<FighterManifest>::new(&["fighter.ron"]),
|
bevy_common_assets::ron::RonAssetPlugin::<FighterManifest>::new(&["fighter.ron"]),
|
||||||
@@ -109,6 +111,10 @@ impl Plugin for GamePlugin {
|
|||||||
.on_failure_continue_to_state(AppState::CommonAssetLoadFailed)
|
.on_failure_continue_to_state(AppState::CommonAssetLoadFailed)
|
||||||
.load_collection::<stage::manifest::StageManifestAssets>(),
|
.load_collection::<stage::manifest::StageManifestAssets>(),
|
||||||
)
|
)
|
||||||
|
.add_systems(
|
||||||
|
Update,
|
||||||
|
wait_for_asset_processor.run_if(in_state(AppState::WaitForAssetProcessing)),
|
||||||
|
)
|
||||||
.add_systems(
|
.add_systems(
|
||||||
OnEnter(AppState::PrepareFighterManifests),
|
OnEnter(AppState::PrepareFighterManifests),
|
||||||
fighter::manifest::prepare_fighter_manifests,
|
fighter::manifest::prepare_fighter_manifests,
|
||||||
@@ -131,6 +137,15 @@ impl Plugin for GamePlugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn wait_for_asset_processor(
|
||||||
|
processor: Option<Res<AssetProcessor>>,
|
||||||
|
mut next_state: ResMut<NextState<AppState>>,
|
||||||
|
) {
|
||||||
|
if processor.is_none_or(|processor| block_on(processor.get_state()) == ProcessorState::Finished) {
|
||||||
|
next_state.set(AppState::LoadCommonAssets);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Spin-rate in radians per second. Attach it in the inspector; the
|
/// Spin-rate in radians per second. Attach it in the inspector; the
|
||||||
/// editor sees this component with no registration code because Bevy's
|
/// editor sees this component with no registration code because Bevy's
|
||||||
/// `reflect_auto_register` picks up the `Reflect` derive. `Default` +
|
/// `reflect_auto_register` picks up the `Reflect` derive. `Default` +
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
//! without the editor. The editor's Play button runs the same
|
//! without the editor. The editor's Play button runs the same
|
||||||
//! [`game_test::GamePlugin`] through its own game runner instead.
|
//! [`game_test::GamePlugin`] through its own game runner instead.
|
||||||
|
|
||||||
|
use bevy::asset::AssetMode::Processed;
|
||||||
use bevy::camera_controller::free_camera::FreeCameraPlugin;
|
use bevy::camera_controller::free_camera::FreeCameraPlugin;
|
||||||
use bevy::image::{ImageAddressMode, ImagePlugin, ImageSamplerDescriptor};
|
use bevy::image::{ImageAddressMode, ImagePlugin, ImageSamplerDescriptor};
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
@@ -21,6 +22,9 @@ fn main() -> AppExit {
|
|||||||
address_mode_w: ImageAddressMode::Repeat,
|
address_mode_w: ImageAddressMode::Repeat,
|
||||||
..ImageSamplerDescriptor::linear()
|
..ImageSamplerDescriptor::linear()
|
||||||
},
|
},
|
||||||
|
}).set(AssetPlugin {
|
||||||
|
mode: Processed,
|
||||||
|
..default()
|
||||||
});
|
});
|
||||||
|
|
||||||
App::new()
|
App::new()
|
||||||
|
|||||||
+44
-6
@@ -12,6 +12,7 @@ use crate::{
|
|||||||
fighter::{
|
fighter::{
|
||||||
FighterId,
|
FighterId,
|
||||||
animation::AnimKind,
|
animation::AnimKind,
|
||||||
|
baked_animation::BakedFighterAnimations,
|
||||||
manifest::{FighterManifest, FighterManifestRegistry},
|
manifest::{FighterManifest, FighterManifestRegistry},
|
||||||
spawn_fighter,
|
spawn_fighter,
|
||||||
visual::FighterAnimations,
|
visual::FighterAnimations,
|
||||||
@@ -39,6 +40,7 @@ pub struct PendingMatch {
|
|||||||
pub struct SelectedFighterAssets {
|
pub struct SelectedFighterAssets {
|
||||||
pub fighter: FighterId,
|
pub fighter: FighterId,
|
||||||
pub model: Handle<Gltf>,
|
pub model: Handle<Gltf>,
|
||||||
|
pub baked_animations: Handle<BakedFighterAnimations>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Resource)]
|
#[derive(Resource)]
|
||||||
@@ -79,6 +81,7 @@ impl MatchAssets {
|
|||||||
SelectedFighterAssets {
|
SelectedFighterAssets {
|
||||||
fighter: player.fighter,
|
fighter: player.fighter,
|
||||||
model: asset_server.load(manifest.model_path.clone()),
|
model: asset_server.load(manifest.model_path.clone()),
|
||||||
|
baked_animations: asset_server.load(manifest.baked_animation_path.clone()),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
@@ -97,6 +100,7 @@ impl AssetCollection for MatchAssets {
|
|||||||
handles.push(assets.stage.clone().untyped());
|
handles.push(assets.stage.clone().untyped());
|
||||||
for character in assets.fighters {
|
for character in assets.fighters {
|
||||||
handles.push(character.model.untyped());
|
handles.push(character.model.untyped());
|
||||||
|
handles.push(character.baked_animations.untyped());
|
||||||
}
|
}
|
||||||
handles
|
handles
|
||||||
}
|
}
|
||||||
@@ -141,6 +145,7 @@ pub fn prepare_match(
|
|||||||
request: Res<PendingMatch>,
|
request: Res<PendingMatch>,
|
||||||
assets: Res<MatchAssets>,
|
assets: Res<MatchAssets>,
|
||||||
gltfs: Res<Assets<Gltf>>,
|
gltfs: Res<Assets<Gltf>>,
|
||||||
|
baked_animations: Res<Assets<BakedFighterAnimations>>,
|
||||||
registry: Res<FighterManifestRegistry>,
|
registry: Res<FighterManifestRegistry>,
|
||||||
stage_registry: Res<StageManifestRegistry>,
|
stage_registry: Res<StageManifestRegistry>,
|
||||||
manifests: Res<Assets<FighterManifest>>,
|
manifests: Res<Assets<FighterManifest>>,
|
||||||
@@ -180,6 +185,13 @@ pub fn prepare_match(
|
|||||||
fail_match(&mut next_state, "a loaded character GLTF is unavailable");
|
fail_match(&mut next_state, "a loaded character GLTF is unavailable");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
let Some(baked) = baked_animations.get(&character.baked_animations) else {
|
||||||
|
fail_match(
|
||||||
|
&mut next_state,
|
||||||
|
"a fighter baked animation asset is unavailable",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
};
|
||||||
let Some(scene) = gltf.scenes.first().cloned() else {
|
let Some(scene) = gltf.scenes.first().cloned() else {
|
||||||
fail_match(&mut next_state, "a character GLTF contains no scene");
|
fail_match(&mut next_state, "a character GLTF contains no scene");
|
||||||
return;
|
return;
|
||||||
@@ -191,6 +203,8 @@ pub fn prepare_match(
|
|||||||
animations.clone(),
|
animations.clone(),
|
||||||
manifest.attributes,
|
manifest.attributes,
|
||||||
manifest.camera,
|
manifest.camera,
|
||||||
|
manifest,
|
||||||
|
manifest_handle,
|
||||||
));
|
));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -210,6 +224,16 @@ pub fn prepare_match(
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
clips.insert(*kind, graph.add_clip(clip.clone(), 1.0, graph.root));
|
clips.insert(*kind, graph.add_clip(clip.clone(), 1.0, graph.root));
|
||||||
|
if baked.frame_count(*kind).is_none() {
|
||||||
|
fail_match(
|
||||||
|
&mut next_state,
|
||||||
|
&format!(
|
||||||
|
"{:?} baked data has no {kind:?} animation",
|
||||||
|
character.fighter
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !clips.contains_key(&AnimKind::Wait) {
|
if !clips.contains_key(&AnimKind::Wait) {
|
||||||
@@ -220,9 +244,17 @@ pub fn prepare_match(
|
|||||||
let animations = FighterAnimations {
|
let animations = FighterAnimations {
|
||||||
graph: graphs.add(graph),
|
graph: graphs.add(graph),
|
||||||
clips,
|
clips,
|
||||||
|
baked: character.baked_animations.clone(),
|
||||||
};
|
};
|
||||||
shared_animations.insert(character.fighter, animations.clone());
|
shared_animations.insert(character.fighter, animations.clone());
|
||||||
prepared.push((scene, animations, manifest.attributes, manifest.camera));
|
prepared.push((
|
||||||
|
scene,
|
||||||
|
animations,
|
||||||
|
manifest.attributes,
|
||||||
|
manifest.camera,
|
||||||
|
manifest,
|
||||||
|
manifest_handle,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let session = match create_session(&args, request.players.len()) {
|
let session = match create_session(&args, request.players.len()) {
|
||||||
@@ -238,8 +270,12 @@ pub fn prepare_match(
|
|||||||
|
|
||||||
let root = commands.spawn((Name::new("Match"), MatchRoot)).id();
|
let root = commands.spawn((Name::new("Match"), MatchRoot)).id();
|
||||||
|
|
||||||
let stage_handle = stage_registry.get(request.stage).expect("Initialization check should ensure all stages exist");
|
let stage_handle = stage_registry
|
||||||
let stage_manifest = stage_manifests.get(&stage_handle).expect("Initialization check should ensure stage manifests are kept alive");
|
.get(request.stage)
|
||||||
|
.expect("Initialization check should ensure all stages exist");
|
||||||
|
let stage_manifest = stage_manifests
|
||||||
|
.get(&stage_handle)
|
||||||
|
.expect("Initialization check should ensure stage manifests are kept alive");
|
||||||
stage::spawn_stage(
|
stage::spawn_stage(
|
||||||
&mut commands,
|
&mut commands,
|
||||||
root,
|
root,
|
||||||
@@ -248,16 +284,18 @@ pub fn prepare_match(
|
|||||||
stage_manifest.camera.clone(),
|
stage_manifest.camera.clone(),
|
||||||
);
|
);
|
||||||
|
|
||||||
for (index, (player, (scene, animations, attributes, camera_profile))) in
|
for (
|
||||||
request.players.iter().zip(prepared).enumerate()
|
index,
|
||||||
|
(player, (scene, animations, attributes, camera_profile, manifest, manifest_handle)),
|
||||||
|
) in request.players.iter().zip(prepared).enumerate()
|
||||||
{
|
{
|
||||||
let spawn_x = (index as i32 * 2 + 1 - request.players.len() as i32) * 5;
|
let spawn_x = (index as i32 * 2 + 1 - request.players.len() as i32) * 5;
|
||||||
spawn_fighter(
|
spawn_fighter(
|
||||||
&mut commands,
|
&mut commands,
|
||||||
player.handle,
|
player.handle,
|
||||||
FGVec2::new(FGi32::from_num(spawn_x), FGi32::lit("12.5")),
|
FGVec2::new(FGi32::from_num(spawn_x), FGi32::lit("12.5")),
|
||||||
|
(manifest_handle.clone(), manifest),
|
||||||
attributes,
|
attributes,
|
||||||
camera_profile,
|
|
||||||
animations,
|
animations,
|
||||||
WorldAssetRoot(scene),
|
WorldAssetRoot(scene),
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user