sprite_picking/
sprite_picking.rs

1//! Demonstrates picking for sprites and sprite atlases.
2//! By default, the sprite picking backend considers a sprite only when a pointer is over an opaque pixel.
3
4use bevy::{prelude::*, sprite::Anchor};
5use std::fmt::Debug;
6
7fn main() {
8    App::new()
9        .add_plugins(DefaultPlugins.set(ImagePlugin::default_nearest()))
10        .add_systems(Startup, (setup, setup_atlas))
11        .add_systems(Update, (move_sprite, animate_sprite))
12        .run();
13}
14
15fn move_sprite(
16    time: Res<Time>,
17    mut sprite: Query<&mut Transform, (Without<Sprite>, With<Children>)>,
18) {
19    let t = time.elapsed_secs() * 0.1;
20    for mut transform in &mut sprite {
21        let new = Vec2 {
22            x: 50.0 * ops::sin(t),
23            y: 50.0 * ops::sin(t * 2.0),
24        };
25        transform.translation.x = new.x;
26        transform.translation.y = new.y;
27    }
28}
29
30/// Set up a scene that tests all sprite anchor types.
31fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
32    commands.spawn(Camera2d);
33
34    let len = 128.0;
35    let sprite_size = Vec2::splat(len / 2.0);
36
37    commands
38        .spawn((Transform::default(), Visibility::default()))
39        .with_children(|commands| {
40            for (anchor_index, anchor) in [
41                Anchor::TOP_LEFT,
42                Anchor::TOP_CENTER,
43                Anchor::TOP_RIGHT,
44                Anchor::CENTER_LEFT,
45                Anchor::CENTER,
46                Anchor::CENTER_RIGHT,
47                Anchor::BOTTOM_LEFT,
48                Anchor::BOTTOM_CENTER,
49                Anchor::BOTTOM_RIGHT,
50            ]
51            .iter()
52            .enumerate()
53            {
54                let i = (anchor_index % 3) as f32;
55                let j = (anchor_index / 3) as f32;
56
57                // Spawn black square behind sprite to show anchor point
58                commands
59                    .spawn((
60                        Sprite::from_color(Color::BLACK, sprite_size),
61                        Transform::from_xyz(i * len - len, j * len - len, -1.0),
62                        Pickable::default(),
63                    ))
64                    .observe(recolor_on::<Pointer<Over>>(Color::srgb(0.0, 1.0, 1.0)))
65                    .observe(recolor_on::<Pointer<Out>>(Color::BLACK))
66                    .observe(recolor_on::<Pointer<Press>>(Color::srgb(1.0, 1.0, 0.0)))
67                    .observe(recolor_on::<Pointer<Release>>(Color::srgb(0.0, 1.0, 1.0)));
68
69                commands
70                    .spawn((
71                        Sprite {
72                            image: asset_server.load("branding/bevy_bird_dark.png"),
73                            custom_size: Some(sprite_size),
74                            color: Color::srgb(1.0, 0.0, 0.0),
75                            ..default()
76                        },
77                        anchor.to_owned(),
78                        // 3x3 grid of anchor examples by changing transform
79                        Transform::from_xyz(i * len - len, j * len - len, 0.0)
80                            .with_scale(Vec3::splat(1.0 + (i - 1.0) * 0.2))
81                            .with_rotation(Quat::from_rotation_z((j - 1.0) * 0.2)),
82                        Pickable::default(),
83                    ))
84                    .observe(recolor_on::<Pointer<Over>>(Color::srgb(0.0, 1.0, 0.0)))
85                    .observe(recolor_on::<Pointer<Out>>(Color::srgb(1.0, 0.0, 0.0)))
86                    .observe(recolor_on::<Pointer<Press>>(Color::srgb(0.0, 0.0, 1.0)))
87                    .observe(recolor_on::<Pointer<Release>>(Color::srgb(0.0, 1.0, 0.0)));
88            }
89        });
90}
91
92#[derive(Component)]
93struct AnimationIndices {
94    first: usize,
95    last: usize,
96}
97
98#[derive(Component, Deref, DerefMut)]
99struct AnimationTimer(Timer);
100
101fn animate_sprite(
102    time: Res<Time>,
103    mut query: Query<(&AnimationIndices, &mut AnimationTimer, &mut Sprite)>,
104) {
105    for (indices, mut timer, mut sprite) in &mut query {
106        let Some(texture_atlas) = &mut sprite.texture_atlas else {
107            continue;
108        };
109
110        timer.tick(time.delta());
111
112        if timer.just_finished() {
113            texture_atlas.index = if texture_atlas.index == indices.last {
114                indices.first
115            } else {
116                texture_atlas.index + 1
117            };
118        }
119    }
120}
121
122fn setup_atlas(
123    mut commands: Commands,
124    asset_server: Res<AssetServer>,
125    mut texture_atlas_layouts: ResMut<Assets<TextureAtlasLayout>>,
126) {
127    let texture_handle = asset_server.load("textures/rpg/chars/gabe/gabe-idle-run.png");
128    let layout = TextureAtlasLayout::from_grid(UVec2::new(24, 24), 7, 1, None, None);
129    let texture_atlas_layout_handle = texture_atlas_layouts.add(layout);
130    // Use only the subset of sprites in the sheet that make up the run animation
131    let animation_indices = AnimationIndices { first: 1, last: 6 };
132    commands
133        .spawn((
134            Sprite::from_atlas_image(
135                texture_handle,
136                TextureAtlas {
137                    layout: texture_atlas_layout_handle,
138                    index: animation_indices.first,
139                },
140            ),
141            Transform::from_xyz(300.0, 0.0, 0.0).with_scale(Vec3::splat(6.0)),
142            animation_indices,
143            AnimationTimer(Timer::from_seconds(0.1, TimerMode::Repeating)),
144            Pickable::default(),
145        ))
146        .observe(recolor_on::<Pointer<Over>>(Color::srgb(0.0, 1.0, 1.0)))
147        .observe(recolor_on::<Pointer<Out>>(Color::srgb(1.0, 1.0, 1.0)))
148        .observe(recolor_on::<Pointer<Press>>(Color::srgb(1.0, 1.0, 0.0)))
149        .observe(recolor_on::<Pointer<Release>>(Color::srgb(0.0, 1.0, 1.0)));
150}
151
152// An observer that changes the target entity's color.
153fn recolor_on<E: EntityEvent + Debug + Clone + Reflect>(
154    color: Color,
155) -> impl Fn(On<E>, Query<&mut Sprite>) {
156    move |ev, mut sprites| {
157        let Ok(mut sprite) = sprites.get_mut(ev.event_target()) else {
158            return;
159        };
160        sprite.color = color;
161    }
162}