diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a04e1f8..e55b4b2 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -100,7 +100,7 @@ jobs: components: rustfmt, clippy - name: Install cargo-tarpaulin run: | - RUST_BACKTRACE=1 cargo install --version 0.31.2 cargo-tarpaulin + RUST_BACKTRACE=1 cargo install --version 0.32.8 cargo-tarpaulin - name: Generate code coverage run: | RUST_BACKTRACE=1 cargo tarpaulin --engine llvm --verbose --timeout 120 --out Lcov --workspace --all-features diff --git a/Cargo.toml b/Cargo.toml index 3967c08..cb5acf9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,7 +30,7 @@ bevy_text = ["bevy/bevy_text", "bevy/bevy_render", "bevy/bevy_sprite"] [dependencies] # Note: abuse 'bevy_color' to force 'bevy_math/curve' feature, which defines EaseFunction -bevy = { version = "0.18", default-features = false, features = [ +bevy = { version = "0.19", default-features = false, features = [ "bevy_color", "bevy_asset", "bevy_log", @@ -38,7 +38,7 @@ bevy = { version = "0.18", default-features = false, features = [ thiserror = "2" [dev-dependencies] -bevy-inspector-egui = { version = "0.36", default-features = false, features = [ +bevy-inspector-egui = { version = "0.37", default-features = false, features = [ "bevy_render", "bevy_pbr", ] } diff --git a/benchmarks/Cargo.toml b/benchmarks/Cargo.toml index 5cedb30..5074586 100644 --- a/benchmarks/Cargo.toml +++ b/benchmarks/Cargo.toml @@ -18,7 +18,7 @@ criterion = { version = "0.5", features = ["html_reports"] } bevy_tweening = { path = "../" } [dependencies.bevy] -version = "0.18" +version = "0.19" default-features = false features = ["bevy_render", "bevy_sprite", "bevy_text", "bevy_ui"] diff --git a/examples/ambient_light.rs b/examples/ambient_light.rs index 4d094f6..c3db4ec 100644 --- a/examples/ambient_light.rs +++ b/examples/ambient_light.rs @@ -1,30 +1,31 @@ //! Example demonstrating resource animation and various transform shortcuts. //! -//! The example animates the `AmbientLight` resource of Bevy's PBR renderer. -//! This is mostly for example purpose; you probably want to animate some other -//! (custom) resource. It also moves a capsule object back and forth with the -//! `move_to()` command extension, and make it "resonate" by quickly scaling it -//! between 100% and 110% size with the `scale_to()` command extension. +//! The example animates the `GlobalAmbientLight` resource of Bevy's PBR +//! renderer. This is mostly for example purpose; you probably want to animate +//! some other (custom) resource. It also moves a capsule object back and forth +//! with the `move_to()` command extension, and make it "resonate" by quickly +//! scaling it between 100% and 110% size with the `scale_to()` command +//! extension. use std::{ f32::consts::{FRAC_PI_2, FRAC_PI_4}, time::Duration, }; -use bevy::{color::palettes::css::*, post_process::bloom::Bloom, prelude::*, render::view::Hdr}; +use bevy::{camera::Hdr, color::palettes::css::*, post_process::bloom::Bloom, prelude::*}; use bevy_tweening::{lens::*, *}; mod utils; -// Define our own `Lens` to animate the `AmbientLight` resource. +// Define our own `Lens` to animate the `GlobalAmbientLight` resource. struct AmbientLightBrightnessLens { pub start: f32, pub end: f32, } // Implement the `Lens` trait. -impl Lens for AmbientLightBrightnessLens { - fn lerp(&mut self, mut target: Mut, ratio: f32) { +impl Lens for AmbientLightBrightnessLens { + fn lerp(&mut self, mut target: Mut, ratio: f32) { target.brightness = self.start.lerp(self.end, ratio); } } @@ -50,7 +51,7 @@ fn setup( mut commands: Commands, mut meshes: ResMut>, mut materials: ResMut>, - mut ambient_light: ResMut, + mut ambient_light: ResMut, ) -> Result<(), BevyError> { // Some fancy 3D camera with HDR and bloom, to emphasize the change of ambient // brightness. @@ -97,7 +98,7 @@ fn setup( .with_repeat(RepeatCount::Infinite, RepeatStrategy::MirroredRepeat); commands.spawn(( TweenAnim::new(tween), - AnimTarget::resource::(), + AnimTarget::resource::(), )); // Spawn some animated character-like capsule... diff --git a/examples/colormaterial_color.rs b/examples/colormaterial_color.rs index 096387b..c3358c9 100644 --- a/examples/colormaterial_color.rs +++ b/examples/colormaterial_color.rs @@ -1,27 +1,48 @@ use std::time::Duration; use bevy::{color::palettes::css::*, prelude::*}; +use bevy_inspector_egui::{bevy_egui::EguiPlugin, prelude::*, quick::ResourceInspectorPlugin}; use bevy_tweening::{lens::*, *}; mod utils; fn main() { App::default() - .add_plugins(DefaultPlugins.set(WindowPlugin { - primary_window: Some(Window { - title: "ColorMaterialColorLens".to_string(), - resolution: bevy::window::WindowResolution::new(1200, 600), - present_mode: bevy::window::PresentMode::Fifo, // vsync + .add_plugins(( + DefaultPlugins.set(WindowPlugin { + primary_window: Some(Window { + title: "ColorMaterialColorLens".to_string(), + resolution: bevy::window::WindowResolution::new(1200, 600), + present_mode: bevy::window::PresentMode::Fifo, // vsync + ..default() + }), ..default() }), - ..default() - })) + EguiPlugin::default(), + ResourceInspectorPlugin::::new(), + )) + .init_resource::() + .register_type::() .add_systems(Update, utils::close_on_esc) .add_plugins(TweeningPlugin) .add_systems(Startup, setup) + .add_systems(Update, update_animation_speed) .run(); } +#[derive(Resource, Reflect, InspectorOptions)] +#[reflect(InspectorOptions)] +struct Options { + #[inspector(min = 0., max = 100.)] + speed: f64, +} + +impl Default for Options { + fn default() -> Self { + Self { speed: 1. } + } +} + fn setup( mut commands: Commands, mut meshes: ResMut>, @@ -105,3 +126,13 @@ fn setup( Ok(()) } + +fn update_animation_speed(options: Res, mut q_anims: Query<&mut TweenAnim>) { + if !options.is_changed() { + return; + } + + for mut anim in &mut q_anims { + anim.speed = options.speed; + } +} diff --git a/examples/menu.rs b/examples/menu.rs index f90a8f5..3df6089 100644 --- a/examples/menu.rs +++ b/examples/menu.rs @@ -117,12 +117,15 @@ fn setup(mut commands: Commands, asset_server: Res) { children![( Text::new(text.to_string()), TextFont { - font: font.clone(), - font_size: 48.0, + font: font.clone().into(), + font_size: 48.0.into(), ..default() }, TextColor(TEXT_COLOR), - TextLayout::new_with_justify(Justify::Center), + TextLayout { + justify: Justify::Center, + ..default() + }, )], )) .id(); diff --git a/examples/sequence.rs b/examples/sequence.rs index 813ece2..557579c 100644 --- a/examples/sequence.rs +++ b/examples/sequence.rs @@ -43,8 +43,8 @@ fn setup(mut commands: Commands, asset_server: Res) -> Result<()> { let font = asset_server.load("fonts/FiraMono-Regular.ttf"); let text_font = TextFont { - font, - font_size: 50.0, + font: font.into(), + font_size: 50.0.into(), ..default() }; @@ -57,7 +57,10 @@ fn setup(mut commands: Commands, asset_server: Res) -> Result<()> { commands .spawn(( Text2d::default(), - TextLayout::new_with_justify(justify), + TextLayout { + justify, + ..default() + }, Transform::from_translation(Vec3::new(0., 40., 0.)), RedProgress, )) @@ -79,7 +82,10 @@ fn setup(mut commands: Commands, asset_server: Res) -> Result<()> { commands .spawn(( Text2d::default(), - TextLayout::new_with_justify(justify), + TextLayout { + justify, + ..default() + }, Transform::from_translation(Vec3::new(0., -40., 0.)), BlueProgress, )) diff --git a/examples/text_color.rs b/examples/text_color.rs index bb44899..3796cdc 100644 --- a/examples/text_color.rs +++ b/examples/text_color.rs @@ -84,8 +84,8 @@ fn setup(mut commands: Commands, asset_server: Res) { commands.spawn(( Text::new(*ease_name), TextFont { - font: font.clone(), - font_size: 24.0, + font: font.clone().into(), + font_size: 24.0.into(), ..default() }, TextColor(Color::WHITE), diff --git a/src/lens.rs b/src/lens.rs index 9974005..857859c 100644 --- a/src/lens.rs +++ b/src/lens.rs @@ -507,7 +507,10 @@ pub struct ColorMaterialColorLens { #[cfg(feature = "bevy_sprite")] impl Lens for ColorMaterialColorLens { fn lerp(&mut self, mut target: Mut, ratio: f32) { - target.color = self.start.mix(&self.end, ratio); + let color = self.start.mix(&self.end, ratio); + if target.color != color { + target.color = color; + } } } @@ -1150,7 +1153,7 @@ mod tests { let mut added = Tick::new(0); let mut last_changed = Tick::new(0); let mut caller = MaybeLocation::caller(); - let asset = assets.get_mut(handle.id()).unwrap(); + let asset = assets.get_mut_untracked(handle.id()).unwrap(); let target = Mut::new( asset, &mut added, @@ -1167,7 +1170,7 @@ mod tests { let mut added = Tick::new(0); let mut last_changed = Tick::new(0); let mut caller = MaybeLocation::caller(); - let asset = assets.get_mut(handle.id()).unwrap(); + let asset = assets.get_mut_untracked(handle.id()).unwrap(); let target = Mut::new( asset, &mut added, @@ -1184,7 +1187,7 @@ mod tests { let mut added = Tick::new(0); let mut last_changed = Tick::new(0); let mut caller = MaybeLocation::caller(); - let asset = assets.get_mut(handle.id()).unwrap(); + let asset = assets.get_mut_untracked(handle.id()).unwrap(); let target = Mut::new( asset, &mut added, diff --git a/src/lib.rs b/src/lib.rs index b7a3ac1..9f83417 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -299,7 +299,7 @@ use std::{ use bevy::{ asset::UntypedAssetId, ecs::{ - change_detection::MutUntyped, + change_detection::{MaybeLocation, MutUntyped, Tick}, component::{ComponentId, Components, Mutable}, }, platform::collections::HashMap, @@ -915,6 +915,7 @@ pub(crate) struct MoveToCommand { } impl EntityCommand for MoveToCommand { + type Out = (); fn apply(self, mut entity: EntityWorldMut) { if let Some(start) = entity.get::().map(|tr| tr.translation) { let lens = TransformPositionLens { @@ -950,6 +951,7 @@ pub(crate) struct MoveFromCommand { } impl EntityCommand for MoveFromCommand { + type Out = (); fn apply(self, mut entity: EntityWorldMut) { if let Some(end) = entity.get::().map(|tr| tr.translation) { let lens = TransformPositionLens { @@ -985,6 +987,7 @@ pub(crate) struct ScaleToCommand { } impl EntityCommand for ScaleToCommand { + type Out = (); fn apply(self, mut entity: EntityWorldMut) { if let Some(start) = entity.get::().map(|tr| tr.scale) { let lens = TransformScaleLens { @@ -1020,6 +1023,7 @@ pub(crate) struct ScaleFromCommand { } impl EntityCommand for ScaleFromCommand { + type Out = (); fn apply(self, mut entity: EntityWorldMut) { if let Some(end) = entity.get::().map(|tr| tr.scale) { let lens = TransformScaleLens { @@ -1054,6 +1058,7 @@ pub(crate) struct RotateXCommand { } impl EntityCommand for RotateXCommand { + type Out = (); fn apply(self, mut entity: EntityWorldMut) { if let Some(base_rotation) = entity.get::().map(|tr| tr.rotation) { let lens = TransformRotateAdditiveXLens { @@ -1090,6 +1095,7 @@ pub(crate) struct RotateYCommand { } impl EntityCommand for RotateYCommand { + type Out = (); fn apply(self, mut entity: EntityWorldMut) { if let Some(base_rotation) = entity.get::().map(|tr| tr.rotation) { let lens = TransformRotateAdditiveYLens { @@ -1126,6 +1132,7 @@ pub(crate) struct RotateZCommand { } impl EntityCommand for RotateZCommand { + type Out = (); fn apply(self, mut entity: EntityWorldMut) { if let Some(base_rotation) = entity.get::().map(|tr| tr.rotation) { let lens = TransformRotateAdditiveZLens { @@ -1163,6 +1170,7 @@ pub(crate) struct RotateXByCommand { } impl EntityCommand for RotateXByCommand { + type Out = (); fn apply(self, mut entity: EntityWorldMut) { if let Some(base_rotation) = entity.get::().map(|tr| tr.rotation) { let lens = TransformRotateAdditiveXLens { @@ -1199,6 +1207,7 @@ pub(crate) struct RotateYByCommand { } impl EntityCommand for RotateYByCommand { + type Out = (); fn apply(self, mut entity: EntityWorldMut) { if let Some(base_rotation) = entity.get::().map(|tr| tr.rotation) { let lens = TransformRotateAdditiveYLens { @@ -1235,6 +1244,7 @@ pub(crate) struct RotateZByCommand { } impl EntityCommand for RotateZByCommand { + type Out = (); fn apply(self, mut entity: EntityWorldMut) { if let Some(base_rotation) = entity.get::().map(|tr| tr.rotation) { let lens = TransformRotateAdditiveZLens { @@ -2200,10 +2210,10 @@ impl TweenAnim { .get_id(type_id) .ok_or(TweeningError::ComponentNotRegistered(type_id))?, AnimTargetKind::Resource => components - .get_resource_id(type_id) + .get_id(type_id) .ok_or(TweeningError::ResourceNotRegistered(type_id))?, AnimTargetKind::Asset { assets_type_id, .. } => components - .get_resource_id(*assets_type_id) + .get_id(*assets_type_id) .ok_or(TweeningError::AssetNotRegistered(type_id))?, }; let is_retargetable = false; // explicit target @@ -2622,7 +2632,7 @@ pub struct TweenResolver { impl TweenResolver { /// Register a resolver for the given resource type. pub(crate) fn register_resource_resolver_for(&mut self, components: &Components) { - let resource_id = components.resource_id::().unwrap(); + let resource_id = components.component_id::().unwrap(); let resolver = |world: &mut World, entity: Entity, target_type_id: &TypeId, @@ -2669,7 +2679,7 @@ impl TweenResolver { /// Register a resolver for the given asset type. pub(crate) fn register_asset_resolver_for(&mut self, components: &Components) { - let resource_id = components.resource_id::>().unwrap(); + let resource_id = components.component_id::>().unwrap(); let resolver = |world: &mut World, asset_id: UntypedAssetId, entity: Entity, @@ -2681,9 +2691,13 @@ impl TweenResolver { let asset_id = asset_id.typed::(); // First, remove the Assets from the world so we can access it mutably in // parallel of the TweenAnim - world.resource_scope(|world, assets: Mut>| { - // Next, fetch the asset A itself from its Assets based on its asset ID - let Some(asset) = assets.filter_map_unchanged(|assets| assets.get_mut(asset_id)) + world.resource_scope(|world, mut assets: Mut>| { + // We abuse the fact that Assets is changed every single frame by the asset_events() + // system, and assume that the current tick is therefore equal to the last time + // Assets was changed. + let this_tick = assets.last_changed().get(); + + let Some(mut asset_mut) = assets.get_mut(asset_id) else { return Err(TweeningError::InvalidAssetId(asset_id.into())); }; @@ -2703,17 +2717,39 @@ impl TweenResolver { return Err(TweeningError::MissingTweenAnim(ent.id())); }; + // Create a fake Mut which is always unchanged before the anim steps. + // Its sole purpose is to know if the Lens::lerp() changed the asset. + // Ideally we'd directly use AssetMut<> but the interface doesn't match. + let mut added = Tick::MAX; // hopefully unused... + let last_tick = this_tick.saturating_sub(1); + let mut last_changed = Tick::new(last_tick); + let last_run = last_changed; + let this_run = Tick::new(this_tick); + let mut caller = MaybeLocation::caller(); + let typed_mut = Mut::new(asset_mut.bypass_change_detection(), &mut added, &mut last_changed, last_run, this_run, caller.as_mut()); + assert!(!typed_mut.is_changed()); + let mut mut_untyped: MutUntyped = typed_mut.into(); + // Finally, step the TweenAnim and mutate the target let ret = anim.step_self( commands, entity, delta_time, &target, - asset.into(), + mut_untyped.reborrow(), target_type_id, cycle_events.reborrow(), anim_events.reborrow(), ); + + // If the asset actually changed (as reported by Mut<>), mark it as such + // through Assets (which will send the asset modified message). + if mut_untyped.is_changed() { + // into_inner() marks the asset as changed, which triggers the regular + // asset modified flow (emits AssetEvents::Modified). + let _ = asset_mut.into_inner(); + } + ret.map(|result| { assert!(!result.needs_retarget, "Cannot use a multi-target sequence of tweenable animations with an asset target."); result.retain @@ -2916,7 +2952,7 @@ mod tests { let mut added = Tick::new(0); let mut last_changed = Tick::new(0); let mut caller = MaybeLocation::caller(); - let asset = assets.get_mut(handle.id()).unwrap(); + let asset = assets.get_mut_untracked(handle.id()).unwrap(); let target = Mut::new( asset, &mut added, @@ -3800,7 +3836,7 @@ mod tests { env.world.flush(); let delta_time = Duration::from_millis(200); - let resource_id = env.world.resource_id::().unwrap(); + let resource_id = env.world.component_id::().unwrap(); // Resource resolver not registered; fails env.world @@ -3882,7 +3918,7 @@ mod tests { env.world.flush(); let delta_time = Duration::from_millis(200); - let resource_id = env.world.resource_id::>().unwrap(); + let resource_id = env.world.component_id::>().unwrap(); // Asset resolver not registered; fails env.world diff --git a/src/tweenable.rs b/src/tweenable.rs index d4c5d34..3cb804a 100644 --- a/src/tweenable.rs +++ b/src/tweenable.rs @@ -1933,8 +1933,8 @@ mod tests { // Messages are only sent when playing forward if playback_direction.is_forward() { - //let component_id = world.component_id::().unwrap(); - let mut event_reader = event_reader_system_state.get_mut(&mut world); + let mut event_reader = + event_reader_system_state.get_mut(&mut world).unwrap(); let event = event_reader.read().next(); if just_completed { assert!(event.is_some());