Fixed Timestep

Relevant official examples: fixed_timestep.


If you need to run some systems at a fixed rate, independent of the display frame rate, Bevy provides a solution.

// These systems will run every frame
// (at the framerate being rendered to your screen)
app.add_systems(Update, (
    camera_movement,
    animation,
    juicy_explosions,
));

// These systems will run as many times as needed
// as to maintain a fixed rate on average
app.add_systems(FixedUpdate, (
    physics_collisions,
    enemy_ai,
    gameplay_simulation,
));

Every frame update, Bevy will run the FixedUpdate schedule as many times as needed to catch up. If the game is running slow, it might run multiple times. If the game is running fast, it might be skipped.

This happens before the regular Update schedule runs for that frame, but after state transitions.

The default fixed timestep interval is 64 Hz. If you want something else, you can configure it as follows:

// Set the Fixed Timestep interval to 96 Hz
app.insert_resource(Time::<Fixed>::from_hz(96.0));

// Set the Fixed Timestep interval to 250 milliseconds
app.insert_resource(Time::<Fixed>::from_seconds(0.25));

Checking the Time

Just use Res<Time> as normal. When your system is running in FixedUpdate, Bevy will automatically detect that, and all the timing information (such as delta) will represent the fixed timestep instead of the display frame rate.

fn print_time_delta(time: Res<Time>) {
    // If we add this system to `Update`, this will print the time delta
    // between subsequent frames (the display frame rate)

    // If we add this system to `FixedUpdate`, this will always print the
    // same value (equal to the fixed timestep interval).

    println!("Elapsed seconds: {}", time.delta_seconds());
}

// This system will access the Fixed time
// regardless of what schedule it runs in
fn print_fixed_time_info(time_fixed: Res<Time<Fixed>>) {
    // `Time<Fixed>` gives us some additional methods, such as checking
    // the overstep (partial timestep / amount of extra time accumulated)
    println!(
        "Time remaining until the next fixed update run: {}",
        time_fixed.delta_seconds() - time_fixed.overstep().as_secs_f32()
    );
}

// This system will access the regular frame time regardless
// of what schedule it runs in
fn check_virtual_time(time_fixed: Res<Time<Virtual>>) {
    // ...
}

If you need to access the regular frame-time from a system running under fixed timestep, you can use Res<Time<Virtual>> instead. Res<Time<Real>> gives you the real (wall-clock) time, without pausing or scaling.

If you need to access the fixed-timestep-time from a system running outside of fixed timestep, you can use Res<Time<Fixed>> instead.

Should I put my systems in Update or FixedUpdate?

The purpose of fixed timestep is to make gameplay code behave predictably and reliably. Things such as physics and simulation work best if they are computed with fixed time intervals, as that avoids floating point errors from accumulating and glitchy behavior from variable framerate.

The following things should probably be done in FixedUpdate:

  • Physics and collision detection
  • Networking / netcode
  • AI for enemies and NPCs (pathfinding, decisions, etc.)
  • Spawning/despawning gameplay-related entities
  • Other simulation and decision-making

However, anything that directly affects what is displayed on-screen should run per-frame, in order to look smooth. If you do movement or animation under fixed timestep, it will look choppy, especially on high-refresh-rate screens.

The following things should probably be done in Update:

  • Camera movement and controls
  • Animations
  • UI
  • Visual effects
  • Anything that is part of your game's graphics/visuals or interactivity
  • App state transitions

Bridging the Gap

Sometimes there is a logical conflict:

For something like player movement, you want it to be computed reliably as part of your gameplay/physics simulation, but you also want it to look smooth on-screen.

For input handling, you want it to be responsive and handled every frame, but you also have game mechanics that need to respond to it.

The most elegant solution to both of these problems is to handle synchonization yourself using custom types.

Movement

For player (and other) movement, you could create your own custom component type to use instead of Transform. Implement your player movement using your own types. Then have a system in Update to sync/update Transform from that, with some interpolation to make it look smooth.

// TODO show how to do this

Input Handling

If you use Res<ButtonInput<...>> and .just_pressed/.just_released to check for key/button presses, beware that the state is updated once per frame. This API is not reliable inside FixedUpdate. Use events for input handling instead, or roll your own abstractions.

One way to do this is to put your input handling systems in PreUpdate, order them after Bevy's InputSystem set, and do your input handling there. Convert it into your own custom event types or some other useful representation, which you can then handle from your gameplay code in FixedUpdate.

// TODO show how to do this

Timing Caveats

Fixed timestep does not run in real-world time! You cannot rely on it for timing!

For example, if you try to play audio from it, or send network packets, you will notice that they don't actually occur at the fixed timestep interval. They will not be evenly spaced!

Your systems are still called as part of the regular frame-update cycle. Every frame update, Bevy will run the FixedMain schedule as many times as needed to catch up.

This means if you specify, for example, a 60 Hz fixed timestep interval, your systems will not actually run in 1/60 second intervals in real time.

What will happen is the following:

  • If the display frame rate is faster than the timestep, some frame update cycles will skip the FixedMain schedule entirely.
  • If the display frame rate is slower than the timestep, some frame update cycles will run the FixedMain multiple times.

In any case, FixedMain will run right before Update, where your per-frame systems live.

Additional Schedules

FixedUpdate is actually part of a larger FixedMain schedule, which also contains other schedules:

They are analogous to the schedules in Main, that run every frame update. They can be used for analogous purposes (to contain "engine systems" from Bevy and plugins).