From 3997beee03894fbbe02d4e7444e0ac7e7e99f2be Mon Sep 17 00:00:00 2001 From: PROMETHIA-27 <42193387+PROMETHIA-27@users.noreply.github.com> Date: Mon, 23 Jun 2025 06:02:26 -0400 Subject: [PATCH] Wiring up animations (#47) --- src/abilities/healing.rs | 3 +- src/abilities/mod.rs | 3 +- src/ai/mod.rs | 3 +- src/animation.rs | 127 ++++++++++++++++++++++++++++++ src/backpack/mod.rs | 3 +- src/camera.rs | 5 +- src/character.rs | 100 +++++++++++++++-------- src/control/controller_common.rs | 37 ++++----- src/control/controller_flying.rs | 11 +-- src/control/controller_running.rs | 49 ++++++++---- src/control/controls.rs | 5 +- src/control/mod.rs | 3 +- src/heads/heads_ui.rs | 3 +- src/heads/mod.rs | 2 +- src/hitpoints.rs | 64 +++++++++++++-- src/main.rs | 5 +- src/player.rs | 59 ++------------ src/tb_entities.rs | 8 +- src/utils/explosions.rs | 3 +- src/utils/trail.rs | 3 +- src/water.rs | 6 +- 21 files changed, 333 insertions(+), 169 deletions(-) create mode 100644 src/animation.rs diff --git a/src/abilities/healing.rs b/src/abilities/healing.rs index a147602..0d1e6d3 100644 --- a/src/abilities/healing.rs +++ b/src/abilities/healing.rs @@ -1,9 +1,8 @@ -use bevy::prelude::*; - use crate::{ GameState, global_observer, heads::ActiveHeads, heads_database::HeadsDatabase, hitpoints::Hitpoints, loading_assets::AudioAssets, }; +use bevy::prelude::*; #[derive(Component)] pub struct Healing(pub Entity); diff --git a/src/abilities/mod.rs b/src/abilities/mod.rs index d62fda6..fd24de0 100644 --- a/src/abilities/mod.rs +++ b/src/abilities/mod.rs @@ -5,8 +5,6 @@ mod healing; mod missile; mod thrown; -pub use healing::Healing; - use crate::{ GameState, aim::AimTarget, @@ -19,6 +17,7 @@ use crate::{ sounds::PlaySound, }; use bevy::prelude::*; +pub use healing::Healing; use healing::HealingStateChanged; use serde::{Deserialize, Serialize}; diff --git a/src/ai/mod.rs b/src/ai/mod.rs index edd24ba..5a95d05 100644 --- a/src/ai/mod.rs +++ b/src/ai/mod.rs @@ -1,5 +1,3 @@ -use bevy::prelude::*; - use crate::{ GameState, abilities::{HeadAbility, TriggerData, TriggerThrow}, @@ -8,6 +6,7 @@ use crate::{ heads_database::HeadsDatabase, player::Player, }; +use bevy::prelude::*; #[derive(Component, Reflect)] #[reflect(Component)] diff --git a/src/animation.rs b/src/animation.rs new file mode 100644 index 0000000..be34c28 --- /dev/null +++ b/src/animation.rs @@ -0,0 +1,127 @@ +use crate::{ + GameState, character::CharacterAnimations, head::ActiveHead, heads_database::HeadsDatabase, +}; +use bevy::{animation::RepeatAnimation, ecs::query::QueryData, prelude::*}; +use std::time::Duration; + +pub fn plugin(app: &mut App) { + app.register_type::(); + + app.add_systems( + Update, + update_animation.run_if(in_state(GameState::Playing)), + ); +} + +#[derive(Component, Default, Reflect)] +#[reflect(Component)] +pub struct AnimationFlags { + pub any_direction: bool, + pub jumping: bool, + pub just_jumped: bool, + pub shooting: bool, + pub hit: bool, +} + +#[derive(QueryData)] +#[query_data(mutable)] +pub struct AnimationController { + pub transitions: &'static mut AnimationTransitions, + pub player: &'static mut AnimationPlayer, +} + +impl AnimationController { + pub fn play_inner( + player: &mut AnimationPlayer, + transitions: &mut AnimationTransitions, + animation: AnimationNodeIndex, + transition: Duration, + repeat: RepeatAnimation, + ) { + transitions + .play(player, animation, transition) + .set_repeat(repeat); + } +} + +impl AnimationControllerItem<'_> { + pub fn play( + &mut self, + animation: AnimationNodeIndex, + transition: Duration, + repeat: RepeatAnimation, + ) { + AnimationController::play_inner( + &mut self.player, + &mut self.transitions, + animation, + transition, + repeat, + ); + } + + pub fn is_playing(&self, index: AnimationNodeIndex) -> bool { + self.player.is_playing_animation(index) + } +} + +const DEFAULT_TRANSITION_DURATION: Duration = Duration::from_millis(100); + +fn update_animation( + mut animated: Query<( + AnimationController, + &CharacterAnimations, + &mut AnimationFlags, + )>, +) { + for (mut controller, anims, mut flags) in animated.iter_mut() { + if flags.shooting && flags.any_direction && anims.run_shoot.is_some() { + if !controller.is_playing(anims.run_shoot.unwrap()) { + controller.play( + anims.run_shoot.unwrap(), + DEFAULT_TRANSITION_DURATION, + RepeatAnimation::Forever, + ); + } + } else if flags.shooting && anims.shoot.is_some() { + if !controller.is_playing(anims.shoot.unwrap()) { + controller.play( + anims.shoot.unwrap(), + DEFAULT_TRANSITION_DURATION, + RepeatAnimation::Forever, + ); + } + } else if flags.hit { + if !controller.is_playing(anims.hit) { + controller.play( + anims.hit, + DEFAULT_TRANSITION_DURATION, + RepeatAnimation::Never, + ); + } + } else if flags.jumping { + if !controller.is_playing(anims.jump) || flags.just_jumped { + controller.play( + anims.jump, + DEFAULT_TRANSITION_DURATION, + RepeatAnimation::Never, + ); + flags.just_jumped = false; + } + } else if flags.any_direction { + if !controller.player.is_playing_animation(anims.run) { + controller.play( + anims.run, + DEFAULT_TRANSITION_DURATION, + RepeatAnimation::Forever, + ); + } + } else if !controller.is_playing(anims.idle) { + controller.play( + anims.idle, + DEFAULT_TRANSITION_DURATION, + RepeatAnimation::Forever, + ); + } + } +} diff --git a/src/backpack/mod.rs b/src/backpack/mod.rs index fbf9287..e42dc55 100644 --- a/src/backpack/mod.rs +++ b/src/backpack/mod.rs @@ -5,9 +5,8 @@ use crate::{ cash::CashCollectEvent, global_observer, head_drop::HeadCollected, heads::HeadState, heads_database::HeadsDatabase, }; -use bevy::prelude::*; - pub use backpack_ui::BackpackAction; +use bevy::prelude::*; pub use ui_head_state::UiHeadState; #[derive(Resource, Default)] diff --git a/src/camera.rs b/src/camera.rs index 1983dab..45eeff2 100644 --- a/src/camera.rs +++ b/src/camera.rs @@ -1,9 +1,8 @@ -use avian3d::prelude::*; -use bevy::prelude::*; - use crate::{ GameState, control::ControlState, loading_assets::UIAssets, physics_layers::GameLayer, }; +use avian3d::prelude::*; +use bevy::prelude::*; #[derive(Component, Reflect, Debug)] pub struct CameraTarget; diff --git a/src/character.rs b/src/character.rs index 7a68033..a2b0884 100644 --- a/src/character.rs +++ b/src/character.rs @@ -1,8 +1,14 @@ use crate::{ - GameState, heads_database::HeadsDatabase, loading_assets::GameAssets, utils::trail::Trail, + GameState, + animation::{AnimationController, AnimationFlags}, + heads_database::HeadsDatabase, + hitpoints::Hitpoints, + loading_assets::GameAssets, + utils::trail::Trail, }; use bevy::{ - ecs::system::SystemParam, platform::collections::HashMap, prelude::*, scene::SceneInstanceReady, + animation::RepeatAnimation, ecs::system::SystemParam, platform::collections::HashMap, + prelude::*, scene::SceneInstanceReady, }; use std::{f32::consts::PI, time::Duration}; @@ -39,14 +45,31 @@ impl CharacterHierarchy<'_, '_> { #[derive(Component, Debug, Reflect)] #[reflect(Component)] +#[relationship(relationship_target = HasCharacterAnimations)] +#[require(AnimationFlags)] pub struct CharacterAnimations { + #[relationship] + pub of_character: Entity, pub idle: AnimationNodeIndex, pub run: AnimationNodeIndex, pub jump: AnimationNodeIndex, - pub shooting: Option, + pub shoot: Option, + pub run_shoot: Option, + pub hit: AnimationNodeIndex, pub graph: Handle, } +const ANIM_IDLE: &str = "idle"; +const ANIM_RUN: &str = "run"; +const ANIM_JUMP: &str = "jump"; +const ANIM_SHOOT: &str = "shoot"; +const ANIM_RUN_SHOOT: &str = "run_shoot"; +const ANIM_HIT: &str = "hit"; + +#[derive(Component)] +#[relationship_target(relationship = CharacterAnimations)] +pub struct HasCharacterAnimations(Entity); + pub fn plugin(app: &mut App) { app.add_systems( Update, @@ -142,18 +165,25 @@ fn setup_once_loaded( mut query: Query<(Entity, &mut AnimationPlayer), Added>, parent: Query<&ChildOf>, animated_character: Query<(&AnimatedCharacter, &AnimatedCharacterAsset)>, - + characters: Query>, gltf_assets: Res>, mut graphs: ResMut>, ) { - for (entity, mut player) in &mut query { - let Some((_character, asset)) = parent + for (entity, mut player) in query.iter_mut() { + let Some((_, asset)) = parent .iter_ancestors(entity) .find_map(|ancestor| animated_character.get(ancestor).ok()) else { continue; }; + let Some(character) = parent + .iter_ancestors(entity) + .find_map(|ancestor| characters.get(ancestor).ok()) + else { + continue; + }; + let asset = gltf_assets.get(asset.0.id()).unwrap(); let animations = asset @@ -162,41 +192,45 @@ fn setup_once_loaded( .map(|(name, animation)| (name.to_string(), animation.clone())) .collect::>(); - let mut clips = vec![ - animations["idle"].clone(), - animations["run"].clone(), - animations["jump"].clone(), - ]; - - if let Some(shooting_animation) = animations.get("shoot") { - clips.push(shooting_animation.clone()); - } - - let (graph, node_indices) = AnimationGraph::from_clips(clips); + let mut graph = AnimationGraph::new(); + let root = graph.root; + let idle = graph.add_clip(animations[ANIM_IDLE].clone(), 1.0, root); + let run = graph.add_clip(animations[ANIM_RUN].clone(), 1.0, root); + let jump = graph.add_clip(animations[ANIM_JUMP].clone(), 1.0, root); + let shoot = animations + .get(ANIM_SHOOT) + .map(|it| graph.add_clip(it.clone(), 1.0, root)); + let run_shoot = animations + .get(ANIM_RUN_SHOOT) + .map(|it| graph.add_clip(it.clone(), 1.0, root)); + let hit = graph.add_clip(animations[ANIM_HIT].clone(), 1.0, root); // Insert a resource with the current scene information let graph_handle = graphs.add(graph); let animations = CharacterAnimations { - idle: node_indices[0], - run: node_indices[1], - jump: node_indices[2], - shooting: if node_indices.len() == 4 { - Some(node_indices[3]) - } else { - None - }, + of_character: character, + idle, + run, + jump, + shoot, + run_shoot, + hit, graph: graph_handle.clone(), }; let mut transitions = AnimationTransitions::new(); - transitions - .play(&mut player, animations.idle, Duration::ZERO) - .repeat(); - commands - .entity(entity) - .insert(AnimationGraphHandle(animations.graph.clone())) - .insert(transitions) - .insert(animations); + AnimationController::play_inner( + &mut player, + &mut transitions, + animations.idle, + Duration::ZERO, + RepeatAnimation::Forever, + ); + commands.entity(entity).insert(( + AnimationGraphHandle(animations.graph.clone()), + transitions, + animations, + )); } } diff --git a/src/control/controller_common.rs b/src/control/controller_common.rs index 1fcc37a..19ed8ea 100644 --- a/src/control/controller_common.rs +++ b/src/control/controller_common.rs @@ -1,27 +1,26 @@ +use super::{ControllerSet, ControllerSwitchEvent}; +use crate::{ + GameState, + control::{SelectedController, controls::ControllerSettings}, + heads_database::HeadControls, + player::PlayerBodyMesh, +}; use avian3d::{math::*, prelude::*}; use bevy::prelude::*; -use happy_feet::KinematicVelocity; -use happy_feet::ground::{Grounding, GroundingConfig}; -use happy_feet::prelude::{ - Character, CharacterDrag, CharacterFriction, CharacterGravity, CharacterMovement, - CharacterPlugin, MoveInput, SteppingBehaviour, SteppingConfig, +use happy_feet::{ + KinematicVelocity, + ground::{Grounding, GroundingConfig}, + prelude::{ + Character, CharacterDrag, CharacterFriction, CharacterGravity, CharacterMovement, + CharacterPlugin, MoveInput, SteppingBehaviour, SteppingConfig, + }, }; -use crate::GameState; -use crate::control::SelectedController; -use crate::control::controls::ControllerSettings; -use crate::heads_database::HeadControls; -use crate::player::PlayerBodyMesh; - -use super::{ControllerSet, ControllerSwitchEvent}; - pub fn plugin(app: &mut App) { app.add_plugins(CharacterPlugin::default()); app.register_type::(); - app.init_resource::(); - app.add_systems( PreUpdate, reset_upon_switch @@ -78,7 +77,7 @@ fn decelerate( &ControllerSettings, )>, ) { - for (mut velocity, input, grounding, settings) in &mut character { + for (mut velocity, input, grounding, settings) in character.iter_mut() { let direction = input.value.normalize(); let ground_normal = grounding .and_then(|it| it.normal()) @@ -105,12 +104,6 @@ fn decelerate( } } -#[derive(Resource, Default)] -pub struct PlayerMovement { - pub any_direction: bool, - pub shooting: bool, -} - #[derive(Component, Reflect)] #[reflect(Component)] pub struct MovementSpeedFactor(pub f32); diff --git a/src/control/controller_flying.rs b/src/control/controller_flying.rs index e218661..3faeccc 100644 --- a/src/control/controller_flying.rs +++ b/src/control/controller_flying.rs @@ -1,13 +1,8 @@ -use std::f32::consts::PI; - +use super::{ControlState, ControllerSet}; +use crate::{GameState, control::controller_common::MovementSpeedFactor, player::PlayerBodyMesh}; use bevy::prelude::*; use happy_feet::prelude::MoveInput; - -use crate::GameState; -use crate::control::controller_common::MovementSpeedFactor; -use crate::player::PlayerBodyMesh; - -use super::{ControlState, ControllerSet}; +use std::f32::consts::PI; pub struct CharacterControllerPlugin; diff --git a/src/control/controller_running.rs b/src/control/controller_running.rs index 2c415e0..67858e8 100644 --- a/src/control/controller_running.rs +++ b/src/control/controller_running.rs @@ -1,19 +1,22 @@ use super::{ControlState, ControllerSet, Controls}; -use crate::control::controller_common::MovementSpeedFactor; -use crate::control::controls::ControllerSettings; -use crate::{GameState, abilities::TriggerStateRes, player::PlayerBodyMesh}; +use crate::{ + GameState, + abilities::TriggerStateRes, + animation::AnimationFlags, + character::HasCharacterAnimations, + control::{controller_common::MovementSpeedFactor, controls::ControllerSettings}, + player::{Player, PlayerBodyMesh}, +}; use bevy::prelude::*; use happy_feet::prelude::{Grounding, KinematicVelocity, MoveInput}; -use super::controller_common::PlayerMovement; - pub struct CharacterControllerPlugin; impl Plugin for CharacterControllerPlugin { fn build(&self, app: &mut App) { app.add_systems( PreUpdate, - (set_movement_flag, rotate_view, apply_controls) + (set_animation_flags, rotate_view, apply_controls) .chain() .in_set(ControllerSet::ApplyControlsRun) .run_if(in_state(GameState::Playing)), @@ -22,28 +25,38 @@ impl Plugin for CharacterControllerPlugin { } /// Sets the movement flag, which is an indicator for the rig animation and the braking system. -fn set_movement_flag( - mut player_movement: ResMut, +fn set_animation_flags( + mut flags: Query<&mut AnimationFlags>, controls: Res, trigger: Res, + player: Single<(&Grounding, &HasCharacterAnimations), With>, ) { let mut direction = controls.keyboard_state.move_dir; let deadzone = 0.2; + let (grounding, has_anims) = *player; + let mut flags = flags.get_mut(*has_anims.collection()).unwrap(); + if let Some(gamepad) = controls.gamepad_state { direction += gamepad.move_dir; } - if player_movement.any_direction { + if flags.any_direction { if direction.length_squared() < deadzone { - player_movement.any_direction = false; + flags.any_direction = false; } } else if direction.length_squared() > deadzone { - player_movement.any_direction = true; + flags.any_direction = true; } - if player_movement.shooting != trigger.is_active() { - player_movement.shooting = trigger.is_active(); + if flags.shooting != trigger.is_active() { + flags.shooting = trigger.is_active(); + } + + // `apply_controls` sets the jump flag when the player actually jumps. + // Unset the flag on hitting the ground + if grounding.is_grounded() { + flags.jumping = false; } } @@ -55,7 +68,7 @@ fn rotate_view( return; } - for mut tr in &mut player { + for mut tr in player.iter_mut() { tr.rotate_y(controls.look_dir.x * -0.001); } } @@ -68,15 +81,19 @@ fn apply_controls( &mut KinematicVelocity, &ControllerSettings, &MovementSpeedFactor, + &HasCharacterAnimations, )>, rig_transform_q: Option>>, + mut anim_flags: Query<&mut AnimationFlags>, ) { - let Ok((mut move_input, mut grounding, mut velocity, settings, move_factor)) = + let Ok((mut move_input, mut grounding, mut velocity, settings, move_factor, has_anims)) = character.single_mut() else { return; }; + let mut flags = anim_flags.get_mut(*has_anims.collection()).unwrap(); + let mut direction = -controls.move_dir.extend(0.0).xzy(); if let Some(ref rig_transform) = rig_transform_q { @@ -92,6 +109,8 @@ fn apply_controls( move_input.set(direction * move_factor.0); if controls.jump && grounding.is_grounded() { + flags.jumping = true; + flags.just_jumped = true; happy_feet::movement::jump(settings.jump_force, &mut velocity, &mut grounding, Dir3::Y) } } diff --git a/src/control/controls.rs b/src/control/controls.rs index bfa5b20..81cd60b 100644 --- a/src/control/controls.rs +++ b/src/control/controls.rs @@ -1,8 +1,9 @@ -use crate::control::ControllerSet; +use super::{ControlState, Controls}; use crate::{ GameState, abilities::{TriggerCashHeal, TriggerState}, backpack::BackpackAction, + control::ControllerSet, heads::SelectActiveHead, }; use bevy::{ @@ -14,8 +15,6 @@ use bevy::{ prelude::*, }; -use super::{ControlState, Controls}; - pub fn plugin(app: &mut App) { app.init_resource::(); diff --git a/src/control/mod.rs b/src/control/mod.rs index 1639b64..1611ee2 100644 --- a/src/control/mod.rs +++ b/src/control/mod.rs @@ -1,10 +1,9 @@ -use bevy::prelude::*; - use crate::{ GameState, head::ActiveHead, heads_database::{HeadControls, HeadsDatabase}, }; +use bevy::prelude::*; pub mod controller_common; pub mod controller_flying; diff --git a/src/heads/heads_ui.rs b/src/heads/heads_ui.rs index 065d1c4..64cb031 100644 --- a/src/heads/heads_ui.rs +++ b/src/heads/heads_ui.rs @@ -1,10 +1,9 @@ +use super::{ActiveHeads, HEAD_SLOTS, HeadsImages}; use crate::{GameState, backpack::UiHeadState, loading_assets::UIAssets, player::Player}; use bevy::{ecs::spawn::SpawnIter, prelude::*}; use bevy_ui_gradients::{AngularColorStop, BackgroundGradient, ConicGradient, Gradient, Position}; use std::f32::consts::PI; -use super::{ActiveHeads, HEAD_SLOTS, HeadsImages}; - #[derive(Component, Reflect, Default)] #[reflect(Component)] struct HeadSelector(pub usize); diff --git a/src/heads/mod.rs b/src/heads/mod.rs index 49e65ac..5b3c0b4 100644 --- a/src/heads/mod.rs +++ b/src/heads/mod.rs @@ -202,7 +202,7 @@ fn setup(mut commands: Commands, asset_server: Res, heads: Res) { for (mut active_heads, hp) in query.iter_mut() { - if active_heads.hp() != *hp { + if active_heads.hp().get() != hp.get() { active_heads.set_hitpoint(hp); } } diff --git a/src/hitpoints.rs b/src/hitpoints.rs index b4dba50..0b3ec60 100644 --- a/src/hitpoints.rs +++ b/src/hitpoints.rs @@ -1,7 +1,11 @@ +use crate::{ + GameState, + animation::AnimationFlags, + character::{CharacterAnimations, HasCharacterAnimations}, + sounds::PlaySound, +}; use bevy::prelude::*; -use crate::sounds::PlaySound; - #[derive(Event, Reflect)] pub struct Kill; @@ -10,15 +14,20 @@ pub struct Hit { pub damage: u32, } -#[derive(Component, Reflect, Debug, PartialEq, Eq, Clone, Copy)] +#[derive(Component, Reflect, Debug, Clone, Copy)] pub struct Hitpoints { max: u32, current: u32, + last_hit_timestamp: f32, } impl Hitpoints { pub fn new(v: u32) -> Self { - Self { max: v, current: v } + Self { + max: v, + current: v, + last_hit_timestamp: f32::NEG_INFINITY, + } } pub fn with_health(mut self, v: u32) -> Self { @@ -45,10 +54,17 @@ impl Hitpoints { pub fn max(&self) -> bool { self.current == self.max } + + pub fn time_since_hit(&self, time: &Time) -> f32 { + time.elapsed_secs() - self.last_hit_timestamp + } } pub fn plugin(app: &mut App) { - app.add_systems(Update, on_hp_added); + app.add_systems(Update, on_hp_added).add_systems( + PreUpdate, + reset_hit_animation_flag.run_if(in_state(GameState::Playing)), + ); } fn on_hp_added(mut commands: Commands, query: Query>) { @@ -57,18 +73,52 @@ fn on_hp_added(mut commands: Commands, query: Query>) { } } -fn on_hit(trigger: Trigger, mut commands: Commands, mut query: Query<&mut Hitpoints>) { +fn on_hit( + trigger: Trigger, + mut commands: Commands, + mut query: Query<(&mut Hitpoints, Option<&HasCharacterAnimations>)>, + mut anim_flags: Query<&mut AnimationFlags>, + time: Res