Add animation baking system

This commit is contained in:
2026-08-28 23:05:36 +02:00
parent c1315e09b6
commit c3f1074fe1
30 changed files with 1200 additions and 244 deletions
+1
View File
@@ -1,2 +1,3 @@
/target /target
/.jackdaw /.jackdaw
imported_assets
Generated
+22 -2
View File
@@ -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
View File
@@ -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" }
+12 -2
View File
@@ -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",
} }
) )
+3
View File
@@ -0,0 +1,3 @@
(
manifest_path: "fighters/test/test.fighter.ron",
)
+20 -1
View File
@@ -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,
@@ -62,4 +81,4 @@
] ]
) )
] ]
) )
+11 -7
View File
@@ -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(
+40 -3
View File
@@ -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,
} }
+1 -1
View File
@@ -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,
+528
View File
@@ -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
View File
@@ -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
View File
@@ -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)
} }
} }
+8
View File
@@ -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
View File
@@ -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(),
)); ));
+11 -6
View File
@@ -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;
+9 -10
View File
@@ -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
View File
@@ -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) {
+7 -8
View File
@@ -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(
+19 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;
+8 -12
View File
@@ -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;
+92
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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` +
+4
View File
@@ -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
View File
@@ -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),
); );