From 47c68544d2cb9004a73259f08efcc5c7a0bc5fd7 Mon Sep 17 00:00:00 2001 From: Louis Capitanchik <contact@louiscap.co> Date: Sun, 5 Nov 2023 20:32:22 +0000 Subject: [PATCH] Update to Bevy 0.12, remove AnimationQuery, export Query & Filter types --- .gitattributes | 1 + CHANGELOG.md | 18 ++ Cargo.toml | 11 +- README.md | 26 +- assets/character.anim.json | 18 ++ assets/character.png | 3 + examples/basic.rs | 96 +++++++ src/definitions.rs | 86 +++--- src/directionality.rs | 1 + src/loader.rs | 117 +++++--- src/query.rs | 569 +++++++++++++------------------------ src/systems.rs | 102 ++----- 12 files changed, 496 insertions(+), 552 deletions(-) create mode 100644 .gitattributes create mode 100644 assets/character.anim.json create mode 100644 assets/character.png create mode 100644 examples/basic.rs diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..24a8e87 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.png filter=lfs diff=lfs merge=lfs -text diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fab514..1a01ce5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,24 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.7.0] + +### Added + +- Add a series of WorldQuery types for retrieving animation components +- Add a series of WorldQuery types for filtering animation entities + +### Changed + +- Required Bevy version is now 0.12 + +### Removed + +- `AnimationQuery` has been removed; instead, the ergonomics of controlling animations by manipulating components +directly is much more ergonomic +- Support for `bevy_ecs_tilemap` has been dropped +- + ## [0.6.0] ### Changed diff --git a/Cargo.toml b/Cargo.toml index 3042beb..4a38547 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "micro_banimate" -version = "0.6.0" +version = "0.7.0" edition = "2021" license = "Apache-2.0" description = "Easily manage complex Bevy 2D sprite animations" @@ -9,6 +9,11 @@ authors = [ ] repository = "https://lab.lcr.gr/microhacks/micro-banimate" +[[example]] +name = "basic" +path = "examples/basic.rs" +required-features = ["json_loader"] + [features] default = ["json_loader"] json_loader = ["serde", "dep:serde_json"] @@ -21,3 +26,7 @@ serde = { version = "^1.0.145", optional = true } serde_json = { version = "^1.0.85", optional = true } toml = { version = "0.7.4", optional = true } bevy = { version = "^0.12.0", default-features = false, features = ["bevy_asset", "bevy_sprite"] } + +[dev_dependencies] +bevy = "0.12" +log = "0.4" diff --git a/README.md b/README.md index 62fe981..75381e4 100644 --- a/README.md +++ b/README.md @@ -8,31 +8,8 @@ Easily manage complex 2D sprite animations - Load one or more animation sets (see below for defining animation sets in files) - Spawn an entity with a `SpriteSheetBundle` and attach a `SpriteAnimationBundle` component, providing a `Handle` to an animation set -- Watch your sprites doing their thing! -- Optional: Use `AnimationQuery` in one of your systems to control the animations - -```rust -fn main() { - App::new() - .add_plugins(micro_banimate::BanimatePluginGroup) - // Other plugins - .add_startup_system(spawn_animated) - .run(); -} -pub fn spawn_animated(mut commands: Commands, assets: Res<AssetServer>) { - commands - .spawn_bundle(SpriteSheetBundle { - texture_atlas: assets.load("my_sprite_sheet.atlas"), - sprite: TextureAtlasSprite::new(0), - ..Default::default() - }) - .insert_bundle(SpriteAnimationBundle::new( - String::from("idle"), - server.load("my_animatoins.anim.json") - )); -} -``` +> Code examples have been moved to the examples folder - clone the source and see them in action! ## Types of Animation @@ -122,6 +99,7 @@ frame_time = 100 | banimate version | bevy version | tilemap version | |---------------------|--------------|------------------------------------------| +| 0.7.0 | 0.12 | n/a | | 0.6.0-rc.1 | 0.11 | 55c15bfa43c7a9e2adef6b70007e92d699377454 | | 0.5.x | 0.10 | 0.10 | | 0.5.x | 0.10 | 0.10 | diff --git a/assets/character.anim.json b/assets/character.anim.json new file mode 100644 index 0000000..d4c0eb7 --- /dev/null +++ b/assets/character.anim.json @@ -0,0 +1,18 @@ +{ + "idle_right": { + "frames": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + "frame_secs": 0.1 + }, + "idle_left": { + "frames": [10, 11, 12, 13, 14, 15, 16, 17, 18, 19], + "frame_secs": 0.1 + }, + "run_right": { + "frames": [20, 21, 22, 23, 24, 25, 26, 27], + "frame_secs": 0.1 + }, + "run_left": { + "frames": [30, 31, 32, 33, 34, 35, 36, 37], + "frame_secs": 0.1 + } +} \ No newline at end of file diff --git a/assets/character.png b/assets/character.png new file mode 100644 index 0000000..287e050 --- /dev/null +++ b/assets/character.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:97ea038890c7ea1ecdecef6a9284339bf1d4abbe359a83dd00b1259e5b767f70 +size 14603 diff --git a/examples/basic.rs b/examples/basic.rs new file mode 100644 index 0000000..0cac19e --- /dev/null +++ b/examples/basic.rs @@ -0,0 +1,96 @@ +use bevy::prelude::*; +use bevy::render::camera::ScalingMode; + +use micro_banimate::definitions::{AnimationSet, AnimationStatus, DirectionalAnimationBundle}; +use micro_banimate::directionality::{Directionality, Horizontal}; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_plugins(micro_banimate::BanimatePluginGroup) + .add_systems(Startup, load_assets) + .add_systems(PostStartup, spawn_assets) + .add_systems(Update, process_input) + .run(); +} + +#[derive(Default, Resource)] +struct ExampleAssets { + spritesheet: Handle<TextureAtlas>, + animations: Handle<AnimationSet>, +} + +fn load_assets( + mut commands: Commands, + assets: Res<AssetServer>, + mut atlas: ResMut<Assets<TextureAtlas>>, +) { + let atlas_handle = atlas.add(TextureAtlas::from_grid( + assets.load("character.png"), + Vec2::new(48., 48.), + 10, + 6, + None, + None, + )); + let anim_handle = assets.load("character.anim.json"); + + commands.insert_resource(ExampleAssets { + spritesheet: atlas_handle, + animations: anim_handle, + }); +} + +fn spawn_assets(mut commands: Commands, assets: Res<ExampleAssets>) { + const WIDTH: f32 = 480.0; + const HEIGHT: f32 = 320.0; + + commands.spawn(( + SpriteSheetBundle { + texture_atlas: assets.spritesheet.clone_weak(), + ..Default::default() + }, + DirectionalAnimationBundle::with_direction( + "idle", + assets.animations.clone_weak(), + Directionality::Right, + ), + )); + + commands.spawn(Camera2dBundle { + projection: OrthographicProjection { + area: Rect::new(-(WIDTH / 2.0), -(HEIGHT / 2.0), WIDTH / 2.0, HEIGHT / 2.0), + scaling_mode: ScalingMode::AutoMin { + min_height: HEIGHT, + min_width: WIDTH, + }, + far: 1000., + near: -1000., + ..Default::default() + }, + ..Default::default() + }); +} + +fn process_input( + input: Res<Input<KeyCode>>, + mut status_query: Query<(&mut AnimationStatus, &mut Directionality)>, +) { + for (mut status, mut direction) in &mut status_query { + if input.just_released(KeyCode::Left) { + direction.with_horizontal(Horizontal::Left); + } + + if input.just_released(KeyCode::Right) { + direction.with_horizontal(Horizontal::Right); + } + + if input.just_released(KeyCode::Key1) { + status.start_or_continue("idle"); + } + + if input.just_released(KeyCode::Key2) { + status.start_or_continue("run"); + } + } +} diff --git a/src/definitions.rs b/src/definitions.rs index c1c2096..8ab40ab 100644 --- a/src/definitions.rs +++ b/src/definitions.rs @@ -1,5 +1,4 @@ use std::collections::HashMap; -use std::ops::{Deref, DerefMut}; use std::time::Duration; use bevy::prelude::*; @@ -20,7 +19,7 @@ impl AnimationFrames { } } -#[derive(Clone, Debug, TypeUuid, PartialEq, Default, Reflect, Deref, DerefMut)] +#[derive(Clone, Debug, TypeUuid, TypePath, PartialEq, Default, Deref, DerefMut, Asset)] #[cfg_attr( feature = "serde", derive(serde::Serialize, serde::Deserialize), @@ -69,6 +68,16 @@ pub struct AnimationStatus { pub frame_time: f32, } +impl From<String> for AnimationStatus { + fn from(value: String) -> Self { + AnimationStatus { + active_name: value, + active_step: 0, + frame_time: 0.0, + } + } +} + impl AnimationStatus { pub fn set_animation(&mut self, name: impl ToString) { self.active_name = name.to_string(); @@ -86,7 +95,7 @@ impl AnimationStatus { } } -#[derive(Clone, Debug, Bundle, PartialEq, PartialOrd, Default)] +#[derive(Clone, Debug, Bundle, Default)] pub struct DirectionalAnimationBundle { pub animation_handle: Handle<AnimationSet>, pub mode: AnimationMode, @@ -95,11 +104,11 @@ pub struct DirectionalAnimationBundle { } impl DirectionalAnimationBundle { - pub fn new(initial_anim: String, handle: Handle<AnimationSet>) -> Self { + pub fn new(initial_anim: impl ToString, handle: Handle<AnimationSet>) -> Self { Self { animation_handle: handle, status: AnimationStatus { - active_name: initial_anim, + active_name: initial_anim.to_string(), active_step: 0, frame_time: 0.0, }, @@ -108,14 +117,14 @@ impl DirectionalAnimationBundle { } } pub fn with_direction( - initial_anim: String, + initial_anim: impl ToString, handle: Handle<AnimationSet>, direction: Directionality, ) -> Self { Self { animation_handle: handle, status: AnimationStatus { - active_name: initial_anim, + active_name: initial_anim.to_string(), active_step: 0, frame_time: 0.0, }, @@ -125,7 +134,7 @@ impl DirectionalAnimationBundle { } } -#[derive(Clone, Debug, Bundle, PartialEq, PartialOrd, Default)] +#[derive(Clone, Debug, Bundle, Default)] pub struct SpriteAnimationBundle { pub animation_handle: Handle<AnimationSet>, pub mode: AnimationMode, @@ -133,11 +142,11 @@ pub struct SpriteAnimationBundle { } impl SpriteAnimationBundle { - pub fn new(initial_anim: String, handle: Handle<AnimationSet>) -> Self { + pub fn new(initial_anim: impl ToString, handle: Handle<AnimationSet>) -> Self { Self { animation_handle: handle, status: AnimationStatus { - active_name: initial_anim, + active_name: initial_anim.to_string(), active_step: 0, frame_time: 0.0, }, @@ -184,45 +193,50 @@ impl SimpleAnimationBundle { } } -#[derive(Clone, Debug, Component, PartialEq, Eq, Default)] +#[derive(Clone, Debug, Component, PartialEq, Eq, Default, Deref, DerefMut)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct AnimationOverride { - pub name: String, - pub user_data: u128, -} +pub struct OverrideData(pub u128); + +#[derive(Clone, Debug, Component, PartialEq, Default, Deref, DerefMut)] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(transparent) +)] +pub struct AnimationOverride(pub AnimationStatus); + impl AnimationOverride { pub fn new(name: impl ToString) -> Self { - AnimationOverride { - name: name.to_string(), - user_data: 0, - } - } - pub fn with_user_data(name: impl ToString, user_data: u128) -> Self { - AnimationOverride { - name: name.to_string(), - user_data, - } + AnimationOverride(name.to_string().into()) } } -#[derive(Clone, Debug, Component, PartialEq, Eq, Default, Deref, DerefMut)] -pub struct OverrideStatus(pub AnimationStatus); -impl From<AnimationStatus> for OverrideStatus { +impl From<AnimationStatus> for AnimationOverride { fn from(other: AnimationStatus) -> Self { Self(other) } } +impl From<String> for AnimationOverride { + fn from(other: String) -> Self { + AnimationStatus::from(other).into() + } +} -#[derive(Clone, Debug, Bundle, PartialEq, PartialOrd, Default)] -pub struct ChildAnimationBundle { - pub animation_handle: Handle<AnimationSet>, - pub marker: SyncToParent, +#[derive(Bundle, Default)] +pub struct AnimationOverrideBundle { + pub anim: AnimationOverride, + pub data: OverrideData, } -impl ChildAnimationBundle { - pub fn new(handle: Handle<AnimationSet>) -> Self { +impl AnimationOverrideBundle { + pub fn new(anim: impl Into<AnimationOverride>) -> Self { Self { - animation_handle: handle, - ..Default::default() + anim: anim.into(), + data: OverrideData(0), } } } + +#[derive(Clone, Debug, Bundle, Default)] +pub struct ChildAnimationBundle { + marker: SyncToParent, +} diff --git a/src/directionality.rs b/src/directionality.rs index 1597a80..6c70c15 100644 --- a/src/directionality.rs +++ b/src/directionality.rs @@ -83,6 +83,7 @@ pub enum Directionality { RightUp, LeftUp, LeftDown, + #[default] RightDown, } diff --git a/src/loader.rs b/src/loader.rs index 5216464..90c65d8 100644 --- a/src/loader.rs +++ b/src/loader.rs @@ -1,20 +1,57 @@ +use std::error::Error; +use std::fmt::{Debug, Display, Formatter}; + +#[derive(Debug)] +pub enum LoaderError { + #[cfg(feature = "json_loader")] + Json(serde_json::Error), + #[cfg(feature = "toml_loader")] + Toml(toml::de::Error), + Custom(String), +} + +impl Display for LoaderError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Self::Custom(message) => Display::fmt(message, f), + #[cfg(feature = "json_loader")] + Self::Json(err) => Display::fmt(err, f), + #[cfg(feature = "toml_loader")] + Self::Toml(err) => Display::fmt(err, f), + } + } +} + +impl Error for LoaderError {} + #[cfg(feature = "json_loader")] -mod json { - use bevy::asset::{AssetLoader, BoxedFuture, Error, LoadContext, LoadedAsset}; +mod json_loader { + use bevy::asset::io::Reader; + use bevy::asset::{AssetLoader, AsyncReadExt, BoxedFuture, LoadContext}; use crate::definitions::AnimationSet; + use crate::loader::LoaderError; + + pub struct AnimationLoader; + impl AssetLoader for AnimationLoader { + type Asset = AnimationSet; + type Settings = (); + type Error = LoaderError; - pub struct AnimationSetJsonLoader; - impl AssetLoader for AnimationSetJsonLoader { fn load<'a>( &'a self, - bytes: &'a [u8], - load_context: &'a mut LoadContext, - ) -> BoxedFuture<'a, anyhow::Result<(), Error>> { + reader: &'a mut Reader, + _settings: &'a Self::Settings, + _load_context: &'a mut LoadContext, + ) -> BoxedFuture<'a, Result<Self::Asset, Self::Error>> { Box::pin(async move { - let value: AnimationSet = serde_json::from_slice(bytes)?; - load_context.set_default_asset(LoadedAsset::new(value)); - Ok(()) + let mut bytes = Vec::new(); + reader + .read_to_end(&mut bytes) + .await + .expect("Failed to read all bytes"); + + serde_json::from_slice(bytes.as_slice()).map_err(LoaderError::Json) }) } @@ -26,23 +63,33 @@ mod json { } #[cfg(feature = "toml_loader")] -mod toml { - use bevy::asset::{AssetLoader, BoxedFuture, Error, LoadContext, LoadedAsset}; +mod toml_loader { + use bevy::asset::io::Reader; + use bevy::asset::{AssetLoader, AsyncReadExt, BoxedFuture, LoadContext}; use crate::definitions::AnimationSet; + use crate::loader::LoaderError; + + pub struct AnimationLoader; + impl AssetLoader for AnimationLoader { + type Asset = AnimationSet; + type Settings = (); + type Error = LoaderError; - pub struct AnimationSetTomlLoader; - impl AssetLoader for AnimationSetTomlLoader { fn load<'a>( &'a self, - bytes: &'a [u8], - load_context: &'a mut LoadContext, - ) -> BoxedFuture<'a, anyhow::Result<(), Error>> { + reader: &'a mut Reader, + _settings: &'a Self::Settings, + _load_context: &'a mut LoadContext, + ) -> BoxedFuture<'a, Result<Self::Asset, Self::Error>> { Box::pin(async move { - let contents = String::from_utf8(bytes.to_vec())?; - let value: AnimationSet = toml::from_str(contents.as_str())?; - load_context.set_default_asset(LoadedAsset::new(value)); - Ok(()) + let mut bytes = String::new(); + reader + .read_to_string(&mut bytes) + .await + .expect("Failed to read all bytes"); + + toml::from_str(bytes.as_str()).map_err(LoaderError::Toml) }) } @@ -53,16 +100,22 @@ mod toml { } } -use bevy::app::App; -use bevy::prelude::{AddAsset, Plugin}; - -pub struct AnimationLoadersPlugin; -impl Plugin for AnimationLoadersPlugin { - fn build(&self, app: &mut App) { - app.add_asset::<crate::definitions::AnimationSet>(); - #[cfg(feature = "json_loader")] - app.add_asset_loader(json::AnimationSetJsonLoader); - #[cfg(feature = "toml_loader")] - app.add_asset_loader(self::toml::AnimationSetTomlLoader); +mod _plugin { + use bevy::app::{App, Plugin}; + use bevy::asset::AssetApp; + + pub struct AnimationLoadersPlugin; + impl Plugin for AnimationLoadersPlugin { + fn build(&self, app: &mut App) { + #[cfg(any(feature = "json_loader", feature = "toml_loader"))] + app.init_asset::<crate::definitions::AnimationSet>(); + + #[cfg(feature = "json_loader")] + app.register_asset_loader(super::json_loader::AnimationLoader); + #[cfg(feature = "toml_loader")] + app.register_asset_loader(super::toml_loader::AnimationLoader); + } } } + +pub use _plugin::AnimationLoadersPlugin; diff --git a/src/query.rs b/src/query.rs index af6fdf7..630b8cf 100644 --- a/src/query.rs +++ b/src/query.rs @@ -1,409 +1,226 @@ -use std::ops::{Deref, DerefMut}; -use std::time::Duration; - -use anyhow::{Error, Result}; -use bevy::ecs::system::SystemParam; -use bevy::prelude::*; - use crate::definitions::{ - AnimationFrames, AnimationMode, AnimationOverride, AnimationPaused, AnimationSet, - AnimationStatus, HasAnimations, HasDirectionalityAnimation, HasSimpleAnimations, - SyncAnimationsToParent, + AnimationOverride, AnimationPaused, AnimationSet, AnimationStatus, OverrideData, + SimpleAnimation, SimpleAnimationStatus, SyncToParent, }; -use crate::directionality::{Directionality, Horizontal, Vertical}; +use crate::directionality::Directionality; use crate::systems::AnimationCompleted; - -#[allow(clippy::type_complexity)] -pub type AnimationQueryType<'w, 's> = Query< - 'w, - 's, - ( - Entity, - &'static Handle<AnimationSet>, - &'static mut AnimationMode, - &'static mut AnimationStatus, - ), - ( - Or<(With<HasAnimations>, With<HasDirectionalityAnimation>)>, - Without<HasSimpleAnimations>, - Without<SyncAnimationsToParent>, - ), ->; - -/// Manage animated entities -#[derive(SystemParam)] -pub struct AnimationQuery<'w, 's> { - commands: Commands<'w, 's>, - animations: Res<'w, Assets<AnimationSet>>, - #[allow(clippy::type_complexity)] - inner: AnimationQueryType<'w, 's>, - #[allow(clippy::type_complexity)] - inner_child: Query< - 'w, - 's, - ( - Entity, - &'static Parent, - &'static Handle<AnimationSet>, - &'static mut AnimationStatus, - ), - ( - With<SyncAnimationsToParent>, - Without<HasAnimations>, - Without<HasDirectionalityAnimation>, - Without<HasSimpleAnimations>, - ), - >, - direction: Query<'w, 's, &'static mut Directionality>, - action_animation: Query<'w, 's, &'static mut AnimationOverride>, - tile_sprite: Query<'w, 's, &'static mut TextureAtlasSprite>, - paused: Query<'w, 's, (), With<AnimationPaused>>, - events: EventWriter<'w, AnimationCompleted>, +use bevy::asset::Assets; +use bevy::ecs::query::WorldQuery; +use bevy::prelude::{ + Commands, Entity, EventWriter, Handle, Parent, Query, Res, Time, With, Without, +}; +use bevy::sprite::TextureAtlasSprite; + +#[derive(WorldQuery)] +#[world_query(mutable)] +pub struct AnimationComponents { + handle: &'static Handle<AnimationSet>, + status: &'static mut AnimationStatus, + sprite: &'static mut TextureAtlasSprite, } -impl<'w, 's> Deref for AnimationQuery<'w, 's> { - type Target = AnimationQueryType<'w, 's>; - fn deref(&self) -> &Self::Target { - &self.inner - } +#[derive(WorldQuery)] +#[world_query(mutable)] +pub struct DirectionalAnimationComponents { + handle: &'static Handle<AnimationSet>, + direction: &'static Directionality, + status: &'static mut AnimationStatus, + sprite: &'static mut TextureAtlasSprite, } -impl<'w, 's> DerefMut for AnimationQuery<'w, 's> { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.inner - } +#[derive(WorldQuery)] +#[world_query(mutable)] +pub struct OverrideAnimationComponents { + handle: &'static Handle<AnimationSet>, + data: Option<&'static OverrideData>, + status: &'static mut AnimationOverride, + sprite: &'static mut TextureAtlasSprite, } -impl<'w, 's> AnimationQuery<'w, 's> { - /// Given an entity, ensure that the currently playing animation matches - /// the provided name. This method does not change the current animation - /// step - pub fn assert_animation<T: ToString>(&mut self, e: Entity, name: T) -> Result<()> { - let (_, _, _, mut status) = self.inner.get_mut(e)?; - status.assert_animation(name); - Ok(()) - } - - /// Given an entity, start an animation from that entity's animation set - /// from frame 0. If the currently playing animation is requested, it - /// will be restarted - pub fn start_animation<T: ToString>(&mut self, e: Entity, name: T) -> Result<()> { - let (_, _, _, mut status) = self.inner.get_mut(e)?; - status.start_animation(name); - Ok(()) - } - - /// Given an entity, start an animation from that entity's animation set - /// from frame 0. If the currently playing animation is requested, no change - /// will happen and it will continue to play uninterrupted - pub fn start_or_continue_animation<T: ToString>(&mut self, e: Entity, name: T) -> Result<()> { - let (_, _, _, mut status) = self.inner.get_mut(e)?; - status.start_or_continue(name); - Ok(()) - } +#[derive(WorldQuery)] +#[world_query(mutable)] +pub struct SimpleAnimationComponents { + anim: &'static SimpleAnimation, + status: &'static mut SimpleAnimationStatus, + sprite: &'static mut TextureAtlasSprite, +} - /// Given an entity, start playing an animation once and then switch - /// to a looped version of another animation. If the currently playing - /// animation is requested, it will be restarted - pub fn start_animation_and_then<T: ToString, Y: ToString>( - &mut self, - e: Entity, - name: T, - next: Y, - ) -> Result<()> { - let (_, _, mut mode, mut status) = self.inner.get_mut(e)?; +#[derive(WorldQuery)] +pub struct OnlyAnimations { + _status: With<AnimationStatus>, + _override: Without<AnimationOverride>, + _direction: Without<Directionality>, + _paused: Without<AnimationPaused>, +} - *mode = AnimationMode::OnceThenPlay(next.to_string()); - status.active_name = name.to_string(); - status.active_step = 0; +#[derive(WorldQuery)] +pub struct OnlyDirectionalAnimations { + _status: With<AnimationStatus>, + _direction: With<Directionality>, + _override: Without<AnimationOverride>, + _paused: Without<AnimationPaused>, +} - Ok(()) - } +#[derive(WorldQuery)] +pub struct OnlyOverrideAnimations { + _override: With<AnimationOverride>, + _direction: Without<Directionality>, + _paused: Without<AnimationPaused>, +} - /// Given an entity, start playing an animation once and then switch - /// to a looped version of another animation. If the currently playing - /// animation is requested, it will be restarted - pub fn continue_animation_and_then<T: ToString, Y: ToString>( - &mut self, - e: Entity, - name: T, - next: Y, - ) -> Result<()> { - let (_, _, mut mode, mut status) = self.inner.get_mut(e)?; - let name = name.to_string(); +#[derive(WorldQuery)] +pub struct OnlyDirectionalOverrideAnimations { + _override: With<AnimationOverride>, + _direction: With<Directionality>, + _paused: Without<AnimationPaused>, +} - *mode = AnimationMode::OnceThenPlay(next.to_string()); - if status.active_name != name { - status.active_name = name; - status.active_step = 0; +macro_rules! get_current_anim { + ($anims: expr, $handle: expr, $name: expr) => { + match $anims.get($handle) { + Some(active) => match active.get(&$name) { + Some(inner) => inner, + None => continue, + }, + None => continue, } - Ok(()) - } - - /// Set an entity's animations to play one time. The animation - /// will pause on the last frame - pub fn play_once(&mut self, e: Entity) -> Result<()> { - let (_, _, mut mode, _) = self.inner.get_mut(e)?; - *mode = AnimationMode::Once; - Ok(()) - } - - /// Set an entity's animations to loop continuously. The - /// animation will reset to the start once it has reached the - /// last frame - pub fn play_loop(&mut self, e: Entity) -> Result<()> { - let (_, _, mut mode, _) = self.inner.get_mut(e)?; - *mode = AnimationMode::Loop; - Ok(()) - } - - /// Set an entity's animations to play one time. The animation - /// will be changed to the specified set once the current animation - /// has reached the last frame - pub fn play_next<T: ToString>(&mut self, e: Entity, next: T) -> Result<()> { - let (_, _, mut mode, _) = self.inner.get_mut(e)?; - *mode = AnimationMode::OnceThenPlay(next.to_string()); - Ok(()) - } - - /// Measure the amount of time it will take an animation to run. - /// The exact time that an animation runs for will differ slightly - /// from this, due to the minimum time increment being 1 frame - /// (16ms when running at 60fps) - pub fn get_animation_duration<T: ToString>( - &self, - handle: Handle<AnimationSet>, - name: T, - ) -> Result<Duration> { - let sheet = self - .animations - .get(&handle) - .ok_or_else(|| anyhow::Error::msg("Failed to fetch animation set"))?; - - let details = sheet.get(name.to_string().as_str()).ok_or_else(|| { - anyhow::Error::msg(format!("Missing animation from set: {}", name.to_string())) - })?; + }; + ($anims: expr, $handle: expr, $name: expr, $($also: expr),+) => { + match $anims.get($handle) { + Some(active) => match active.get(&$name)$(.or_else(|| active.get(&$also)))+ { + Some(inner) => inner, + None => continue, + }, + None => continue, + } + }; +} - Ok(Duration::from_secs_f32( - details.frame_secs * details.frames.len() as f32, - )) - } +macro_rules! tick_animation { + ($delta: expr, $anim: expr, $status: expr, $sprite: expr) => {{ + let current_frame = $sprite.index; + let mut has_looped = false; + + $status.frame_time += $delta.as_secs_f32(); + while $status.frame_time >= $anim.frame_secs { + $status.frame_time = ($status.frame_time - $anim.frame_secs).max(0.0); + let next_frame = $status.active_step.saturating_add(1); + $status.active_step = if next_frame >= $anim.frames.len() { + has_looped = true; + 0 + } else { + next_frame + }; + } - pub fn resolve_animation( - &self, - handle: &Handle<AnimationSet>, - name: &String, - ) -> Option<&AnimationFrames> { - self.animations - .get(handle) - .and_then(|sheet| sheet.get(name)) - } + if current_frame != $anim.frames[$status.active_step] { + $sprite.index = $anim.frames[$status.active_step]; + } - /// Check whether or not an entity is currently paused. If so, - /// the entity will not tick its animation state - pub fn is_paused(&self, e: Entity) -> bool { - self.paused.get(e).is_ok() - } + has_looped + }}; +} - /// Attach a "paused" marker to this entity that will cause - /// standard animations to not tick. This is safe to call - /// with an entity that might already be paused; no effect will - /// take place - pub fn pause(&mut self, e: Entity) { - self.commands.entity(e).insert(AnimationPaused); +pub fn play_animations( + time: Res<Time>, + mut anim_query: Query<AnimationComponents, OnlyAnimations>, + animations: Res<Assets<AnimationSet>>, +) { + let delta = time.delta(); + for AnimationComponentsItem { + mut status, + handle, + mut sprite, + } in &mut anim_query + { + let anim = get_current_anim!(animations, handle, status.active_name); + tick_animation!(delta, anim, status, sprite); } +} - /// Remove any "paused" markers from this entity, which will cause - /// standard animations to resume ticking. This is safe to call - /// with an entity that might already be paused; no effect will - /// take place - pub fn unpause(&mut self, e: Entity) { - self.commands.entity(e).remove::<AnimationPaused>(); +pub fn play_directional_animations( + time: Res<Time>, + mut anim_query: Query<DirectionalAnimationComponents, OnlyDirectionalAnimations>, + animations: Res<Assets<AnimationSet>>, +) { + let delta = time.delta(); + for DirectionalAnimationComponentsItem { + mut status, + handle, + mut sprite, + direction, + } in &mut anim_query + { + let anim = get_current_anim!( + animations, + handle, + format!("{}_{}", status.active_name, direction), + status.active_name + ); + + tick_animation!(delta, anim, status, sprite); } +} - pub fn set_direction(&mut self, e: Entity, horizontal: Horizontal, vertical: Vertical) { - if let Ok(mut direction) = self.direction.get_mut(e) { - direction.horizontal = horizontal; - direction.vertical = vertical; - } - } - pub fn set_partial_direction( - &mut self, - e: Entity, - horizontal: Option<Horizontal>, - vertical: Option<Vertical>, - ) { - if let Ok(mut direction) = self.direction.get_mut(e) { - if let Some(horizontal) = horizontal { - direction.horizontal = horizontal; - } - if let Some(vertical) = vertical { - direction.vertical = vertical; +pub fn play_override_animation( + time: Res<Time>, + mut commands: Commands, + mut anim_query: Query<(Entity, OverrideAnimationComponents), OnlyOverrideAnimations>, + animations: Res<Assets<AnimationSet>>, + mut events: EventWriter<AnimationCompleted>, +) { + let delta = time.delta(); + for ( + entity, + OverrideAnimationComponentsItem { + mut status, + handle, + mut sprite, + data, + }, + ) in &mut anim_query + { + let anim = get_current_anim!(animations, handle, status.active_name); + let looped = tick_animation!(delta, anim, status, sprite); + if looped { + commands + .entity(entity) + .remove::<(AnimationOverride, OverrideData)>(); + + if let Some(data) = data { + events.send(AnimationCompleted { + entity, + user_data: **data, + }); } } } +} - /// Apply a number of delta seconds to all unpaused animations - pub fn tick_all(&mut self, dt: f32) -> Result<()> { - for (entity, handle, mut mode, mut status) in &mut self.inner { - if self.paused.get(entity).is_ok() { - continue; - } - let sheet = match self.animations.get(handle) { - Some(sheet) => sheet, - None => continue, - }; - - if let Ok(mut status) = self.action_animation.get_mut(entity) { - let (a, b) = match self.direction.get(entity) { - Ok(dir) => { - let directional = format!("{}_{}", status.name, dir); - (directional, status.name.clone()) - } - Err(_) => (status.name.clone(), status.name.clone()), - }; - - let current = match sheet.get(&a).or_else(|| sheet.get(&b)) { - Some(set) => set, - None => { - self.commands.entity(entity).remove::<AnimationOverride>(); - self.events.send(AnimationCompleted { - entity, - user_data: status.user_data, - }); - continue; - } - }; - - status.frame_time += dt; - while status.frame_time >= current.frame_secs { - status.frame_time -= current.frame_secs; - status.frame_step += 1; - } - - if status.frame_step >= current.frames.len() { - self.commands.entity(entity).remove::<AnimationOverride>(); - self.events.send(AnimationCompleted { - entity, - user_data: status.user_data, - }); - status.frame_step = current.frames.len() - 1; - } - - if let Ok(mut sprite) = self.tile_sprite.get_mut(entity) { - sprite.index = current.frames[status.frame_step]; - } - } else { - let mut current = match sheet.get(&status.active_name) { - Some(set) => set, - None => continue, - }; - - status.frame_time += dt; - while status.frame_time >= current.frame_secs { - status.frame_time -= current.frame_secs; - status.active_step += 1; - } - - if status.active_step >= current.frames.len() { - match mode.clone() { - AnimationMode::Loop => { - status.active_step = 0; - } - AnimationMode::Once => { - status.active_step = current.frames.len() - 1; - } - AnimationMode::OnceThenPlay(next) => { - *mode = AnimationMode::Loop; - status.active_name = next.clone(); - status.frame_time = 0.0; - status.active_step = 0; - - current = match sheet.get(&status.active_name) { - Some(set) => set, - None => continue, - }; - } - } - } - - if let Ok(mut sprite) = self.tile_sprite.get_mut(entity) { - sprite.index = current.frames[status.active_step]; - } - } - } - - Ok(()) +pub fn play_simple_animation( + time: Res<Time>, + mut anim_query: Query<SimpleAnimationComponents, Without<AnimationPaused>>, +) { + let delta = time.delta(); + for SimpleAnimationComponentsItem { + mut status, + mut sprite, + anim, + } in &mut anim_query + { + tick_animation!(delta, *anim, status, sprite); } +} - /// Syncs all animations that are set to track their parents - pub fn sync_parent_animations(&mut self) { - for (entity, parent, handle, mut status) in &mut self.inner_child { - if let Ok((_, _, _, parent_status)) = self.inner.get(**parent) { - *status = parent_status.clone(); - } - - if let Ok(mut sprite) = self.tile_sprite.get_mut(entity) { - let (directional_name, active_name, frame) = - match self.action_animation.get_mut(**parent) { - Ok(override_status) => match self.direction.get(**parent) { - Ok(dir) => { - let directional = format!("{}_{}", override_status.name, dir); - ( - directional, - override_status.name.clone(), - override_status.frame_step, - ) - } - Err(_) => ( - override_status.name.clone(), - override_status.name.clone(), - override_status.frame_step, - ), - }, - Err(_) => ( - status.active_name.clone(), - status.active_name.clone(), - status.active_step, - ), - }; - - if let Some(current) = self.animations.get(handle).and_then(|sheet| { - sheet - .get(&directional_name) - .or_else(|| sheet.get(&active_name)) - }) { - sprite.index = current.frames[frame]; - } +pub fn sync_child_animation( + mut children: Query<(&Parent, &mut TextureAtlasSprite), With<SyncToParent>>, + parents: Query<&TextureAtlasSprite, Without<SyncToParent>>, +) { + for (parent, mut child_sprite) in &mut children { + if let Ok(parent_sprite) = parents.get(**parent) { + if parent_sprite.index != child_sprite.index { + child_sprite.index = parent_sprite.index; } } } - - pub fn apply_directionality_to<T: ToString>(&mut self, action: T, e: Entity) -> Result<()> { - if self.paused.get(e).is_ok() { - return Ok(()); - } - - let (_, animations, _, mut status) = self.inner.get_mut(e)?; - let direction = self.direction.get(e)?; - let over = self.action_animation.get(e).ok(); - let set = self - .animations - .get(animations) - .ok_or_else(|| Error::msg("Missing Animation"))?; - - let fallback = over - .map(|o| o.name.clone()) - .unwrap_or_else(|| action.to_string()); - - let directed = format!("{}_{}", &fallback, direction); - - if set.contains_key(&directed) { - status.start_or_continue(directed); - } else { - status.start_or_continue(fallback); - } - - Ok(()) - } } diff --git a/src/systems.rs b/src/systems.rs index 58aef2c..4febe7f 100644 --- a/src/systems.rs +++ b/src/systems.rs @@ -1,8 +1,9 @@ +use crate::query::{ + play_animations, play_directional_animations, play_override_animation, play_simple_animation, + sync_child_animation, +}; use bevy::prelude::*; -use crate::definitions::*; -use crate::query::AnimationQuery; - #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, SystemSet)] pub enum AnimationSystems { TickAnimations, @@ -16,95 +17,30 @@ pub struct AnimationCompleted { pub user_data: u128, } -pub fn tick_animations(time: Res<Time>, mut query: AnimationQuery) { - let seconds = time.delta_seconds(); - let _ = query.tick_all(seconds); -} - -pub fn tick_simple_sprite_animations( - time: Res<Time>, - mut query: Query< - ( - &SimpleLoopedAnimation, - &mut SimpleLoopedAnimationStatus, - &mut TextureAtlasSprite, - ), - With<HasSimpleAnimations>, - >, -) { - let seconds = time.delta_seconds(); - for (animation, mut state, mut sprite) in &mut query { - state.frame_time += seconds; - while state.frame_time >= animation.frame_secs { - state.frame_time -= animation.frame_secs; - state.active_step += 1; - if state.active_step >= animation.frames.len() { - state.active_step = 0; - } - } - - sprite.index = animation.frames[state.active_step]; - } -} - -pub fn sync_parent_animations(mut query: AnimationQuery) { - query.sync_parent_animations(); -} - -#[cfg(feature = "ecs_tilemap")] -pub fn tick_simple_tilemap_animation( - time: Res<Time>, - mut query: Query< - ( - &SimpleLoopedAnimation, - &mut SimpleLoopedAnimationStatus, - &mut bevy_ecs_tilemap::tiles::TileTextureIndex, - ), - With<HasSimpleAnimations>, - >, -) { - let seconds = time.delta_seconds(); - for (animation, mut state, mut tile) in &mut query { - state.frame_time += seconds; - while state.frame_time >= animation.frame_secs { - state.frame_time -= animation.frame_secs; - state.active_step += 1; - if state.active_step >= animation.frames.len() { - state.active_step = 0; - } - } - - *tile = - bevy_ecs_tilemap::tiles::TileTextureIndex(animation.frames[state.active_step] as u32); - } -} - pub struct AnimationSystemsPlugin; impl Plugin for AnimationSystemsPlugin { fn build(&self, app: &mut App) { + app.add_event::<AnimationCompleted>(); + app.configure_sets( PostUpdate, (AnimationSystems::TickAnimations.before(AnimationSystems::SyncAnimations),), ); - app.add_event::<AnimationCompleted>() - .add_systems( - PostUpdate, - (tick_animations, tick_simple_sprite_animations) - .in_set(AnimationSystems::SyncAnimations), + app.add_systems( + PostUpdate, + ( + play_animations, + play_override_animation, + play_directional_animations, + play_simple_animation, ) - .add_systems( - PostUpdate, - sync_parent_animations.in_set(AnimationSystems::SyncAnimations), - ); - - #[cfg(feature = "ecs_tilemap")] - { - app.add_systems( - PostUpdate, - tick_simple_tilemap_animation.in_set(AnimationSystems::TickAnimations), - ); - } + .in_set(AnimationSystems::SyncAnimations), + ) + .add_systems( + PostUpdate, + sync_child_animation.in_set(AnimationSystems::SyncAnimations), + ); } } -- GitLab