From 4d0ae1cf8dfa71fdebd6bec6619b86ea22927b05 Mon Sep 17 00:00:00 2001 From: Jonathan Cornaz Date: Sun, 5 Jun 2022 21:10:56 +0200 Subject: [PATCH] feat(unstable): animation asset loader (#61) --- Cargo.toml | 8 +++- assets/coin.animation.yml | 12 ++++++ examples/using_animation_file.rs | 41 ++++++++++++++++++++ src/animation.rs | 29 ++------------ src/animation/load.rs | 24 ++++++++++++ src/animation/parse.rs | 66 +++++++++++++++++++++++++++++--- src/lib.rs | 3 ++ 7 files changed, 151 insertions(+), 32 deletions(-) create mode 100644 assets/coin.animation.yml create mode 100644 examples/using_animation_file.rs create mode 100644 src/animation/load.rs diff --git a/Cargo.toml b/Cargo.toml index 71c29d5..f01bb53 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ categories = ["game-development"] [features] default = [] -unstable-load-from-file = ["serde", "serde_yaml"] +unstable-load-from-file = ["serde", "serde_yaml", "anyhow", "bevy_utils"] [dependencies] bevy_core = { version = "0.7.0", default-features = false } @@ -21,8 +21,10 @@ bevy_app = { version = "0.7.0", default-features = false } bevy_reflect = { version = "0.7.0", default-features = false } bevy_sprite = { version = "0.7.0", default-features = false } bevy_asset = { version = "0.7.0", default-features = false } +bevy_utils = { version = "0.7.0", default-features = false, optional = true } serde = { version = "1.0", default-features = false, features = ["derive"], optional = true } serde_yaml = { version = "0.8.24", default-features = false, optional = true } +anyhow = { version = "1.0", default-features = false, optional = true } [dev-dependencies] bevy = { version = "0.7.0", default-features = false, features = ["render", "x11", "png"] } @@ -33,5 +35,9 @@ criterion = { version = "0.3.5", default-features = false } name = "play_component" harness = false +[[example]] +name = "using_animation_file" +required-features = ["unstable-load-from-file"] + [profile.dev] debug = 0 diff --git a/assets/coin.animation.yml b/assets/coin.animation.yml new file mode 100644 index 0000000..55d933f --- /dev/null +++ b/assets/coin.animation.yml @@ -0,0 +1,12 @@ +mode: repeat +frames: + - index: 0 + duration: 100 + - index: 1 + duration: 100 + - index: 2 + duration: 100 + - index: 3 + duration: 100 + - index: 4 + duration: 100 diff --git a/examples/using_animation_file.rs b/examples/using_animation_file.rs new file mode 100644 index 0000000..403c390 --- /dev/null +++ b/examples/using_animation_file.rs @@ -0,0 +1,41 @@ +use bevy::prelude::*; + +use benimator::*; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_plugin(AnimationPlugin::default()) // <-- Add the plugin + .add_startup_system(spawn) + .run(); +} + +fn spawn( + mut commands: Commands, + asset_server: Res, + mut textures: ResMut>, +) { + // Don't forget the camera ;-) + commands.spawn_bundle(OrthographicCameraBundle::new_2d()); + + // Create an animation + // Here we use an index-range (from 0 to 4) where each frame has the same duration + let animation_handle: Handle = asset_server.load("coin.animation.yml"); + + commands + // Spawn a bevy sprite-sheet + .spawn_bundle(SpriteSheetBundle { + texture_atlas: textures.add(TextureAtlas::from_grid( + asset_server.load("coin.png"), + Vec2::new(16.0, 16.0), + 5, + 1, + )), + transform: Transform::from_scale(Vec3::splat(10.0)), + ..Default::default() + }) + // Insert the asset handle of the animation + .insert(animation_handle) + // Start the animation immediately. Remove this component in order to pause the animation. + .insert(Play); +} diff --git a/src/animation.rs b/src/animation.rs index 0c321e2..3d218c2 100644 --- a/src/animation.rs +++ b/src/animation.rs @@ -1,6 +1,9 @@ #[cfg(feature = "unstable-load-from-file")] mod parse; +#[cfg(feature = "unstable-load-from-file")] +pub(crate) mod load; + use std::{ops::RangeInclusive, time::Duration}; use bevy_reflect::TypeUuid; @@ -138,32 +141,6 @@ impl SpriteSheetAnimation { pub(crate) fn has_frames(&self) -> bool { !self.frames.is_empty() } - - /// Parse content of a yaml file representing the animation - /// - /// # Yaml schema - /// - /// ```yaml - /// # The mode can be one of: 'once', 'repeat', 'ping-pong' - /// # or 'repeatFrom(n)' (where 'n' is the frame-index to repeat from) - /// # The default is 'repeat' - /// mode: ping-pong - /// frames: - /// - index: 0 # index in the sprite sheet for that frame - /// duration: 100 # duration of the frame in milliseconds - /// - index: 1 - /// duration: 100 - /// - index: 2 - /// duration: 120 - /// ``` - /// - /// # Errors - /// - /// Returns an error if the content is not a valid yaml representation of an animation - #[cfg(feature = "unstable-load-from-file")] - pub fn from_yaml(yaml: &str) -> Result { - serde_yaml::from_str(yaml).map_err(AnimationParseError) - } } #[derive(Debug, Copy, Clone, Eq, PartialEq)] diff --git a/src/animation/load.rs b/src/animation/load.rs new file mode 100644 index 0000000..b9ef60e --- /dev/null +++ b/src/animation/load.rs @@ -0,0 +1,24 @@ +use super::SpriteSheetAnimation; +use bevy_asset::{AssetLoader, LoadContext, LoadedAsset}; +use bevy_utils::BoxedFuture; + +#[derive(Debug, Default)] +pub(crate) struct SpriteSheetAnimationLoader; + +impl AssetLoader for SpriteSheetAnimationLoader { + fn load<'a>( + &'a self, + bytes: &'a [u8], + load_context: &'a mut LoadContext<'_>, + ) -> BoxedFuture<'a, Result<(), anyhow::Error>> { + Box::pin(async move { + let custom_asset = SpriteSheetAnimation::from_yaml_bytes(bytes)?; + load_context.set_default_asset(LoadedAsset::new(custom_asset)); + Ok(()) + }) + } + + fn extensions(&self) -> &[&str] { + &["animation.yml", "animation.yaml"] + } +} diff --git a/src/animation/parse.rs b/src/animation/parse.rs index 82f1cd2..d75bc55 100644 --- a/src/animation/parse.rs +++ b/src/animation/parse.rs @@ -6,8 +6,64 @@ use std::{ use serde::{de, Deserialize, Deserializer}; +use crate::SpriteSheetAnimation; + use super::Mode; +impl SpriteSheetAnimation { + /// Parse content of a yaml string representing the animation + /// + /// # Yaml schema + /// + /// ```yaml + /// # The mode can be one of: 'once', 'repeat', 'ping-pong' + /// # or 'repeatFrom(n)' (where 'n' is the frame-index to repeat from) + /// # The default is 'repeat' + /// mode: ping-pong + /// frames: + /// - index: 0 # index in the sprite sheet for that frame + /// duration: 100 # duration of the frame in milliseconds + /// - index: 1 + /// duration: 100 + /// - index: 2 + /// duration: 120 + /// ``` + /// + /// # Errors + /// + /// Returns an error if the content is not a valid yaml representation of an animation + #[cfg(feature = "unstable-load-from-file")] + pub fn from_yaml_str(yaml: &str) -> Result { + serde_yaml::from_str(yaml).map_err(AnimationParseError) + } + + /// Parse content of a yaml bytes representing the animation + /// + /// # Yaml schema + /// + /// ```yaml + /// # The mode can be one of: 'once', 'repeat', 'ping-pong' + /// # or 'repeatFrom(n)' (where 'n' is the frame-index to repeat from) + /// # The default is 'repeat' + /// mode: ping-pong + /// frames: + /// - index: 0 # index in the sprite sheet for that frame + /// duration: 100 # duration of the frame in milliseconds + /// - index: 1 + /// duration: 100 + /// - index: 2 + /// duration: 120 + /// ``` + /// + /// # Errors + /// + /// Returns an error if the content is not a valid yaml representation of an animation + #[cfg(feature = "unstable-load-from-file")] + pub fn from_yaml_bytes(yaml: &[u8]) -> Result { + serde_yaml::from_slice(yaml).map_err(AnimationParseError) + } +} + #[derive(Debug)] #[non_exhaustive] pub struct AnimationParseError(pub(super) serde_yaml::Error); @@ -107,7 +163,7 @@ mod tests { duration: 120"; // when - let animation = SpriteSheetAnimation::from_yaml(content).unwrap(); + let animation = SpriteSheetAnimation::from_yaml_str(content).unwrap(); // then assert_eq!(animation.mode, Mode::PingPong); @@ -130,7 +186,7 @@ mod tests { duration: 100"; // when - let animation = SpriteSheetAnimation::from_yaml(content).unwrap(); + let animation = SpriteSheetAnimation::from_yaml_str(content).unwrap(); // then assert_eq!(animation.mode, Mode::RepeatFrom(0)); @@ -146,7 +202,7 @@ mod tests { duration: 100"; // when - let animation = SpriteSheetAnimation::from_yaml(content).unwrap(); + let animation = SpriteSheetAnimation::from_yaml_str(content).unwrap(); // then assert_eq!(animation.mode, Mode::RepeatFrom(0)); @@ -162,7 +218,7 @@ mod tests { duration: 100"; // when - let animation = SpriteSheetAnimation::from_yaml(content).unwrap(); + let animation = SpriteSheetAnimation::from_yaml_str(content).unwrap(); // then assert_eq!(animation.mode, Mode::Once); @@ -180,7 +236,7 @@ mod tests { duration: 100"; // when - let animation = SpriteSheetAnimation::from_yaml(content).unwrap(); + let animation = SpriteSheetAnimation::from_yaml_str(content).unwrap(); // then assert_eq!(animation.mode, Mode::RepeatFrom(1)); diff --git a/src/lib.rs b/src/lib.rs index f6aab1d..a01f0b1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -159,5 +159,8 @@ impl Plugin for AnimationPlugin { app.add_asset::() .add_system_set_to_stage(CoreStage::PreUpdate, state::maintenance_systems()) .add_system_set_to_stage(CoreStage::Update, state::post_update_systems()); + + #[cfg(feature = "unstable-load-from-file")] + app.init_asset_loader::(); } }