generated from 2ndbeam/bevy-template
feat: Working AnimatedSprite
This commit is contained in:
parent
4646a27978
commit
254704135c
3 changed files with 119 additions and 42 deletions
|
|
@ -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) => {
|
||||
self.current = Some(data.clone());
|
||||
self.timer.set_duration(self.bpm.mul_f32(data.duration));
|
||||
self.timer.set_mode(mode);
|
||||
/// 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 = 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(¤t.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
35
src/lib.rs
35
src/lib.rs
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue