diff --git a/CHANGELOG.md b/CHANGELOG.md index ddf2c3c4eb972815937606ef1a3886246790948b..4335441de397c5ee6fcf13cc9cc54f17f73ba05a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added `Tweenable::is_looping()`, `Tweenable::set_progress()`, `Tweenable::times_completed()`, and `Tweenable::rewind()`. - Added `Animator::set_progress()`, `Animator::progress()`, `Animator::stop()`, and `Animator::rewind()`. - Added `AssetAnimator::set_progress()`, `AssetAnimator::progress()`, `AssetAnimator::stop()`, and `AssetAnimator::rewind()`. +- Added the `TweenCompleted` event, raised when a `Tween<T>` completed its animation if that feature was previously activated with `set_completed_event()` or `with_completed_event()`. ### Changed diff --git a/README.md b/README.md index 6efebcdc4c728e5183de445d78124b3daff537c0..335ba82bf4993f0f840b6812af80d62f7980bc52 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ Tweening animation plugin for the Bevy game engine. - [x] Animate any field of any component or asset, including custom ones. - [x] Run multiple tweens (animations) per component/asset in parallel. - [x] Chain multiple tweens (animations) one after the other for complex animations. +- [x] Raise a Bevy event or invoke a callback when an tween completed. ## Usage diff --git a/examples/sequence.rs b/examples/sequence.rs index 08d7ad7ca727fa7d3cc45e1cf5a2cfa1a14311b1..0cefa6b2abe43ebb9f93d2bf8effbf2ee433341b 100644 --- a/examples/sequence.rs +++ b/examples/sequence.rs @@ -1,223 +1,226 @@ -use bevy::prelude::*; -use bevy_tweening::{lens::*, *}; -use std::time::Duration; - -fn main() -> Result<(), Box<dyn std::error::Error>> { - App::default() - .insert_resource(WindowDescriptor { - title: "Sequence".to_string(), - width: 600., - height: 600., - vsync: true, - ..Default::default() - }) - .add_plugins(DefaultPlugins) - .add_plugin(TweeningPlugin) - .add_startup_system(setup) - .add_system(update_text) - .run(); - - Ok(()) -} - -#[derive(Component)] -struct RedProgress; - -#[derive(Component)] -struct BlueProgress; - -#[derive(Component)] -struct RedSprite; - -#[derive(Component)] -struct BlueSprite; - -fn setup(mut commands: Commands, asset_server: Res<AssetServer>) { - commands.spawn_bundle(OrthographicCameraBundle::new_2d()); - - let font = asset_server.load("fonts/FiraMono-Regular.ttf"); - let text_style_red = TextStyle { - font: font.clone(), - font_size: 50.0, - color: Color::RED, - }; - let text_style_blue = TextStyle { - font: font.clone(), - font_size: 50.0, - color: Color::BLUE, - }; - - let text_alignment = TextAlignment { - vertical: VerticalAlign::Center, - horizontal: HorizontalAlign::Center, - }; - - // Text with the index of the active tween in the sequence - commands - .spawn_bundle(Text2dBundle { - text: Text { - sections: vec![ - TextSection { - value: "progress: ".to_owned(), - style: text_style_red.clone(), - }, - TextSection { - value: "0%".to_owned(), - style: text_style_red.clone(), - }, - ], - alignment: text_alignment, - }, - transform: Transform::from_translation(Vec3::new(0., 40., 0.)), - ..Default::default() - }) - .insert(RedProgress); - - // Text with progress of the active tween in the sequence - commands - .spawn_bundle(Text2dBundle { - text: Text { - sections: vec![ - TextSection { - value: "progress: ".to_owned(), - style: text_style_blue.clone(), - }, - TextSection { - value: "0%".to_owned(), - style: text_style_blue.clone(), - }, - ], - alignment: text_alignment, - }, - transform: Transform::from_translation(Vec3::new(0., -40., 0.)), - ..Default::default() - }) - .insert(BlueProgress); - - let size = 25.; - - let margin = 40.; - let screen_x = 600.; - let screen_y = 600.; - let center = Vec3::new(screen_x / 2., screen_y / 2., 0.); - - // Run around the window from corner to corner - let dests = &[ - Vec3::new(margin, margin, 0.), - Vec3::new(screen_x - margin, margin, 0.), - Vec3::new(screen_x - margin, screen_y - margin, 0.), - Vec3::new(margin, screen_y - margin, 0.), - Vec3::new(margin, margin, 0.), - ]; - // Build a sequence from an iterator over a Tweenable (here, a Tween<Transform>) - let seq = Sequence::new(dests.windows(2).map(|pair| { - Tween::new( - EaseFunction::QuadraticInOut, - TweeningType::Once, - Duration::from_secs(1), - TransformPositionLens { - start: pair[0] - center, - end: pair[1] - center, - }, - ) - .with_completed_event(true) // Get an event after each segment - })); - - commands - .spawn_bundle(SpriteBundle { - sprite: Sprite { - color: Color::RED, - custom_size: Some(Vec2::new(size, size)), - ..Default::default() - }, - ..Default::default() - }) - .insert(RedSprite) - .insert(Animator::new(seq)); - - // First move from left to right, then rotate around self 180 degrees while scaling - // size at the same time. - let tween_move = Tween::new( - EaseFunction::QuadraticInOut, - TweeningType::Once, - Duration::from_secs(1), - TransformPositionLens { - start: Vec3::new(-200., 100., 0.), - end: Vec3::new(200., 100., 0.), - }, - ) - .with_completed_event(true); // Get an event once move completed - let tween_rotate = Tween::new( - EaseFunction::QuadraticInOut, - TweeningType::Once, - Duration::from_secs(1), - TransformRotationLens { - start: Quat::IDENTITY, - end: Quat::from_rotation_z(180_f32.to_radians()), - }, - ); - let tween_scale = Tween::new( - EaseFunction::QuadraticInOut, - TweeningType::Once, - Duration::from_secs(1), - TransformScaleLens { - start: Vec3::ONE, - end: Vec3::splat(2.0), - }, - ); - // Build parallel tracks executing two tweens at the same time : rotate and scale. - let tracks = Tracks::new([tween_rotate, tween_scale]); - // Build a sequence from an heterogeneous list of tweenables by casting them manually - // to a boxed Tweenable<Transform> : first move, then { rotate + scale }. - let seq2 = Sequence::new([ - Box::new(tween_move) as Box<dyn Tweenable<Transform> + Send + Sync + 'static>, - Box::new(tracks) as Box<dyn Tweenable<Transform> + Send + Sync + 'static>, - ]); - - commands - .spawn_bundle(SpriteBundle { - sprite: Sprite { - color: Color::BLUE, - custom_size: Some(Vec2::new(size * 3., size)), - ..Default::default() - }, - ..Default::default() - }) - .insert(BlueSprite) - .insert(Animator::new(seq2)); -} - -fn update_text( - // Note: need a QuerySet<> due to the "&mut Text" in both queries - mut query_text: QuerySet<( - QueryState<&mut Text, With<RedProgress>>, - QueryState<&mut Text, With<BlueProgress>>, - )>, - query_anim_red: Query<&Animator<Transform>, With<RedSprite>>, - query_anim_blue: Query<&Animator<Transform>, With<BlueSprite>>, - mut query_event: EventReader<TweenCompleted>, -) { - let anim_red = query_anim_red.single(); - let tween_red = anim_red.tweenable().unwrap(); - let progress_red = tween_red.progress(); - - let anim_blue = query_anim_blue.single(); - let tween_blue = anim_blue.tweenable().unwrap(); - let progress_blue = tween_blue.progress(); - - // Use scopes to force-drop the mutable context before opening the next one - { - let mut q0 = query_text.q0(); - let mut red_text = q0.single_mut(); - red_text.sections[1].value = format!("{:5.1}%", progress_red * 100.).to_string(); - } - { - let mut q1 = query_text.q1(); - let mut blue_text = q1.single_mut(); - blue_text.sections[1].value = format!("{:5.1}%", progress_blue * 100.).to_string(); - } - - for ev in query_event.iter() { - println!("Event: TweenCompleted entity={:?}", ev.entity); - } -} +use bevy::prelude::*; +use bevy_tweening::{lens::*, *}; +use std::time::Duration; + +fn main() -> Result<(), Box<dyn std::error::Error>> { + App::default() + .insert_resource(WindowDescriptor { + title: "Sequence".to_string(), + width: 600., + height: 600., + vsync: true, + ..Default::default() + }) + .add_plugins(DefaultPlugins) + .add_plugin(TweeningPlugin) + .add_startup_system(setup) + .add_system(update_text) + .run(); + + Ok(()) +} + +#[derive(Component)] +struct RedProgress; + +#[derive(Component)] +struct BlueProgress; + +#[derive(Component)] +struct RedSprite; + +#[derive(Component)] +struct BlueSprite; + +fn setup(mut commands: Commands, asset_server: Res<AssetServer>) { + commands.spawn_bundle(OrthographicCameraBundle::new_2d()); + + let font = asset_server.load("fonts/FiraMono-Regular.ttf"); + let text_style_red = TextStyle { + font: font.clone(), + font_size: 50.0, + color: Color::RED, + }; + let text_style_blue = TextStyle { + font: font.clone(), + font_size: 50.0, + color: Color::BLUE, + }; + + let text_alignment = TextAlignment { + vertical: VerticalAlign::Center, + horizontal: HorizontalAlign::Center, + }; + + // Text with the index of the active tween in the sequence + commands + .spawn_bundle(Text2dBundle { + text: Text { + sections: vec![ + TextSection { + value: "progress: ".to_owned(), + style: text_style_red.clone(), + }, + TextSection { + value: "0%".to_owned(), + style: text_style_red.clone(), + }, + ], + alignment: text_alignment, + }, + transform: Transform::from_translation(Vec3::new(0., 40., 0.)), + ..Default::default() + }) + .insert(RedProgress); + + // Text with progress of the active tween in the sequence + commands + .spawn_bundle(Text2dBundle { + text: Text { + sections: vec![ + TextSection { + value: "progress: ".to_owned(), + style: text_style_blue.clone(), + }, + TextSection { + value: "0%".to_owned(), + style: text_style_blue.clone(), + }, + ], + alignment: text_alignment, + }, + transform: Transform::from_translation(Vec3::new(0., -40., 0.)), + ..Default::default() + }) + .insert(BlueProgress); + + let size = 25.; + + let margin = 40.; + let screen_x = 600.; + let screen_y = 600.; + let center = Vec3::new(screen_x / 2., screen_y / 2., 0.); + + // Run around the window from corner to corner + let dests = &[ + Vec3::new(margin, margin, 0.), + Vec3::new(screen_x - margin, margin, 0.), + Vec3::new(screen_x - margin, screen_y - margin, 0.), + Vec3::new(margin, screen_y - margin, 0.), + Vec3::new(margin, margin, 0.), + ]; + // Build a sequence from an iterator over a Tweenable (here, a Tween<Transform>) + let seq = Sequence::new(dests.windows(2).enumerate().map(|(index, pair)| { + Tween::new( + EaseFunction::QuadraticInOut, + TweeningType::Once, + Duration::from_secs(1), + TransformPositionLens { + start: pair[0] - center, + end: pair[1] - center, + }, + ) + .with_completed_event(true, index as u64) // Get an event after each segment + })); + + commands + .spawn_bundle(SpriteBundle { + sprite: Sprite { + color: Color::RED, + custom_size: Some(Vec2::new(size, size)), + ..Default::default() + }, + ..Default::default() + }) + .insert(RedSprite) + .insert(Animator::new(seq)); + + // First move from left to right, then rotate around self 180 degrees while scaling + // size at the same time. + let tween_move = Tween::new( + EaseFunction::QuadraticInOut, + TweeningType::Once, + Duration::from_secs(1), + TransformPositionLens { + start: Vec3::new(-200., 100., 0.), + end: Vec3::new(200., 100., 0.), + }, + ) + .with_completed_event(true, 99); // Get an event once move completed + let tween_rotate = Tween::new( + EaseFunction::QuadraticInOut, + TweeningType::Once, + Duration::from_secs(1), + TransformRotationLens { + start: Quat::IDENTITY, + end: Quat::from_rotation_z(180_f32.to_radians()), + }, + ); + let tween_scale = Tween::new( + EaseFunction::QuadraticInOut, + TweeningType::Once, + Duration::from_secs(1), + TransformScaleLens { + start: Vec3::ONE, + end: Vec3::splat(2.0), + }, + ); + // Build parallel tracks executing two tweens at the same time : rotate and scale. + let tracks = Tracks::new([tween_rotate, tween_scale]); + // Build a sequence from an heterogeneous list of tweenables by casting them manually + // to a boxed Tweenable<Transform> : first move, then { rotate + scale }. + let seq2 = Sequence::new([ + Box::new(tween_move) as Box<dyn Tweenable<Transform> + Send + Sync + 'static>, + Box::new(tracks) as Box<dyn Tweenable<Transform> + Send + Sync + 'static>, + ]); + + commands + .spawn_bundle(SpriteBundle { + sprite: Sprite { + color: Color::BLUE, + custom_size: Some(Vec2::new(size * 3., size)), + ..Default::default() + }, + ..Default::default() + }) + .insert(BlueSprite) + .insert(Animator::new(seq2)); +} + +fn update_text( + // Note: need a QuerySet<> due to the "&mut Text" in both queries + mut query_text: QuerySet<( + QueryState<&mut Text, With<RedProgress>>, + QueryState<&mut Text, With<BlueProgress>>, + )>, + query_anim_red: Query<&Animator<Transform>, With<RedSprite>>, + query_anim_blue: Query<&Animator<Transform>, With<BlueSprite>>, + mut query_event: EventReader<TweenCompleted>, +) { + let anim_red = query_anim_red.single(); + let tween_red = anim_red.tweenable().unwrap(); + let progress_red = tween_red.progress(); + + let anim_blue = query_anim_blue.single(); + let tween_blue = anim_blue.tweenable().unwrap(); + let progress_blue = tween_blue.progress(); + + // Use scopes to force-drop the mutable context before opening the next one + { + let mut q0 = query_text.q0(); + let mut red_text = q0.single_mut(); + red_text.sections[1].value = format!("{:5.1}%", progress_red * 100.).to_string(); + } + { + let mut q1 = query_text.q1(); + let mut blue_text = q1.single_mut(); + blue_text.sections[1].value = format!("{:5.1}%", progress_blue * 100.).to_string(); + } + + for ev in query_event.iter() { + println!( + "Event: TweenCompleted entity={:?} user_data={}", + ev.entity, ev.user_data + ); + } +} diff --git a/src/lib.rs b/src/lib.rs index e48e947e29158babb6d10120679eccd71e6d2e04..845325914e0802cc9a529f486172b67208fb2aee 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -170,11 +170,13 @@ pub use tweenable::{Delay, Sequence, Tracks, Tween, TweenCompleted, TweenState, /// Type of looping for a tween animation. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum TweeningType { - /// Run the animation once from state to end only. + /// Run the animation once from start to end only. Once, - /// Looping, restarting from the start once finished. + /// Loop the animation indefinitely, restarting from the start each time the end is reached. Loop, - /// Repeat the animation back and forth. + /// Loop the animation back and forth, changing direction each time an endpoint is reached. + /// A complete cycle start -> end -> start always counts as 2 loop iterations for the various + /// operations where looping matters. PingPong, } diff --git a/src/tweenable.rs b/src/tweenable.rs index 45d71ef801659126a981f8269bda4cba8244ee7f..e5a324773ee56d343bf2d7c8e1366cf4bda3c28b 100644 --- a/src/tweenable.rs +++ b/src/tweenable.rs @@ -17,10 +17,29 @@ pub enum TweenState { } /// Event raised when a tween completed. +/// +/// This event is raised when a tween completed. For non-looping tweens, this is raised once at the +/// end of the animation. For looping animations, this is raised once per iteration. In case the animation +/// direction changes ([`TweeningType::PingPong`]), an iteration corresponds to a single progress from +/// one endpoint to the other, whatever the direction. Therefore a complete cycle start -> end -> start +/// counts as 2 iterations and raises 2 events (one when reaching the end, one when reaching back the start). +/// +/// # Note +/// +/// The semantic is slightly different from [`TweenState::Completed`], which indicates that the tweenable +/// has finished ticking and do not need to be updated anymore, a state which is never reached for looping +/// animation. Here the [`TweenCompleted`] event instead marks the end of a single loop iteration. #[derive(Copy, Clone)] pub struct TweenCompleted { /// The [`Entity`] the tween which completed and its animator are attached to. pub entity: Entity, + /// An opaque value set by the user when activating event raising, used to identify the particular + /// tween which raised this event. The value is passed unmodified from a call to [`with_completed_event()`] + /// or [`set_completed_event()`]. + /// + /// [`with_completed_event()`]: Tween::with_completed_event + /// [`set_completed_event()`]: Tween::set_completed_event + pub user_data: u64, } /// An animatable entity, either a single [`Tween`] or a collection of them. @@ -136,7 +155,7 @@ pub struct Tween<T> { times_completed: u32, lens: Box<dyn Lens<T> + Send + Sync + 'static>, on_completed: Option<Box<dyn Fn(Entity, &Tween<T>) + Send + Sync + 'static>>, - raise_event: bool, + event_data: Option<u64>, } impl<T: 'static> Tween<T> { @@ -207,18 +226,43 @@ impl<T> Tween<T> { times_completed: 0, lens: Box::new(lens), on_completed: None, - raise_event: false, + event_data: None, } } /// Enable or disable raising a completed event. /// /// If enabled, the tween will raise a [`TweenCompleted`] event when the animation completed. - /// This is similar to the [`set_completed`] callback, but uses Bevy events instead. + /// This is similar to the [`set_completed()`] callback, but uses Bevy events instead. /// - /// [`set_completed`]: Tween::set_completed - pub fn with_completed_event(mut self, enabled: bool) -> Self { - self.raise_event = enabled; + /// # Example + /// ``` + /// # use bevy_tweening::{lens::*, *}; + /// # use bevy::{ecs::event::EventReader, math::Vec3}; + /// # use std::time::Duration; + /// let tween = Tween::new( + /// // [...] + /// # EaseFunction::QuadraticInOut, + /// # TweeningType::Once, + /// # Duration::from_secs_f32(1.0), + /// # TransformPositionLens { + /// # start: Vec3::ZERO, + /// # end: Vec3::new(3.5, 0., 0.), + /// # }, + /// ) + /// .with_completed_event(true, 42); + /// + /// fn my_system(mut reader: EventReader<TweenCompleted>) { + /// for ev in reader.iter() { + /// assert_eq!(ev.user_data, 42); + /// println!("Entity {:?} raised TweenCompleted!", ev.entity); + /// } + /// } + /// ``` + /// + /// [`set_completed()`]: Tween::set_completed + pub fn with_completed_event(mut self, enabled: bool, user_data: u64) -> Self { + self.event_data = if enabled { Some(user_data) } else { None }; self } @@ -250,11 +294,14 @@ impl<T> Tween<T> { /// Enable or disable raising a completed event. /// /// If enabled, the tween will raise a [`TweenCompleted`] event when the animation completed. - /// This is similar to the [`set_completed`] callback, but uses Bevy events instead. + /// This is similar to the [`set_completed()`] callback, but uses Bevy events instead. + /// + /// See [`with_completed_event()`] for details. /// - /// [`set_completed`]: Tween::set_completed - pub fn set_completed_event(&mut self, enabled: bool) { - self.raise_event = enabled; + /// [`set_completed()`]: Tween::set_completed + /// [`with_completed_event()`]: Tween::with_completed_event + pub fn set_completed_event(&mut self, enabled: bool, user_data: u64) { + self.event_data = if enabled { Some(user_data) } else { None }; } } @@ -316,8 +363,11 @@ impl<T> Tweenable<T> for Tween<T> { // Timer::times_finished() returns the number of finished times since last tick only self.times_completed += self.timer.times_finished(); - if self.raise_event { - event_writer.send(TweenCompleted { entity }); + if let Some(user_data) = &self.event_data { + event_writer.send(TweenCompleted { + entity, + user_data: *user_data, + }); } if let Some(cb) = &self.on_completed { cb(entity, &self);