Crate bevy_tween

source ·
Expand description

§Getting started

DefaultTweenPlugins provide most the stuff you will need. Add the plugin to your app:

use bevy::prelude::*;
use bevy_tween::prelude::*;

fn main() {
    App::default()
        .add_plugins((DefaultPlugins, DefaultTweenPlugins))
        .run();
}

§Usages

§Creating animation

Let say we have this sprite:

let mut sprite_commands = commands.spawn(SpriteBundle {
    sprite: Sprite {
        custom_size: Some(Vec2::new(50., 50.)),
        ..Default::default()
    },
    ..Default::default()
});

Now we want to animate it, maybe with one simple tween animation.

§insert_tween_here method

We can do this:

use bevy_tween::{interpolate::sprite_color, combinator::tween};

let sprite = sprite_commands.id().into_target();
sprite_commands.animation().insert_tween_here(
    Duration::from_secs(1),
    EaseFunction::QuadraticOut,
    sprite.with(sprite_color(Color::WHITE, Color::RED))
    // Since this argument accepts a bundle, you can add additional tween to this like so:
    // (
    //     sprite.with(sprite_color(Color::WHITE, Color::RED)),
    //     sprite.with(something_something(...)),
    // )
);

This insert every animation components directly into your entity. Use this if you wish to keep your entity structure simple (no children) and doesn’t need complicated animation.

§Combinator framework

Now what if we want to chain animations?

This crate provide a way to create animation using combinator framework. So we can just:

use bevy_tween::{
    interpolate::sprite_color,
    combinator::{tween, sequence}
};

let sprite = sprite_commands.id().into_target();
sprite_commands.animation().insert(sequence((
    tween(
        Duration::from_secs(1),
        EaseFunction::QuadraticOut,
        sprite.with(sprite_color(Color::WHITE, Color::RED))
    ),
    tween(
        Duration::from_secs(1),
        EaseFunction::QuadraticIn,
        sprite.with(sprite_color(Color::RED, Color::WHITE))
    ),
)));

This adds one TimeRunner to your entity and 2 Tween entities as the child.

§State

There’s a little bit of boilerplate we can improve. Currently we’ve specified Color::RED 2 times because we want our tween to continue from previous value.

We can use state for this:

use bevy_tween::{
    interpolate::sprite_color_to,
    combinator::{tween, sequence}
};

let sprite = sprite_commands.id().into_target();
let mut sprite_color = sprite.state(Color::WHITE); // We want the intial color to be white
sprite_commands.animation().insert(sequence((
    tween(
        Duration::from_secs(1),
        EaseFunction::QuadraticOut,
        // Switch the constructor to the relative variant
        sprite_color.with(sprite_color_to(Color::RED))
    ),
    tween(
        Duration::from_secs(1),
        EaseFunction::QuadraticIn,
        sprite_color.with(sprite_color_to(Color::WHITE))
    ),
)));

Looks good!

§Repeating

If we want to repeat our animation to so we can do:

use bevy_tween::{
    interpolate::sprite_color_to,
    combinator::{tween, sequence}
};

let sprite = sprite_commands.id().into_target();
let mut sprite_color = sprite.state(Color::WHITE);
sprite_commands.animation()
    .repeat(Repeat::Infinitely) // Add repeat
    .insert(sequence((
        tween(
            Duration::from_secs(1),
            EaseFunction::QuadraticOut,
            sprite_color.with(sprite_color_to(Color::RED))
        ),
        tween(
            Duration::from_secs(1),
            EaseFunction::QuadraticIn,
            sprite_color.with(sprite_color_to(Color::WHITE))
        ),
    )));

§Custom combinator

What if you want to abstract animation?

  • To manage large animation code
  • To reuse animation code
  • Custom combinators

Combinator framework got you covered!:

use bevy_tween::{
    interpolate::sprite_color_to,
    combinator::{AnimationCommands, TargetState, tween, sequence},
    tween::TargetComponent,
};

