Using Bevy's FixedUpdate Schedule for Deterministic Game Logic
Learn how to use Bevy's FixedUpdate schedule for deterministic logic, set a fixed timestep, avoid spiral of death, and keep rendering smooth.
08 Oct 2026, 19:00 UTC

Useful answer
In Bevy 0.11 and later the FixedTimestep plugin was removed. Deterministic logic is now run with the built‑in FixedUpdate schedule. By inserting a Time resource you set a constant step size (e.g., 0.016 s for 60 Hz). Bevy accumulates real delta time each frame and steps the FixedUpdate schedule one or more times until the accumulator is exhausted. Systems that you add to FixedUpdate therefore run at a steady rate independent of the rendering frame rate, which keeps physics and simulation stable.
How it works
When you start the app Bevy creates two independent clocks: Time for normal per‑frame work and Time for the fixed step. Each frame Bevy adds the real delta time to an internal accumulator for the fixed clock. While the accumulator is ≥ the step size, Bevy runs the FixedUpdate schedule once, subtracts the step size, and repeats. When the accumulator becomes smaller than the step size the loop stops and the remainder is kept for the next frame. Systems that you place in FixedUpdate (e.g., physics) see a constant dt from Time::delta_seconds(). Systems that you keep in Update (e.g., rendering, UI) run once per frame and can read the latest transform values.
Worked example
use bevy::prelude::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
// Set the fixed timestep to 60 Hz (0.016 s)
.insert_resource(Time::::from_seconds(0.016))
.insert_resource(ClearColor(Color::rgb(0.1, 0.1, 0.1)))
.add_startup_system(setup)
// Physics runs on the fixed step
.add_systems(FixedUpdate, physics_system)
// Rendering runs each frame
.add_systems(Update, render_system)
.run();
}
fn setup(mut commands: Commands) {
commands.spawn(Camera2dBundle::default());
commands.spawn((
SpriteBundle {
sprite: Sprite { color: Color::GREEN, ..default() },
transform: Transform::from_translation(Vec3::ZERO),
..default()
},
Velocity { x: 120.0, y: 80.0 },
));
}
#[derive(Component)]
struct Velocity {
x: f32,
y: f32,
}
fn physics_system(
mut query: Query<(&mut Transform, &Velocity)>,
time: Res>,
) {
let dt = time.delta_seconds();
for (mut transform, vel) in query.iter_mut() {
transform.translation.x += vel.x * dt;
transform.translation.y += vel.y * dt;
}
}
fn render_system(query: Query<&Transform>) {
for transform in query.iter() {
info!("Sprite position: ({:.2}, {:.2})", transform.translation.x, transform.translation.y);
}
}
Explanation: The insert_resource(Time::::from_seconds(0.016)) line tells Bevy to advance the fixed clock in 16 ms increments. The physics system is added to FixedUpdate, so it executes only inside the fixed‑step loop. The render system stays in Update, drawing the latest position each frame.
Limits and common mistakes
- Spiral of death: If a frame takes much longer than the step size (e.g., due to a stall or debug breakpoint), the accumulator can grow large and cause many fixed steps in a single frame, freezing the game. Bevy does not automatically limit the number of sub‑steps; you must guard against this yourself, for example by clamping the accumulated delta time before it reaches the fixed clock or by inserting a system that reads
Timeand resets the accumulator when it exceeds a safe maximum. - Mixing schedules: Adding the same system to both
FixedUpdateandUpdateleads to double updates or missed steps. Keep simulation‑like logic (physics, AI) exclusively inFixedUpdateand visual/UI work inUpdate. - Step size too small: A very small step (e.g., 0.001 s) increases CPU load because the fixed loop may iterate many times per frame with little gain in fidelity.
- Step size too large: A large step (e.g., 0.1 s) reduces simulation fidelity; fast objects may tunnel through colliders or appear to jump.
- Using the wrong time resource: Inside a fixed system you must read
Time; usingTimewill give you the variable frame delta and break determinism.
Practical verification
- Compile and run the example. Resize the window or let the frame rate drop (e.g., by enabling VSync off and limiting FPS). The console log shows the physics system printing a steady delta time of ≈0.016 s each fixed step, while the render log continues each frame.
- Change the fixed step to
0.1(10 Hz) by modifyinginsert_resource(Time::::from_seconds(0.1)). Re‑run; the sprite moves noticeably slower but the render log still appears each frame, confirming that rendering stays smooth. - Introduce an artificial delay: at the start of
physics_systemaddstd::thread::sleep(std::time::Duration::from_millis(150)). With a large delta time the accumulator will cause multiple fixed steps per frame. The app will not hang; you will see several physics update logs before the next render frame. Observe that if you increase the delay beyond a few hundred milliseconds the game may appear to freeze – this demonstrates the spiral‑of‑death risk and the need to clamp accumulator size.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.