Tainted \\ Coders

Bevy Timers

Last updated:

Timers can be created as either resources or components. A Timer on its own won’t do much. Its up to us to tick them ahead until they are done.

Timers come in two modes:

  1. TimerMode::Once which will tick down to 0 once, and only resets manually
  2. TimerMode::Repeat which will tick down to 0 then reset itself automatically

In Bevy, timers don’t tick down from their initial value. Instead they tick up from zero until they reach their maximum.

We can then call finished or just_finished to switch behavior when they are done.

Timers as a resource

Resources are useful when we want a timer that belongs to no particular entity in our game. For example, imagine we wanted to program a timer for a match like in Rocket League.

The match timer wouldn’t make sense as an entity of its own, we only ever want to spawn a single one.

So we can create ourselves a resource:

#[derive(Resource)]
pub struct MatchTime(Timer)

impl MatchTime {
    pub fn new() -> Self {
        Self(Timer::from_seconds(60.0, TimerMode::Once)
    }
}

// We need to implement Default so that our timer can be initialized as
// a resource when we call `init_resource`
impl Default for MatchTime {
    fn default() -> Self {
        Self::new()
    }
}

We create a MatchTime resource and implement Default so it can be easily initialized when we define our app. This makes it available right at the start of our game without any additional setup.

fn countdown(
    time: Res<Time>,
    mut match_time: ResMut<MatchTime>
) {
    match_time.0.tick(time.delta());
}

fn end_match(match_time: Res<MatchTime>) {
    if match_time.0.finished() {
        // Here we would rest our game
    }
}

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .init_resource::<MatchTime>()
        .add_systems(Update, (countdown, end_game))
        .run();
}

The match time won’t tick on its own. We need a system to update according to the time that has passed since the last tick.

Bevy has a built in Time resource we can use to get the delta in seconds of the time between this tick and the last. We simply tick ahead our timer according to this value.

Timers as a component

Now lets say we wanted to have a Cooldown for one of our abilities. This wouldn’t make sense as a resource because the cooldown would be specific to one of our players.

#[derive(Component)]
pub struct Cooldown(Timer);

fn cast_spell(
    mut commands: Commands,
    mut player_query: Query<Entity, With<Player>>,
    cooldowns: Query<&Cooldown, With<Player>>
) {
    let player = player_query.single();

    if let Ok(cooldown) = cooldowns.get_component::<Cooldown>(player) {
        info!(
            "You cannot cast yet. Your cooldown is {:0.0}% complete!",
            cooldown.0.percent() * 100.0
        )
    } else {
        // Add an entity to the world with a timer
        commands
            .entity(player)
            .insert(
                Cooldown(
                    Timer::from_seconds(
                        5.0,
                        TimerMode::Once,
                    )
                )
            );

        // Cast the spell here
    }
}

fn tick_cooldowns(
    mut commands: Commands,
    mut cooldowns: Query<(Entity, &mut Cooldown)>,
    time: Res<Time>
) {
    for (entity, mut cooldown) in &mut cooldowns {
        cooldown.0.tick(time.delta())

        if cooldown.finished() {
            commands.entity(entity).remove::<Cooldown>();
        }
    }
}

We add a cooldown when the player casts their spell and then tick down those cooldowns until they finish.

Finally we remove the cooldown component which lets them cast a spell again.

Read more