// Create new combinator
fn my_animation(
    // You can use `TargetComponent` if you doesn't use state.
    target_sprite_color: &mut TargetState<TargetComponent, Color>,
    duration: Duration
) -> impl FnOnce(&mut AnimationCommands, &mut Duration) {
    sequence((
        tween(
            duration,
            EaseFunction::QuadraticOut,
            target_sprite_color.with(sprite_color_to(Color::RED))
        ),
        tween(
            duration,
            EaseFunction::QuadraticIn,
            target_sprite_color.with(sprite_color_to(Color::WHITE))
        ),
    ))
}

let sprite = sprite_commands.id().into_target();
let mut sprite_color = sprite.state(Color::WHITE);
sprite_commands.animation()
    .repeat(Repeat::Infinitely)
    .insert(my_animation(&mut sprite_color, Duration::from_secs(1)));

There are more combinators you can use. Check them out in the combinator module.

§Animator as a child

You can spawn animator as a child if you want

use bevy_tween::{
    interpolate::sprite_color,
    combinator::{tween, sequence}
};

let sprite = sprite_commands.id().into_target();
sprite_commands.with_children(|c| {
    c.animation().insert(sequence((
        tween(
            Duration::from_secs(1),
            EaseFunction::QuadraticOut,
            sprite.with(sprite_color(Color::WHITE, Color::RED))
        ),
        tween(
            Duration::from_secs(1),
            EaseFunction::QuadraticIn,
            sprite.with(sprite_color(Color::RED, Color::WHITE))
        ),
    )));
});

§AnimationTarget

AnimationTarget can be used for automatic target searching.

use bevy_tween::{
    interpolate::sprite_color,
    combinator::tween,
    tween::AnimationTarget,
};

let sprite = AnimationTarget.into_target(); // which returns TargetComponent::Marker
sprite_commands.insert(AnimationTarget);
sprite_commands.animation().insert_tween_here(
    Duration::from_secs(1),
    EaseFunction::QuadraticOut,
    sprite.with(sprite_color(Color::WHITE, Color::RED))
);

§Custom interpolator

See these documentations for more details:

This example shows how to create your own inteprolator.

interpolator example

use bevy::{prelude::*, time::Stopwatch};
use bevy_tween::{
    combinator::{parallel, tween},
    prelude::*,
};

#[derive(Component)]
pub struct Circle {
    radius: f32,
    hue: f32,
    spikiness: f32,
}

mod interpolate {
    use super::Circle;
    use bevy::prelude::*;
    use bevy_tween::{
        component_dyn_tween_system, component_tween_system, prelude::*,
    };

    pub fn interpolators_plugin(app: &mut App) {
        app.add_tween_systems((
            component_dyn_tween_system::<Circle>(),
            component_tween_system::<CircleRadius>(),
            component_tween_system::<CircleHue>(),
        ));
    }

    pub struct CircleRadius {
        start: f32,
        end: f32,
    }

    impl Interpolator for CircleRadius {
        type Item = Circle;

        fn interpolate(&self, item: &mut Self::Item, value: f32) {
            item.radius = self.start.lerp(self.end, value);
        }
    }

    pub fn circle_radius(start: f32, end: f32) -> CircleRadius {
        CircleRadius { start, end }
    }

    pub struct CircleHue {
        start: f32,
        end: f32,
    }

    impl Interpolator for CircleHue {
        type Item = Circle;

        fn interpolate(&self, item: &mut Self::Item, value: f32) {
            item.hue = self.start.lerp(self.end, value);
        }
    }

    pub fn circle_hue(start: f32, end: f32) -> CircleHue {
        CircleHue { start, end }
    }
}
use interpolate::{circle_hue, circle_radius};

fn main() {
    App::new()
        .add_plugins((
            MinimalPlugins,
            DefaultTweenPlugins,
            interpolate::interpolators_plugin,
        ))
        .add_systems(Startup, setup)
        .add_systems(Update, what_happen)
        .run();
}

fn setup(mut commands: Commands) {
    let circle_id = commands
        .spawn(Circle {
            radius: 1.,
            hue: 0.,
            spikiness: 2.,
        })
        .id();

    let circle = circle_id.into_target();
    commands.animation().insert(parallel((
        tween(
            Duration::from_secs(2),
            EaseFunction::Linear,
            circle.with(circle_hue(0., 10.)),
        ),
        tween(
            Duration::from_secs(2),
            EaseFunction::Linear,
            circle.with(circle_radius(1., 50.)),
        ),
        tween(
            Duration::from_secs(2),
            EaseFunction::Linear,
            // Requires [`component_dyn_tween_system`]
            circle.with_closure(|circle: &mut Circle, value| {
                circle.spikiness = (2.).lerp(4., value);
            }),
        ),
    )));
}

