New cooldown example (#19234)
# Objective We want to extend our examples with a new category "usage" to demonstrate common use cases (see bevyengine/bevy-website#2131). This PR adds an example of animated cooldowns on button clicks. ## Solution - New example in "usage" directory - Implement a cooldown with an animated child Node ## Testing - I ran this on Linux - [x] test web (with bevy CLI: `bevy run --example cooldown web --open`) --------- Co-authored-by: Thierry Berger <contact@thierryberger.com> Co-authored-by: Ida "Iyes" <40234599+inodentry@users.noreply.github.com>
This commit is contained in:
parent
20057e5ed7
commit
923c2ad281
11
Cargo.toml
11
Cargo.toml
@ -4390,3 +4390,14 @@ name = "Extended Bindless Material"
|
|||||||
description = "Demonstrates bindless `ExtendedMaterial`"
|
description = "Demonstrates bindless `ExtendedMaterial`"
|
||||||
category = "Shaders"
|
category = "Shaders"
|
||||||
wasm = false
|
wasm = false
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "cooldown"
|
||||||
|
path = "examples/usage/cooldown.rs"
|
||||||
|
doc-scrape-examples = true
|
||||||
|
|
||||||
|
[package.metadata.example.cooldown]
|
||||||
|
name = "Cooldown"
|
||||||
|
description = "Example for cooldown on button clicks"
|
||||||
|
category = "Usage"
|
||||||
|
wasm = true
|
||||||
|
BIN
assets/textures/food_kenney.png
Normal file
BIN
assets/textures/food_kenney.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 66 KiB |
@ -67,6 +67,7 @@ git checkout v0.4.0
|
|||||||
- [Tools](#tools)
|
- [Tools](#tools)
|
||||||
- [Transforms](#transforms)
|
- [Transforms](#transforms)
|
||||||
- [UI (User Interface)](#ui-user-interface)
|
- [UI (User Interface)](#ui-user-interface)
|
||||||
|
- [Usage](#usage)
|
||||||
- [Window](#window)
|
- [Window](#window)
|
||||||
|
|
||||||
- [Tests](#tests)
|
- [Tests](#tests)
|
||||||
@ -574,6 +575,12 @@ Example | Description
|
|||||||
[Viewport Node](../examples/ui/viewport_node.rs) | Demonstrates how to create a viewport node with picking support
|
[Viewport Node](../examples/ui/viewport_node.rs) | Demonstrates how to create a viewport node with picking support
|
||||||
[Window Fallthrough](../examples/ui/window_fallthrough.rs) | Illustrates how to access `winit::window::Window`'s `hittest` functionality.
|
[Window Fallthrough](../examples/ui/window_fallthrough.rs) | Illustrates how to access `winit::window::Window`'s `hittest` functionality.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Example | Description
|
||||||
|
--- | ---
|
||||||
|
[Cooldown](../examples/usage/cooldown.rs) | Example for cooldown on button clicks
|
||||||
|
|
||||||
## Window
|
## Window
|
||||||
|
|
||||||
Example | Description
|
Example | Description
|
||||||
|
179
examples/usage/cooldown.rs
Normal file
179
examples/usage/cooldown.rs
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
//! Demonstrates implementing a cooldown in UI.
|
||||||
|
//!
|
||||||
|
//! You might want a system like this for abilities, buffs or consumables.
|
||||||
|
//! We create four food buttons to eat with 2, 1, 10, and 4 seconds cooldown.
|
||||||
|
|
||||||
|
use bevy::{color::palettes::tailwind, ecs::spawn::SpawnIter, prelude::*};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
App::new()
|
||||||
|
.add_plugins(DefaultPlugins)
|
||||||
|
.add_systems(Startup, setup)
|
||||||
|
.add_systems(
|
||||||
|
Update,
|
||||||
|
(
|
||||||
|
activate_ability,
|
||||||
|
animate_cooldowns.run_if(any_with_component::<ActiveCooldown>),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup(
|
||||||
|
mut commands: Commands,
|
||||||
|
mut texture_atlas_layouts: ResMut<Assets<TextureAtlasLayout>>,
|
||||||
|
asset_server: Res<AssetServer>,
|
||||||
|
) {
|
||||||
|
commands.spawn(Camera2d);
|
||||||
|
let texture = asset_server.load("textures/food_kenney.png");
|
||||||
|
let layout = TextureAtlasLayout::from_grid(UVec2::splat(64), 7, 7, None, None);
|
||||||
|
let texture_atlas_layout = texture_atlas_layouts.add(layout);
|
||||||
|
commands.spawn((
|
||||||
|
Node {
|
||||||
|
width: Val::Percent(100.),
|
||||||
|
height: Val::Percent(100.),
|
||||||
|
align_items: AlignItems::Center,
|
||||||
|
justify_content: JustifyContent::Center,
|
||||||
|
column_gap: Val::Px(15.),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
Children::spawn(SpawnIter(
|
||||||
|
[
|
||||||
|
FoodItem {
|
||||||
|
name: "an apple",
|
||||||
|
cooldown: 2.,
|
||||||
|
index: 2,
|
||||||
|
},
|
||||||
|
FoodItem {
|
||||||
|
name: "a burger",
|
||||||
|
cooldown: 1.,
|
||||||
|
index: 23,
|
||||||
|
},
|
||||||
|
FoodItem {
|
||||||
|
name: "chocolate",
|
||||||
|
cooldown: 10.,
|
||||||
|
index: 32,
|
||||||
|
},
|
||||||
|
FoodItem {
|
||||||
|
name: "cherries",
|
||||||
|
cooldown: 4.,
|
||||||
|
index: 41,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.map(move |food| build_ability(food, texture.clone(), texture_atlas_layout.clone())),
|
||||||
|
)),
|
||||||
|
));
|
||||||
|
commands.spawn((
|
||||||
|
Text::new("*Click some food to eat it*"),
|
||||||
|
Node {
|
||||||
|
position_type: PositionType::Absolute,
|
||||||
|
top: Val::Px(5.0),
|
||||||
|
left: Val::Px(15.0),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FoodItem {
|
||||||
|
name: &'static str,
|
||||||
|
cooldown: f32,
|
||||||
|
index: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_ability(
|
||||||
|
food: FoodItem,
|
||||||
|
texture: Handle<Image>,
|
||||||
|
layout: Handle<TextureAtlasLayout>,
|
||||||
|
) -> impl Bundle {
|
||||||
|
let FoodItem {
|
||||||
|
name,
|
||||||
|
cooldown,
|
||||||
|
index,
|
||||||
|
} = food;
|
||||||
|
let name = Name::new(name);
|
||||||
|
|
||||||
|
// Every food item is a button with a child node.
|
||||||
|
// The child node's height will be animated to be at 100% at the beginning
|
||||||
|
// of a cooldown, effectively graying out the whole button, and then getting smaller over time.
|
||||||
|
(
|
||||||
|
Node {
|
||||||
|
width: Val::Px(80.0),
|
||||||
|
height: Val::Px(80.0),
|
||||||
|
flex_direction: FlexDirection::ColumnReverse,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
BackgroundColor(tailwind::SLATE_400.into()),
|
||||||
|
Button,
|
||||||
|
ImageNode::from_atlas_image(texture, TextureAtlas { layout, index }),
|
||||||
|
Cooldown(Timer::from_seconds(cooldown, TimerMode::Once)),
|
||||||
|
name,
|
||||||
|
children![(
|
||||||
|
Node {
|
||||||
|
width: Val::Percent(100.),
|
||||||
|
height: Val::Percent(0.),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
BackgroundColor(tailwind::SLATE_50.with_alpha(0.5).into()),
|
||||||
|
)],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
struct Cooldown(Timer);
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
#[component(storage = "SparseSet")]
|
||||||
|
struct ActiveCooldown;
|
||||||
|
|
||||||
|
fn activate_ability(
|
||||||
|
mut commands: Commands,
|
||||||
|
mut interaction_query: Query<
|
||||||
|
(
|
||||||
|
Entity,
|
||||||
|
&Interaction,
|
||||||
|
&mut Cooldown,
|
||||||
|
&Name,
|
||||||
|
Option<&ActiveCooldown>,
|
||||||
|
),
|
||||||
|
(Changed<Interaction>, With<Button>),
|
||||||
|
>,
|
||||||
|
mut text: Query<&mut Text>,
|
||||||
|
) -> Result {
|
||||||
|
for (entity, interaction, mut cooldown, name, on_cooldown) in &mut interaction_query {
|
||||||
|
if *interaction == Interaction::Pressed {
|
||||||
|
if on_cooldown.is_none() {
|
||||||
|
cooldown.0.reset();
|
||||||
|
commands.entity(entity).insert(ActiveCooldown);
|
||||||
|
**text.single_mut()? = format!("You ate {name}");
|
||||||
|
} else {
|
||||||
|
**text.single_mut()? = format!(
|
||||||
|
"You can eat {name} again in {} seconds.",
|
||||||
|
cooldown.0.remaining_secs().ceil()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn animate_cooldowns(
|
||||||
|
time: Res<Time>,
|
||||||
|
mut commands: Commands,
|
||||||
|
buttons: Query<(Entity, &mut Cooldown, &Children), With<ActiveCooldown>>,
|
||||||
|
mut nodes: Query<&mut Node>,
|
||||||
|
) -> Result {
|
||||||
|
for (entity, mut timer, children) in buttons {
|
||||||
|
timer.0.tick(time.delta());
|
||||||
|
let cooldown = children.first().ok_or("No child")?;
|
||||||
|
if timer.0.just_finished() {
|
||||||
|
commands.entity(entity).remove::<ActiveCooldown>();
|
||||||
|
nodes.get_mut(*cooldown)?.height = Val::Percent(0.);
|
||||||
|
} else {
|
||||||
|
nodes.get_mut(*cooldown)?.height = Val::Percent((1. - timer.0.fraction()) * 100.);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user