feat: Working AnimatedSprite

This commit is contained in:
Alexey 2026-04-20 14:29:44 +03:00
commit 254704135c
3 changed files with 119 additions and 42 deletions

View file

@ -3,74 +3,114 @@
use std::{collections::HashMap, time::Duration};
use bevy::prelude::*;
use crate::timer::duration_from_bpm;
use bevy_ecs::system::EntityEntryCommands;
/// Data for [AnimatedSprite]
#[derive(Clone, PartialEq, Debug, Reflect)]
#[reflect(Clone, PartialEq, Debug)]
#[derive(Clone, PartialEq, Debug, Default, Reflect)]
#[reflect(Clone, PartialEq, Debug, Default)]
pub struct AnimationData {
/// Handle to sprite image
pub image: Handle<Image>,
/// Animation duration in beats
pub duration: f32,
/// Sprite frames
pub frames: Handle<TextureAtlasLayout>,
/// [AnimatedSprite] duration multiplier, usually 1.
pub duration_multiplier: f32,
}
impl AnimationData {
/// Construct new [AnimationData]
pub fn new(image: Handle<Image>, duration: f32) -> Self {
Self { image, duration }
/// Construct new [AnimationData] from given [Handle<TextureAtlasLayout>]
pub fn new(image: Handle<Image>, frames: Handle<TextureAtlasLayout>, duration_multiplier: f32) -> Self {
Self { image, frames, duration_multiplier }
}
}
/// Animated sprite with states
#[derive(Clone, PartialEq, Debug, Reflect)]
#[reflect(Clone, PartialEq, Debug, Default)]
#[derive(Component, Clone, PartialEq, Debug, Reflect)]
#[reflect(Component, Clone, PartialEq, Debug)]
#[require(Sprite)]
pub struct AnimatedSprite {
data: HashMap<String, AnimationData>,
current: Option<AnimationData>,
timer: Timer,
bpm: Duration,
}
impl Default for AnimatedSprite {
fn default() -> Self {
Self {
data: HashMap::new(),
current: None,
timer: Timer::new(Duration::default(), TimerMode::Repeating),
bpm: Duration::default(),
}
}
duration: Duration,
}
impl AnimatedSprite {
/// Constructs new AnimatedSprite
pub fn new(data: HashMap<String, AnimationData>, duration: Duration) -> Self {
let timer = Timer::new(duration, TimerMode::Once);
Self { data, current: None, timer, duration }
}
/// Add new animation to sprite
pub fn add(&mut self, data: AnimationData, label: String) {
self.data.insert(label, data);
}
/// Sets internal BPM state
pub fn set_bpm(&mut self, bpm: f32) {
self.bpm = duration_from_bpm(bpm);
/// Sets internal duration
pub fn set_duration(&mut self, duration: Duration) {
self.duration = duration;
}
/// Finds animation by its label and returns true if it started playing
pub fn play(&mut self, label: &str, mode: TimerMode) -> bool {
match self.data.get(label) {
Some(data) => {
/// Finds animation by its label and returns it if started playing
pub fn play(
&mut self,
label: &str,
mode: TimerMode,
mut sprite: EntityEntryCommands<Sprite>,
) -> Option<&AnimationData> {
self.data.get(label).map(|data| {
self.current = Some(data.clone());
self.timer.set_duration(self.bpm.mul_f32(data.duration));
self.timer.set_mode(mode);
self.timer = Timer::new(self.duration.mul_f32(data.duration_multiplier), mode);
true
},
None => false,
}
let sprite_image = data.image.clone();
let layout = data.frames.clone();
sprite.and_modify(move |mut sprite| {
sprite.image = sprite_image;
sprite.texture_atlas = Some(TextureAtlas { layout, index: 0 });
});
data
})
}
/// Updates animation and returns true if animation should stop
pub fn tick(&mut self, dt: Duration) -> bool {
self.timer.tick(dt).just_finished()
/// Updates animation timer
pub fn tick(&mut self, dt: Duration) {
self.timer.tick(dt);
}
/// Returns inner [Timer::fraction]
pub fn fraction(&self) -> f32 {
self.timer.fraction()
}
}
/// Updates [AnimatedSprite] entities
pub fn update_animated_sprites(
time: Res<Time>,
atlases: Res<Assets<TextureAtlasLayout>>,
sprites: Query<(&mut AnimatedSprite, &mut Sprite)>
) {
let delta = time.delta();
for (mut animation, mut sprite) in sprites {
let Some(current) = &animation.current else {
continue;
};
let Some(atlas) = atlases.get(&current.frames) else {
continue;
};
animation.tick(delta);
let max_frames = atlas.len() as f32;
let current_frame = (max_frames * animation.fraction()) as usize;
if let Some(atlas) = &mut sprite.texture_atlas {
atlas.index = current_frame;
}
}
}

View file

@ -13,10 +13,12 @@ pub mod player;
pub mod plugin;
pub mod timer;
use std::{collections::HashMap, time::Duration};
use bevy::prelude::*;
use bevy_rapier2d::prelude::*;
use crate::{player::Player, timer::BpmTimer};
use crate::{anim::sprite::{AnimatedSprite, AnimationData}, player::Player, timer::BpmTimer};
const PIXELS_PER_METER: f32 = 16.;
@ -40,3 +42,34 @@ pub fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn(Player::bundle(&asset_server, Vec2::ZERO));
commands.spawn(BpmTimer::new(120.));
}
/// Test function to setup [AnimatedSprite]
pub fn setup_animated_sprite(
mut commands: Commands,
asset_server: Res<AssetServer>,
mut layouts: ResMut<Assets<TextureAtlasLayout>>
) {
let image = asset_server.load("sprites/animated_placeholder.png");
let atlas = TextureAtlasLayout::from_grid(uvec2(16, 16), 5, 1, None, None);
let layout = layouts.add(atlas);
let mut data = HashMap::new();
data.insert("default".into(), AnimationData::new(image.clone(), layout.clone(), 5.));
let id = commands.spawn((
AnimatedSprite::new(data, Duration::from_millis(500)),
Sprite::from_atlas_image(image, TextureAtlas { layout: layout, index: 0 }),
)).id();
commands.run_system_cached_with(|
In(entity_id): In<Entity>,
mut commands: Commands,
mut query: Query<&mut AnimatedSprite>,
| {
let mut animation = query.get_mut(entity_id).unwrap();
animation.play("default", TimerMode::Repeating, commands.entity(entity_id).entry::<Sprite>());
}, id);
}

View file

@ -23,8 +23,12 @@ impl Plugin for GamePlugin {
StateMachinePlugin::default(),
))
.add_systems(Startup, setup)
.add_systems(Startup, (
setup,
setup_animated_sprite,
))
.add_systems(Update, (
anim::sprite::update_animated_sprites,
combat::attack::update_attack_areas,
player::systems::handle_input,
timer::update_bpm_timers,