fn what_happen(
    time: Res<Time>,
    q_circle: Query<&Circle>,
    mut time_passed: Local<Stopwatch>,
    mut print_tick: Local<Stopwatch>,
) {
    time_passed.tick(time.delta());
    print_tick.tick(time.delta());
    if time_passed.elapsed_secs() < 3. && print_tick.elapsed_secs() > 0.2 {
        let circle = q_circle.single();
        println!(
            "{:.2} {:.2} {:.2}",
            circle.hue, circle.radius, circle.spikiness
        );
        print_tick.reset();
    }
}

§Entity structure

This example shows what’s actually going on under the hood within this crate’s API.

entity_structure example

Run cargo run --example entity_structure to see this in action.

use bevy::prelude::*;
use bevy_tween::interpolate::{AngleZ, Translation};
use bevy_tween::prelude::*;
use bevy_tween::{
    bevy_time_runner::{TimeRunner, TimeSpan},
    tween::{AnimationTarget, TargetComponent},
};

fn main() {
    App::new()
        .add_plugins((DefaultPlugins, DefaultTweenPlugins))
        .add_systems(Startup, setup)
        .run();
}

fn sprite(start_x: f32, start_y: f32) -> SpriteBundle {
    SpriteBundle {
        sprite: Sprite {
            custom_size: Some(Vec2::new(50., 50.)),
            color: Color::WHITE,
            ..Default::default()
        },
        transform: Transform::from_xyz(start_x, start_y, 0.),
        ..Default::default()
    }
}

