Tainted\\Coders

Bevy Timers

Bevy version: 0.19Last updated:

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

Timers come in two modes:

  1. TimerMode::Once which stays finished after reaching the end until it is reset manually
  2. TimerMode::Repeating which resets itself automatically when it reaches the end

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 is_finished or just_finished to switch behavior when they are done.

A repeating timer can finish more than once during a single tick if a lot of time passed. times_finished_this_tick tells us how many times tick crossed the duration so we can apply an effect once per occurrence.

Timers as a resource

Resources are useful when we want a timer that belongs to no particular entity. 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 the timer as a resource:

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

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

impl Default for MatchTime {
  fn default() -> Self {
    Self::new()
  }
}

If we only derived Default, then init_resource would build a zero-length timer because that is what Timer::default returns. To give the match a real duration we implement Default by hand so that it calls new. Now init_resource makes it available right at the start of our game without having to spawn an entity or call a specific system during Startup.

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. We will do this through a countdown system:

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

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

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.is_finished() {
    // Here we would reset our game
  }
}

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

Bevy has a built in Time resource we can use to get the delta between this tick and the last. The delta is a Duration that we can pass straight to tick. If we need the number of seconds we can use delta_secs or delta_secs_f64 instead.

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 a cooldown component to the player entity
      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.is_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.

Local timers

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:

/// `Local` values are initialized from `Default`, and a default `Timer` has a
/// zero-length duration. This wrapper builds a real timer on first access.
struct LocalTimer(Timer);

impl FromWorld for LocalTimer {
  fn from_world(_world: &mut World) -> Self {
    Self(Timer::from_seconds(5.0, TimerMode::Once))
  }
}

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

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

Local values are initialized from Default, and a default Timer has a zero-length duration. To avoid a timer that finishes immediately we wrap it in a LocalTimer type that builds a real timer through FromWorld.

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

Other useful methods

Beyond tick, is_finished, and just_finished there are a few more Timer methods worth knowing about:

If all you need is to run a system on a regular interval, Bevy ships run conditions for exactly that: on_timer, on_real_timer, once_after_delay, and repeating_after_delay. They are often simpler than storing a timer yourself.

Which time?

Res<Time> is Time<Virtual>, so it is affected by pausing and speed changes. If you need time that ignores those, use Time<Real>. Timers that should stay in step with a fixed simulation step instead belong in FixedUpdate with Time<Fixed>.