Bevy Version: | 0.14 | (current) |
---|
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 fixed-timestep-time from a system running outside
of fixed timestep, you can use Res<Time<Fixed>>
instead.
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.
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
Player movement and other movement that is part of gameplay
should be done in FixedUpdate
, so it works reliably and
consistently. To also make it look smooth on-screen, see transform
interpolation/extrapolation.
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).