/// This show all the possible structure you can use.
/// All of these result in exactly the same animation!
/// Just use what fit for your use case.
///
/// These will be presented in its most rawest form.
/// See other examples for better APIs.
fn setup(mut commands: Commands) {
    commands.spawn(Camera2dBundle::default());

    let angle_start = 0.;
    let angle_end = std::f32::consts::PI * 2.;

    let start_x = -300.;
    let end_x = 300.;

    let spacing_y = 100.;
    let offset_y = -(spacing_y * 3.) / 2.;

    // Everything in the same entity
    let y = 0. * spacing_y + offset_y;
    commands.spawn((
        sprite(start_x, y),
        AnimationTarget,
        TimeRunner::new(Duration::from_secs(5)),
        TimeSpan::try_from(..Duration::from_secs(5)).unwrap(),
        EaseFunction::QuadraticInOut,
        ComponentTween::new_target(
            TargetComponent::marker(),
            Translation {
                start: Vec3::new(start_x, y, 0.),
                end: Vec3::new(end_x, y, 0.),
            },
        ),
        ComponentTween::new_target(
            TargetComponent::marker(),
            AngleZ {
                start: angle_start,
                end: angle_end,
            },
        ),
    ));
    // equivalent to
    //
    // let target = TargetComponent::marker();
    // commands.spawn((sprite(...), AnimationTarget)).animate()
    //     .insert_here(
    //         Duration::from_secs(5),
    //         EaseFunction::QuadraticOut,
    //         (
    //             target.with(translation(...)),
    //             target.with(angle_z(...)),
    //         ),
    //     );

    // Sprite and time runner as parent, tweens as children.
    let y = 1. * spacing_y + offset_y;
    commands
        .spawn((
            sprite(start_x, y),
            AnimationTarget,
            TimeRunner::new(Duration::from_secs(5)),
        ))
        .with_children(|c| {
            c.spawn((
                TimeSpan::try_from(..Duration::from_secs(5)).unwrap(),
                EaseFunction::QuadraticInOut,
                ComponentTween::new_target(
                    TargetComponent::marker(),
                    Translation {
                        start: Vec3::new(start_x, y, 0.),
                        end: Vec3::new(end_x, y, 0.),
                    },
                ),
                ComponentTween::new_target(
                    TargetComponent::marker(),
                    AngleZ {
                        start: angle_start,
                        end: angle_end,
                    },
                ),
            ));
        });
    // equivalent to
    //
    // let target = TargetComponent::marker();
    // commands.spawn((sprite(...), AnimationTarget))
    //     .animate()
    //     .insert(tween(
    //         Duration::from_secs(5),
    //         EaseFunction::QuadraticOut,
    //         (
    //             target.with(translation(...)),
    //             target.with(angle_z(...)),
    //         ),
    //     ));

    // Only Sprite as parent, time runner and tweens as children.
    let y = 2. * spacing_y + offset_y;
    commands
        .spawn((sprite(start_x, y), AnimationTarget))
        .with_children(|c| {
            c.spawn((
                TimeRunner::new(Duration::from_secs(5)),
                TimeSpan::try_from(..Duration::from_secs(5)).unwrap(),
                EaseFunction::QuadraticInOut,
                ComponentTween::new_target(
                    TargetComponent::marker(),
                    Translation {
                        start: Vec3::new(start_x, y, 0.),
                        end: Vec3::new(end_x, y, 0.),
                    },
                ),
                ComponentTween::new_target(
                    TargetComponent::marker(),
                    AngleZ {
                        start: angle_start,
                        end: angle_end,
                    },
                ),
            ));
        });
    // equivalent to
    //
    // let target = TargetComponent::marker();
    // commands.spawn((sprite(...), AnimationTarget)).with_children(|c| {
    //     c.animate().insert_here(
    //         Duration::from_secs(5),
    //         EaseFunction::QuadraticOut,
    //         (
    //             target.with(translation(...)),
    //             target.with(angle_z(...))
    //         ),
    //     );
    // });

    // Only Sprite as parent, tweens as children of a time runner.
    let y = 3. * spacing_y + offset_y;
    commands
        .spawn((sprite(start_x, y), AnimationTarget))
        .with_children(|c| {
            c.spawn(TimeRunner::new(Duration::from_secs(5)))
                .with_children(|c| {
                    c.spawn((
                        TimeSpan::try_from(..Duration::from_secs(5)).unwrap(),
                        EaseFunction::QuadraticInOut,
                        ComponentTween::new_target(
                            TargetComponent::marker(),
                            Translation {
                                start: Vec3::new(start_x, y, 0.),
                                end: Vec3::new(end_x, y, 0.),
                            },
                        ),
                        ComponentTween::new_target(
                            TargetComponent::marker(),
                            AngleZ {
                                start: angle_start,
                                end: angle_end,
                            },
                        ),
                    ));
                });
        });
    // equivalent to
    //
    // let target = TargetComponent::marker();
    // commands.spawn((sprite(...), AnimationTarget)).with_children(|c| {
    //     c.animate().insert(tween(
    //         Duration::from_secs(5),
    //         EaseFunction::QuadraticOut,
    //         (
    //             target.with(translation(...)),
    //             target.with(angle_z(...))
    //         ),
    //     ));
    // });

    // or with this completely detached
    let y = 4. * spacing_y + offset_y;

    let sprite = commands.spawn(sprite(start_x, y)).id();

    commands
        .spawn(TimeRunner::new(Duration::from_secs(5)))
        .with_children(|c| {
            c.spawn((
                TimeSpan::try_from(..Duration::from_secs(5)).unwrap(),
                EaseFunction::QuadraticInOut,
                ComponentTween::new_target(
                    sprite,
                    Translation {
                        start: Vec3::new(start_x, y, 0.),
                        end: Vec3::new(end_x, y, 0.),
                    },
                ),
                ComponentTween::new_target(
                    sprite,
                    AngleZ {
                        start: angle_start,
                        end: angle_end,
                    },
                ),
            ));
        });
    // equivalent to
    //
    // let target = TargetComponent::entity(sprite);
    // commands.animate().insert(tween(
    //     Duration::from_secs(5),
    //     EaseFunction::QuadraticOut,
    //     (
    //         target.with(translation(...)),
    //         target.with(angle_z(...))
    //     ),
    // ));
}

Re-exports§

Modules§

  • Combinator framework
  • Module containing some basic built-in interpolator
  • Module containing ease functions and related systems.
  • Commonly used items
  • Module containing implementations for tween

Structs§

Enums§

Traits§

  • Helper trait to add systems by this crate to your app and avoid mistake from forgetting to use the intended schedule and set.