Tainted\\Coders

Bevy Timers

Bevy version: 0.16Last 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 Duration.

We can then call finished or just_finished to switch behavior when they are done. The difference between them is that just_finished only returns true if the timer finished in the last tick.

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, Default)]
pub struct MatchTime(Timer);

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

We create a MatchTime resource and derive Default so it can be easily initialized when we add it to our app with init_resource. 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_match.after(countdown)))
    .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 which is being handled by countdown above.

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.

fn time_passed(time: Res<Time>) {
  info!("Duration passed: {:?}", time.delta());
  info!("Seconds passed: {:?}", time.delta_secs_f64());
  info!("Total time since startup: {:?}", time.elapsed());
}

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)]
struct Cooldown(Timer);

#[derive(Component)]
struct Player;

fn cast_spell(
  mut commands: Commands,
  mut player_query: Query<Entity, With<Player>>,
  cooldowns: Query<&Cooldown, With<Player>>,
) {
  if let Ok(player) = player_query.single() {
    if let Ok(cooldown) = cooldowns.get(player) {
      info!(
        "You cannot cast yet. Your cooldown is {:0.0}% complete!",
        cooldown.0.fraction() * 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.0.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.

Timers for a single system

Sometimes you will want a timer that is only ever used in a single system. For these cases a Resource would be too public and you might prefer a Local system parameter instead:

fn local_timer(time: Res<Time>, mut timer: Local<Timer>) {
  timer.tick(time.delta());

  if timer.just_finished() {
    info!("The timer is finished");
  }
}

This timer would only be available in this one system. This can be useful for only printing debug information after a certain period of time, or delaying a systems functionality.