docs/bevy/examples/2d/sprite_animation.rs 5 K · 145 lines · raw · history

1 //! Animates a sprite in response to a keyboard event.
2 //!
3 //! See `sprite_sheet.rs` for an example where the sprite animation loops indefinitely.
4
5 use std::time::Duration;
6
7 use bevy::{input::common_conditions::input_just_pressed, prelude::*};
8
9 fn main() {
10 App::new()
11 .add_plugins(DefaultPlugins.set(ImagePlugin::default_nearest())) // prevents blurry sprites
12 .add_systems(Startup, setup)
13 .add_systems(Update, execute_animations)
14 .add_systems(
15 Update,
16 (
17 // Press the right arrow key to animate the right sprite
18 trigger_animation::<RightSprite>.run_if(input_just_pressed(KeyCode::ArrowRight)),
19 // Press the left arrow key to animate the left sprite
20 trigger_animation::<LeftSprite>.run_if(input_just_pressed(KeyCode::ArrowLeft)),
21 ),
22 )
23 .run();
24 }
25
26 // This system runs when the user clicks the left arrow key or right arrow key
27 fn trigger_animation<S: Component>(mut animation: Single<&mut AnimationConfig, With<S>>) {
28 // We create a new timer when the animation is triggered
29 animation.frame_timer = AnimationConfig::timer_from_fps(animation.fps);
30 }
31
32 #[derive(Component)]
33 struct AnimationConfig {
34 first_sprite_index: usize,
35 last_sprite_index: usize,
36 fps: u8,
37 frame_timer: Timer,
38 }
39
40 impl AnimationConfig {
41 fn new(first: usize, last: usize, fps: u8) -> Self {
42 Self {
43 first_sprite_index: first,
44 last_sprite_index: last,
45 fps,
46 frame_timer: Self::timer_from_fps(fps),
47 }
48 }
49
50 fn timer_from_fps(fps: u8) -> Timer {
51 Timer::new(Duration::from_secs_f32(1.0 / (fps as f32)), TimerMode::Once)
52 }
53 }
54
55 // This system loops through all the sprites in the `TextureAtlas`, from `first_sprite_index` to
56 // `last_sprite_index` (both defined in `AnimationConfig`).
57 fn execute_animations(time: Res<Time>, mut query: Query<(&mut AnimationConfig, &mut Sprite)>) {
58 for (mut config, mut sprite) in &mut query {
59 // We track how long the current sprite has been displayed for
60 config.frame_timer.tick(time.delta());
61
62 // If it has been displayed for the user-defined amount of time (fps)...
63 if config.frame_timer.just_finished()
64 && let Some(atlas) = &mut sprite.texture_atlas
65 {
66 if atlas.index == config.last_sprite_index {
67 // ...and it IS the last frame, then we move back to the first frame and stop.
68 atlas.index = config.first_sprite_index;
69 } else {
70 // ...and it is NOT the last frame, then we move to the next frame...
71 atlas.index += 1;
72 // ...and reset the frame timer to start counting all over again
73 config.frame_timer = AnimationConfig::timer_from_fps(config.fps);
74 }
75 }
76 }
77 }
78
79 #[derive(Component)]
80 struct LeftSprite;
81
82 #[derive(Component)]
83 struct RightSprite;
84
85 fn setup(
86 mut commands: Commands,
87 asset_server: Res<AssetServer>,
88 mut texture_atlas_layouts: ResMut<Assets<TextureAtlasLayout>>,
89 ) {
90 commands.spawn(Camera2d);
91
92 // Create a minimal UI explaining how to interact with the example
93 commands.spawn((
94 Text::new("Left Arrow: Animate Left Sprite\nRight Arrow: Animate Right Sprite"),
95 Node {
96 position_type: PositionType::Absolute,
97 top: px(12),
98 left: px(12),
99 ..default()
100 },
101 ));
102
103 // Load the sprite sheet using the `AssetServer`
104 let texture = asset_server.load("textures/rpg/chars/gabe/gabe-idle-run.png");
105
106 // The sprite sheet has 7 sprites arranged in a row, and they are all 24px x 24px
107 let layout = TextureAtlasLayout::from_grid(UVec2::splat(24), 7, 1, None, None);
108 let texture_atlas_layout = texture_atlas_layouts.add(layout);
109
110 // The first (left-hand) sprite runs at 10 FPS
111 let animation_config_1 = AnimationConfig::new(1, 6, 10);
112
113 // Create the first (left-hand) sprite
114 commands.spawn((
115 Sprite {
116 image: texture.clone(),
117 texture_atlas: Some(TextureAtlas {
118 layout: texture_atlas_layout.clone(),
119 index: animation_config_1.first_sprite_index,
120 }),
121 ..default()
122 },
123 Transform::from_scale(Vec3::splat(6.0)).with_translation(Vec3::new(-70.0, 0.0, 0.0)),
124 LeftSprite,
125 animation_config_1,
126 ));
127
128 // The second (right-hand) sprite runs at 20 FPS
129 let animation_config_2 = AnimationConfig::new(1, 6, 20);
130
131 // Create the second (right-hand) sprite
132 commands.spawn((
133 Sprite {
134 image: texture.clone(),
135 texture_atlas: Some(TextureAtlas {
136 layout: texture_atlas_layout.clone(),
137 index: animation_config_2.first_sprite_index,
138 }),
139 ..Default::default()
140 },
141 Transform::from_scale(Vec3::splat(6.0)).with_translation(Vec3::new(70.0, 0.0, 0.0)),
142 RightSprite,
143 animation_config_2,
144 ));
145 }