diff --git a/crates/lyra-ecs/src/command.rs b/crates/lyra-ecs/src/command.rs index 0a3b320..ac30f75 100644 --- a/crates/lyra-ecs/src/command.rs +++ b/crates/lyra-ecs/src/command.rs @@ -166,6 +166,15 @@ impl<'a, 'b> Commands<'a, 'b> { e } + /// Insert or update existing components into an Entity. + /// + /// See [`World::insert`]. + pub fn insert(&mut self, entity: Entity, bundle: B) { + self.add(move | world: &mut World| { + world.insert(entity, bundle); + }); + } + /// Execute all commands in the queue, in order of insertion pub fn execute(&mut self, world: &mut World) { self.queue.execute(Some(world)); diff --git a/crates/lyra-game/src/sprite/animation_sheet.rs b/crates/lyra-game/src/sprite/animation_sheet.rs new file mode 100644 index 0000000..4982cf0 --- /dev/null +++ b/crates/lyra-game/src/sprite/animation_sheet.rs @@ -0,0 +1,238 @@ +use std::collections::HashMap; + +use lyra_ecs::query::{Entities, Res, View}; +use lyra_ecs::{Commands, Component}; +use lyra_math::URect; +use lyra_reflect::Reflect; +use lyra_resource::{ResHandle, ResourceStorage}; + +use tracing::error; + +use crate::DeltaTime; + +use super::{AtlasSprite, TextureAtlas}; + +/// A struct describing an animation of a Sprite. +/// +/// This is a single animation for a [`TextureAtlas`]. This is used alongside [`AtlasAnimations`] +/// to use animations from an atlas. +#[derive(Clone, Component, Reflect)] +pub struct SpriteAnimation { + /// The name of the animation. + pub name: String, + /// The frames of the animation. + pub frames: Vec, + /// The length of time a frame is displayed. + pub frame_time: f32, +} + +impl SpriteAnimation { + /// Create an animation from a texture atlas. + /// + /// Parameters: + /// * `name` - The name of the animation. Used to identify the animation in [`AtlasAnimations`]. + /// * `frame_time` - The time per frame of the animation. + /// * `atlas` - The texture atlas that this animation is from, used to acquire `self.frames`. + /// * `sprites` are the rect indexes in the atlas for this animation. + pub fn from_atlas(name: &str, frame_time: f32, atlas: &TextureAtlas, sprites: I) -> Self + where + I: Iterator, + { + let mut frames = vec![];//Vec::with_capacity(sprites.len()); + + for i in sprites { + let r = atlas.index_rect(i); + frames.push(r); + } + + Self { + name: name.into(), + frames, + frame_time, + } + } +} + +/// A helper trait that makes it easier to create the animations for an [`AtlasAnimations`] component. +/// +/// See [`AtlasAnimations::new`]. +pub trait IntoSpriteAnimation { + fn into_animation(&self, atlas: &TextureAtlas) -> SpriteAnimation; +} + +impl IntoSpriteAnimation for SpriteAnimation { + fn into_animation(&self, _: &TextureAtlas) -> SpriteAnimation { + self.clone() + } +} + +impl<'a, I: Iterator + Clone> IntoSpriteAnimation for (&'a str, f32, I) { + fn into_animation(&self, atlas: &TextureAtlas) -> SpriteAnimation { + SpriteAnimation::from_atlas(self.0, self.1, atlas, self.2.clone()) + } +} + +#[derive(Clone, Component, Reflect)] +pub struct AtlasAnimations { + /// The texture atlas to get the animations from. + pub atlas: ResHandle, + /// Animations in the atlas. + pub animations: HashMap, +} + +impl AtlasAnimations { + pub fn from_animations(atlas: ResHandle, animations: Vec) -> Self { + let animations = animations.into_iter() + .map(|a| (a.name.clone(), a)) + .collect::>(); + + Self { + atlas, + animations, + } + } + + /// Helper for creating [`AtlasAnimations`]. + /// + /// If you already have the [`SpriteAnimation`]s, you can just use + /// [`AtlasAnimations::from_animations`] instead of this helper function. + /// + /// Example: + /// ``` + /// let animations = AtlasAnimations::new(atlas, &[ + /// // This slice accepts anything that implements `IntoSpriteAnimation`: + /// // * tuple of (name: &str, frame_time: f32, frame_indexes: Iterator) + /// // * `SpriteAnimation` (will be cloned) + /// + /// // The animation is named "soldier_run", with a frame time of 0.1, and the frames + /// // 9 to 16 (inclusive) from the atlas. + /// ("soldier_run", 0.1, 9..=16), + /// ]); + /// ``` + pub fn new(atlas: ResHandle, animations: &[A]) -> Self + where + A: IntoSpriteAnimation + { + let animations = { + let atlas = atlas.data_ref().unwrap(); + + animations.into_iter() + .map(|a| { + let a = a.into_animation(&atlas); + (a.name.clone(), a) + }) + //.map(|(name, ft, fi)| (name.to_string(), SpriteAnimation::from_atlas(name, *ft, &atlas, fi.clone()))) + .collect::>() + }; + + Self { + atlas, + animations, + } + } + + /// Get the [`ActiveAtlasAnimation`] for an animation with `name`. + /// + /// > NOTE: this asserts that the animation exists in self in debug builds (uses `debug_assert`). + pub fn get_active(&self, name: &str) -> ActiveAtlasAnimation { + debug_assert!(self.animations.contains_key(name), "The animation with name '{name}' does not exist!"); + + ActiveAtlasAnimation::new(name) + } +} + +/// The active sprite animation from an [`AtlasAnimations`]. +#[derive(Clone, Component, Reflect)] +pub struct ActiveAtlasAnimation { + /// The name of the active [`SpriteAnimation`]. + pub name: String, + /// The current frame index in the active [`SpriteAnimation`]. + /// + /// This is not the index of the rect in the atlas. + pub index: u32, + pub paused: bool, + /// The time since last animation frame change. + /// + /// This is used to detect if enough time has passed for the frame. + timer: f32, +} + +impl ActiveAtlasAnimation { + ///Create an [`ActiveAtlasAnimation`]. + /// + /// The animation will not be paused. + pub fn new(name: &str) -> Self { + Self { + name: name.into(), + index: 0, + paused: false, + timer: 0.0, + } + } + + /// Create an [`ActiveAtlasAnimation`] that starts at a specific point in the animation. + /// + /// The animation will not be paused. + pub fn new_at(name: &str, index: u32) -> Self { + Self { + name: name.into(), + index, + paused: false, + timer: 0.0, + } + } +} + +pub fn system_sprite_atlas_animation(mut commands: Commands, dt: Res, view: View<(Entities, Option<&mut AtlasSprite>, &AtlasAnimations, &mut ActiveAtlasAnimation)>) -> anyhow::Result<()> { + let dt = **dt; + + for (en, mut sprite, animations, mut active) in view.iter() { + if active.paused { + // Don't touch paused animations + continue; + } + + if let Some(anim) = animations.animations.get(&active.name) { + if animations.atlas.is_loaded() { + active.timer += dt; + + // Initialize this entity by giving it the first sprite animation frame. + if sprite.is_none() { + // Get the first sprite in the animation. + let rect = anim.frames[active.index as usize]; + let sprite = AtlasSprite { + atlas: animations.atlas.clone(), + sprite: rect, + }; + + commands.insert(en, sprite); + continue; + } + + if active.timer >= anim.frame_time { + active.timer = 0.0; + active.index += 1; + + // wrap the animation around + if active.index as usize >= anim.frames.len() { + active.index = 0; + } + + // Get the sprite for the animation frame + let rect = anim.frames[active.index as usize]; + let new_sprite = AtlasSprite { + atlas: animations.atlas.clone(), + sprite: rect, + }; + + let sprite = sprite.as_mut().unwrap(); + **sprite = new_sprite; + } + } + } else { + error!("Unknown active animation: '{}'", active.name); + } + } + + Ok(()) +} \ No newline at end of file diff --git a/crates/lyra-game/src/sprite/mod.rs b/crates/lyra-game/src/sprite/mod.rs index c754af7..670413c 100644 --- a/crates/lyra-game/src/sprite/mod.rs +++ b/crates/lyra-game/src/sprite/mod.rs @@ -6,6 +6,9 @@ use lyra_math::{Vec3, Vec2}; mod texture_atlas; pub use texture_atlas::*; +mod animation_sheet; +pub use animation_sheet::*; + /// How the sprite is positioned and rotated relative to its [`Transform`]. /// /// Default pivot is `Pivot::Center`, this makes it easier to rotate the sprites. diff --git a/examples/2d/src/main.rs b/examples/2d/src/main.rs index c259a32..a4e914d 100644 --- a/examples/2d/src/main.rs +++ b/examples/2d/src/main.rs @@ -4,7 +4,7 @@ use lyra_engine::{ InputActionPlugin, KeyCode, LayoutId, }, math::{self, Rect, Transform, URect, UVec2, Vec2, Vec3}, render::light::directional::DirectionalLight, scene::{ system_update_world_transforms, Camera2dBundle, CameraProjection, OrthographicProjection, ScaleMode, TopDown2dCamera, TopDown2dCameraPlugin, WorldTransform, ACTLBL_LOOK_LEFT_RIGHT, ACTLBL_LOOK_ROLL, ACTLBL_LOOK_UP_DOWN, ACTLBL_MOVE_FORWARD_BACKWARD, ACTLBL_MOVE_LEFT_RIGHT, ACTLBL_MOVE_UP_DOWN - }, sprite::{self, AtlasSprite, Pivot, Sprite, TextureAtlas}, DeltaTime + }, sprite::{self, AtlasAnimations, AtlasSprite, Pivot, Sprite, SpriteAnimation, TextureAtlas}, DeltaTime }; use tracing::debug; @@ -94,8 +94,9 @@ async fn main() { } fn setup_scene_plugin(app: &mut App) { - app.add_resource(Timer(0.0)); - app.with_system("sprite_change", sprite_change, &[]); + //app.add_resource(Timer(0.0)); + //app.with_system("sprite_change", sprite_change, &[]); + app.with_system("sprite_atlas_animation", sprite::system_sprite_atlas_animation, &[]); let world = &mut app.world; let resman = world.get_resource_mut::().unwrap(); @@ -135,17 +136,17 @@ fn setup_scene_plugin(app: &mut App) { sprite_color: Vec3::ONE, pivot: Pivot::default(), }); - let sprite = AtlasSprite::from_atlas_index(atlas, 9); + + let animations = AtlasAnimations::new(atlas, &[ + ("soldier_run", 0.1, 9..=16), + ]); + let run_anim = animations.get_active("soldier_run"); drop(resman); world.spawn(( - /* Sprite { - texture: sprite, - color: Vec3::ONE, - pivot: sprite::Pivot::Center, - }, */ - sprite, + animations, + run_anim, WorldTransform::default(), Transform::from_xyz(0.0, 0.0, -10.0), )); @@ -180,36 +181,4 @@ fn setup_scene_plugin(app: &mut App) { ..Default::default() } )); -} - -#[derive(Clone, Copy, Debug)] -struct Timer(f32); - -fn sprite_change(mut timer: ResMut, dt: Res, view: View<&mut AtlasSprite>) -> anyhow::Result<()> { - timer.0 += **dt; - - const TIME: f32 = 0.1; - if timer.0 >= TIME { - //println!("{t} seconds timer triggered, moving sprite"); - timer.0 = 0.0; - - for mut a in view.iter() { - //println!("a.sprite: {:?}", a.sprite); - - if a.sprite.max.x >= 800 { - a.sprite = URect { - min: UVec2::new(0, 100), - max: UVec2::new(100, 200), - }; - //println!("restart!"); - } else { - a.sprite += URect { - min: UVec2::new(100, 0), - max: UVec2::new(100, 0), - }; - } - } - } - - Ok(()) } \ No newline at end of file