Closed musjj closed 4 months ago
Composing multiple tweeners is something I've thought of before but because this "operation" is very variadic by how many ways users may want to compose tweeners:
So I decided to leave this up to the users.
I wouldn't want to include any extra stuff to the current SpanTweener
component where its only responsibility is to control their span tweens. So we're left with making additional components which I'm assuming is the solution you've had. (That's great! The crate I'm intended to be extendable is being extended. I wouldn't call this manually reading events too because that's what you need to use to extend the crate) I'd too like a way to make this possible with the builder API but since optional component in a bundle is not possible right now, its work around would be creating more bundle for each variant which I don't think anyone like it. So we're left with using the .spawn()
where this code below is the one I'm assuming you're using to compose tweens.
let prev = parent
.spawn(
SpanTweenerBundle::new(Duration::from_secs_f32(2.0))
.with_repeat(Repeat::times(2))
.with_repeat_style(RepeatStyle::PingPong),
)
.id();
parent.spawn((
PlayAfter(prev),
SpanTweenerBundle::new(Duration::from_secs_f32(2.0))
.with_repeat(Repeat::times(2))
.with_repeat_style(RepeatStyle::PingPong)
.with_paused(true),
));
From my perspective, this looks just fine. To run a one-shot system too is simply
parent.spawn((
OnCompleted(/* a system closure */), // run a closure system once after completed
OnCompletedId(/* a system id */), // run a system via id after completed
PlayAfter(prev),
SpanTweenerBundle::new(Duration::from_secs_f32(2.0))
.with_repeat(Repeat::times(2))
.with_repeat_style(RepeatStyle::PingPong)
.with_paused(true),
));
Already using the .spawn()
API to compose components seems to me like already a perfect fit for composing tweeners, we can add as many conditions and everything else through here without extending the builder API.
I've actually tried something like this before, but I found that if I add two tweeners that targets the same component as a child of an entity, they will interrupt each other. Even when the other one is paused. Do you have any solutions for this kind of use case?
Interesting, do you mind giving those tweeners declaration? It shouldn't interrupt each other if I understand the systems correctly.
Here's an example:
commands
.spawn(MaterialMesh2dBundle {
mesh: meshes.add(Rectangle::from_size(Vec2::new(5.0, 5.0))).into(),
transform: Transform::default(),
material: materials.add(Color::PURPLE),
..default()
})
.with_children(|parent| {
parent
.spawn(SpanTweenerBundle::new(Duration::from_secs_f32(2.0)))
.with_children(|parent| {
parent.span_tweens().tween(
Duration::from_secs_f32(2.0),
EaseFunction::Linear,
ComponentTween::tweener_parent(interpolate::Scale {
start: Vec3::splat(1.0),
end: Vec3::new(8.0, 1.0, 1.0),
}),
);
});
parent
.spawn(SpanTweenerBundle::new(Duration::from_secs_f32(2.0)).with_paused(true))
.with_children(|parent| {
parent.span_tweens().tween(
Duration::from_secs_f32(2.0),
EaseFunction::Linear,
ComponentTween::tweener_parent(interpolate::Scale {
start: Vec3::splat(1.0),
end: Vec3::new(1.0, 8.0, 1.0),
}),
);
});
});
The first tweener won't play unless the second one is removed.
Ah I think I remember now. with_paused
will only pause the timer from ticking but not a tweener from running. Can you try SkipTweener
component?
Thanks, that works for me! I guess I'll close this issue.
Additional info: you need to set both with_paused
AND SkipTweener
or your tween might have finished "playing" before you even removed the SkipTweener
component. This was driving me nuts :sweat_smile:.
Oops, I'll make sure this is properly documented in the next release.😂 These are intentionally separated to support for an editor in the future.
Another question, can you tell my why isn't this working? I was trying to reproduce a bug, but I ended up hitting a different one :sob:
use bevy::{prelude::*, sprite::MaterialMesh2dBundle};
use bevy_tween::{prelude::*, span_tween::SpanTweener, tween::SkipTweener};
fn main() {
App::new()
.add_plugins((DefaultPlugins, DefaultTweenPlugins))
.add_systems(Startup, setup)
.add_systems(Update, transition)
.run();
}
#[derive(Component)]
pub struct First;
#[derive(Component)]
pub struct Second;
fn transition(
mut commands: Commands,
mut ended_reader: EventReader<SpanTweenerEnded>,
mut first: Query<&mut SpanTweener, With<First>>,
mut second: Query<
(Entity, &mut SpanTweener),
(Without<First>, With<Second>, With<SkipTweener>),
>,
) {
for ended in ended_reader.read() {
if ended.is_completed() {
if let Ok(mut tweener) = first.get_mut(ended.tweener) {
tweener.timer.set_paused(true);
commands.entity(ended.tweener).insert(SkipTweener);
let (entity, mut tweener) = second.single_mut();
commands.entity(entity).remove::<SkipTweener>();
tweener.timer.set_paused(false);
}
}
}
}
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<ColorMaterial>>,
) {
commands.spawn(Camera2dBundle::default());
commands
.spawn(MaterialMesh2dBundle {
mesh: meshes
.add(Rectangle::from_size(Vec2::new(100.0, 100.0)))
.into(),
material: materials.add(Color::PURPLE),
..default()
})
.with_children(|parent| {
parent
.spawn((First, SpanTweenerBundle::new(Duration::from_secs_f32(2.0))))
.with_children(|parent| {
parent.span_tweens().tween_exact(
..Duration::from_secs_f32(2.0),
EaseFunction::Linear,
ComponentTween::tweener_parent(interpolate::Scale {
start: Vec3::splat(1.0),
end: Vec3::new(3.0, 1.0, 1.0),
}),
);
});
parent
.spawn((
Second,
SpanTweenerBundle::new(Duration::from_secs_f32(2.0)).with_paused(true),
SkipTweener,
))
.with_children(|parent| {
parent.span_tweens().tween_exact(
..Duration::from_secs_f32(1.2),
EaseFunction::Linear,
ComponentTween::tweener_parent(interpolate::Scale {
start: Vec3::new(1.0, 8.0, 1.0),
end: Vec3::new(1.0, 1.0, 1.0),
}),
);
});
});
}
First
gets instantly skipped because a SpanTweenerEnded
event was instantly sent for the First
entity...
The bug goes away if you remove the transition
system.
First
gets instantly skipped because aSpanTweenerEnded
event was instantly sent for theFirst
entity...
Bug found.
tick_span_tweener_system
doesn't account for timer direction so if elasped time is 0 then the event will be fired.
Thanks!
Should be fixed by bd6841c
Additionally, I have to add apply_deferred
after the transition system to add the SkipTweener
right away.
Thank you :sob:! But now I can finally get to the main bug I was dealing with.
In the snippet above, I'm expecting a square stretching horizontally in the First
animation. Then a tall rectangle squeezing vertically back to a square in the Second
animation.
But after the Second
animation is completed, the square becomes a horizontally stretched rectangle, identical with the final frame of the First
animation despite it being paused.
Can you reproduce this? (no changes to snippet above is required).
@musjj I've fixed that by adding
.add_systems(Update, (transition, apply_deferred).chain())
I tried it, but the final frame is still a wide rectangle instead of a square, weird. The order of the transition
system shouldn't matter because it only reacts to the First
animation completing.
Oh wait I missed that last part. Yeah, something fishy going on.
Not exactly sure why but having your second tween have shorter duration than the tweener causes the problem.
Yeah, the tween's duration being shorter than the tweener's duration was one of the conditions for the bug.
My real use case was that I was doing several parallel tweens. When one of the tweens is shorter than the tweener, this bug happens.
How does one even debug this 😭. I'm going to give system stepper a try. Never used it before. Edit: No need for it. Just added a name component then print em out when they ran.
@musjj There's a bug in tweener's system that didn't remove TweenProgress
(It's used as a marker to make tweens running) from their tweens when SkipTweener
component is added or timer is completed. I'll get started on fixing this.
Usually I remove all my tweeners whenever they're done and only add my tweeners whenever they're needed if possible you might want to consider those.
Yeah, thanks despawning the tweener when it's done works as a workaround for now!
Fixed in 3b858d9 Thanks!
Thank you, that fixed it!
You can already compose tweens, but playing a linear sequence of tweeners require you to manually read events and juggle marker components.
What if you can compose tweeners too? So for example, let's say you want to play these tweeners, one right after the other:
Right now, you need to do this manually, but it would be nice if there's an API that does it for you.
This is roughly how I'd imagine it to look like:
Another flexible option would be the ability to attach callbacks that is run after the tweener is completed:
This will also allow you to run clean up operations after the tween is completed, without having to use marker components. Though for the purpose of constructing sequences, this will quickly turn into nesting hell.
What do you think?