docs/bevy/examples/usage/cooldown.rs 5.4 K · 202 lines · raw · history

1 //! Demonstrates implementing a cooldown in UI.
2 //!
3 //! You might want a system like this for abilities, buffs or consumables.
4 //! We create four food buttons to eat with 2, 1, 10, and 4 seconds cooldown.
5
6 use bevy::{
7 color::palettes::tailwind::{SLATE_400, SLATE_50},
8 image::TextureAtlasTemplate,
9 prelude::*,
10 ui_widgets::{Activate, Button},
11 };
12
13 fn main() {
14 App::new()
15 .add_plugins(DefaultPlugins)
16 .add_systems(Startup, setup)
17 .add_systems(
18 Update,
19 animate_cooldowns.run_if(any_with_component::<ActiveCooldown>),
20 )
21 .add_observer(on_activate_start_cooldown)
22 .run();
23 }
24
25 fn setup(mut commands: Commands, mut texture_atlas_layouts: ResMut<Assets<TextureAtlasLayout>>) {
26 let layout = TextureAtlasLayout::from_grid(UVec2::splat(64), 7, 7, None, None);
27 let texture_atlas_layout = texture_atlas_layouts.add(layout);
28
29 commands.spawn(Camera2d);
30
31 // The food items
32 commands.spawn_scene(bsn! {
33 Node {
34 width: percent(100),
35 height: percent(100),
36 align_items: AlignItems::Center,
37 justify_content: JustifyContent::Center,
38 column_gap: px(15),
39 }
40 Children [
41 { food_items_scene_list(texture_atlas_layout) }
42 ]
43 });
44
45 // Status text
46 commands.spawn_scene(bsn! {
47 Node {
48 position_type: PositionType::Absolute,
49 top: px(12),
50 left: px(12),
51 }
52 Children [
53 Text("*Click some food to eat it*")
54 ]
55 });
56 }
57
58 // Returns the scene list of food items.
59 fn food_items_scene_list(texture_atlas_layout: Handle<TextureAtlasLayout>) -> impl SceneList {
60 let foods = [
61 FoodItem {
62 name: "an apple",
63 cooldown: 2.,
64 index: 2,
65 },
66 FoodItem {
67 name: "a burger",
68 cooldown: 1.,
69 index: 23,
70 },
71 FoodItem {
72 name: "chocolate",
73 cooldown: 10.,
74 index: 32,
75 },
76 FoodItem {
77 name: "cherries",
78 cooldown: 4.,
79 index: 41,
80 },
81 ];
82
83 foods
84 .into_iter()
85 .map(|food| {
86 build_ability(
87 food,
88 "textures/food_kenney.png".into(),
89 texture_atlas_layout.clone(),
90 )
91 })
92 .collect::<Vec<_>>()
93 }
94
95 struct FoodItem {
96 name: &'static str,
97 cooldown: f32,
98 index: usize,
99 }
100
101 // Returns a scene of this food item as a button.
102 fn build_ability(
103 food: FoodItem,
104 texture: String,
105 layout: Handle<TextureAtlasLayout>,
106 ) -> impl Scene {
107 let FoodItem {
108 name,
109 cooldown,
110 index,
111 } = food;
112
113 // Every food item is a button with a child node.
114 // The child node's height will be animated to be at 100% at the beginning
115 // of a cooldown, effectively graying out the whole button, and then getting smaller over time.
116 bsn! {
117 Node {
118 width: px(80),
119 height: px(80),
120 flex_direction: FlexDirection::ColumnReverse,
121 }
122 BackgroundColor(SLATE_400)
123 Button
124 ImageNode {
125 image: texture,
126 texture_atlas: texture_atlas_template(layout, index),
127 }
128 Cooldown(Timer::from_seconds(cooldown, TimerMode::Once))
129 Name(name)
130 Children [
131 Node {
132 width: percent(100),
133 height: percent(0),
134 }
135 BackgroundColor({SLATE_50.with_alpha(0.5)})
136 ]
137 }
138 }
139
140 // Ensure the texture_atlas in the ImageNode can be set correctly.
141 fn texture_atlas_template(
142 layout: Handle<TextureAtlasLayout>,
143 index: usize,
144 ) -> TextureAtlasTemplate {
145 TextureAtlasTemplate {
146 layout: layout.into(),
147 index,
148 }
149 }
150
151 #[derive(Component, Clone, Default)]
152 struct Cooldown(Timer);
153
154 #[derive(Component)]
155 #[component(storage = "SparseSet")]
156 struct ActiveCooldown;
157
158 /// Observer that starts the cooldown whenever a food item button is activated.
159 fn on_activate_start_cooldown(
160 event: On<Activate>,
161 mut commands: Commands,
162 mut interaction_query: Query<
163 (Entity, &mut Cooldown, &Name, Option<&ActiveCooldown>),
164 With<Button>,
165 >,
166 mut text: Query<&mut Text>,
167 ) -> Result {
168 if let Ok((entity, mut cooldown, name, on_cooldown)) = interaction_query.get_mut(event.entity) {
169 if on_cooldown.is_none() {
170 cooldown.0.reset();
171 commands.entity(entity).insert(ActiveCooldown);
172 **text.single_mut()? = format!("You ate {name}");
173 } else {
174 **text.single_mut()? = format!(
175 "You can eat {name} again in {} seconds.",
176 cooldown.0.remaining_secs().ceil()
177 );
178 }
179 }
180
181 Ok(())
182 }
183
184 fn animate_cooldowns(
185 time: Res<Time>,
186 mut commands: Commands,
187 buttons: Query<(Entity, &mut Cooldown, &Children), With<ActiveCooldown>>,
188 mut nodes: Query<&mut Node>,
189 ) -> Result {
190 for (entity, mut timer, children) in buttons {
191 timer.0.tick(time.delta());
192 let cooldown = children.first().ok_or("No child")?;
193 if timer.0.just_finished() {
194 commands.entity(entity).remove::<ActiveCooldown>();
195 nodes.get_mut(*cooldown)?.height = percent(0);
196 } else {
197 nodes.get_mut(*cooldown)?.height = percent((1. - timer.0.fraction()) * 100.);
198 }
199 }
200
201 Ok(())
202 }