Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4361,3 +4361,14 @@ name = "Extended Bindless Material"
description = "Demonstrates bindless `ExtendedMaterial`"
category = "Shaders"
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
Binary file added assets/textures/food_kenney.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ git checkout v0.4.0
- [Tools](#tools)
- [Transforms](#transforms)
- [UI (User Interface)](#ui-user-interface)
- [Usage](#usage)
- [Window](#window)

- [Tests](#tests)
Expand Down Expand Up @@ -571,6 +572,12 @@ Example | Description
[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.

## Usage

Example | Description
--- | ---
[Cooldown](../examples/usage/cooldown.rs) | Example for cooldown on button clicks

## Window

Example | Description
Expand Down
179 changes: 179 additions & 0 deletions examples/usage/cooldown.rs
Original file line number Diff line number Diff line change
@@ -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