2d: create an animation sheet component
This commit is contained in:
parent
4018fdaa80
commit
558f027b19
|
@ -166,6 +166,15 @@ impl<'a, 'b> Commands<'a, 'b> {
|
||||||
e
|
e
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Insert or update existing components into an Entity.
|
||||||
|
///
|
||||||
|
/// See [`World::insert`].
|
||||||
|
pub fn insert<B: Bundle + 'static>(&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
|
/// Execute all commands in the queue, in order of insertion
|
||||||
pub fn execute(&mut self, world: &mut World) {
|
pub fn execute(&mut self, world: &mut World) {
|
||||||
self.queue.execute(Some(world));
|
self.queue.execute(Some(world));
|
||||||
|
|
|
@ -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<URect>,
|
||||||
|
/// 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<I>(name: &str, frame_time: f32, atlas: &TextureAtlas, sprites: I) -> Self
|
||||||
|
where
|
||||||
|
I: Iterator<Item = u32>,
|
||||||
|
{
|
||||||
|
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<Item = u32> + 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<TextureAtlas>,
|
||||||
|
/// Animations in the atlas.
|
||||||
|
pub animations: HashMap<String, SpriteAnimation>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AtlasAnimations {
|
||||||
|
pub fn from_animations(atlas: ResHandle<TextureAtlas>, animations: Vec<SpriteAnimation>) -> Self {
|
||||||
|
let animations = animations.into_iter()
|
||||||
|
.map(|a| (a.name.clone(), a))
|
||||||
|
.collect::<HashMap<_, _>>();
|
||||||
|
|
||||||
|
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<Item = u32>)
|
||||||
|
/// // * `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<A>(atlas: ResHandle<TextureAtlas>, 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::<HashMap<_, _>>()
|
||||||
|
};
|
||||||
|
|
||||||
|
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<DeltaTime>, 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(())
|
||||||
|
}
|
|
@ -6,6 +6,9 @@ use lyra_math::{Vec3, Vec2};
|
||||||
mod texture_atlas;
|
mod texture_atlas;
|
||||||
pub use texture_atlas::*;
|
pub use texture_atlas::*;
|
||||||
|
|
||||||
|
mod animation_sheet;
|
||||||
|
pub use animation_sheet::*;
|
||||||
|
|
||||||
/// How the sprite is positioned and rotated relative to its [`Transform`].
|
/// How the sprite is positioned and rotated relative to its [`Transform`].
|
||||||
///
|
///
|
||||||
/// Default pivot is `Pivot::Center`, this makes it easier to rotate the sprites.
|
/// Default pivot is `Pivot::Center`, this makes it easier to rotate the sprites.
|
||||||
|
|
|
@ -4,7 +4,7 @@ use lyra_engine::{
|
||||||
InputActionPlugin, KeyCode, LayoutId,
|
InputActionPlugin, KeyCode, LayoutId,
|
||||||
}, math::{self, Rect, Transform, URect, UVec2, Vec2, Vec3}, render::light::directional::DirectionalLight, scene::{
|
}, 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
|
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;
|
use tracing::debug;
|
||||||
|
|
||||||
|
@ -94,8 +94,9 @@ async fn main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setup_scene_plugin(app: &mut App) {
|
fn setup_scene_plugin(app: &mut App) {
|
||||||
app.add_resource(Timer(0.0));
|
//app.add_resource(Timer(0.0));
|
||||||
app.with_system("sprite_change", sprite_change, &[]);
|
//app.with_system("sprite_change", sprite_change, &[]);
|
||||||
|
app.with_system("sprite_atlas_animation", sprite::system_sprite_atlas_animation, &[]);
|
||||||
|
|
||||||
let world = &mut app.world;
|
let world = &mut app.world;
|
||||||
let resman = world.get_resource_mut::<ResourceManager>().unwrap();
|
let resman = world.get_resource_mut::<ResourceManager>().unwrap();
|
||||||
|
@ -135,17 +136,17 @@ fn setup_scene_plugin(app: &mut App) {
|
||||||
sprite_color: Vec3::ONE,
|
sprite_color: Vec3::ONE,
|
||||||
pivot: Pivot::default(),
|
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);
|
drop(resman);
|
||||||
|
|
||||||
world.spawn((
|
world.spawn((
|
||||||
/* Sprite {
|
animations,
|
||||||
texture: sprite,
|
run_anim,
|
||||||
color: Vec3::ONE,
|
|
||||||
pivot: sprite::Pivot::Center,
|
|
||||||
}, */
|
|
||||||
sprite,
|
|
||||||
WorldTransform::default(),
|
WorldTransform::default(),
|
||||||
Transform::from_xyz(0.0, 0.0, -10.0),
|
Transform::from_xyz(0.0, 0.0, -10.0),
|
||||||
));
|
));
|
||||||
|
@ -180,36 +181,4 @@ fn setup_scene_plugin(app: &mut App) {
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
));
|
));
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
|
||||||
struct Timer(f32);
|
|
||||||
|
|
||||||
fn sprite_change(mut timer: ResMut<Timer>, dt: Res<DeltaTime>, 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(())
|
|
||||||
}
|
}
|
Loading…
Reference in New Issue