Game Loops & Timing
Fixed vs variable timestep in Bevy - stable physics, smooth rendering, and frame-rate independence.
Recipe
fn physics_step(mut q: Query<&mut Position>, time: Res<Time<Fixed>>) {
let dt = time.delta_secs();
for mut pos in &mut q {
pos.0 += dt * 10.0;
}
}
// App::new().add_systems(FixedUpdate, physics_step);When to reach for this:
- Any simulation sensitive to dt (physics, networking)
- Replay and determinism requirements
- Separating render FPS from simulation Hz
- Fighting jitter when frame times spike
Working Example
use bevy::prelude::*;
#[derive(Component)]
struct SimPosition(f32);
#[derive(Component)]
struct RenderPosition(f32);
fn fixed_sim(mut q: Query<&mut SimPosition>, time: Res<Time<Fixed>>) {
for mut sim in &mut q {
sim.0 += 100.0 * time.delta_secs();
}
}
fn interpolate_render(
sim_q: Query<&SimPosition>,
mut render_q: Query<&mut RenderPosition>,
time: Res<Time<Fixed>>,
) {
let alpha = time.overstep_fraction();
for (sim, mut render) in sim_q.iter().zip(render_q.iter_mut()) {
// Simplified: in full engine store previous sim for true lerp
render.0 = sim.0;
let _ = alpha;
}
}What this demonstrates:
- Physics mutation only in
FixedUpdate - Render-facing component updated after sim
overstep_fractionhook for interpolation alpha
Deep Dive
How It Works
- Variable loop measures real frame time
delta_secs. - Fixed accumulator consumes real time in fixed slices (e.g., 1/60 s).
- Spiral of death if too many fixed steps per frame - cap max steps.
- Interpolation draws between previous and current physics transforms.
Schedule Map
| Schedule | Typical systems |
|---|---|
FixedUpdate | Physics, deterministic sim |
Update | Input, animation state machines |
PostUpdate | Camera follow interpolated transform |
Gotchas
- Physics in
Updatewithdelta_secs- unstable stacks. Fix: move toFixedUpdateonly. - Uncapped fixed catch-up - freeze after hitch. Fix: max substeps per frame (Bevy default helps).
- Forgetting interpolation - physics looks stuttery at 144 FPS display. Fix: lerp render transform between fixed states.
- UI tied to fixed step - sluggish menus. Fix: keep UI in
Updatewith variable time. - Network tick mismatch - desync. Fix: align server sim rate with
FixedHz.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| Variable everything | Simple non-physics arcade | Stacks, ragdolls, competitive physics |
| Fixed 30 Hz sim | Mobile budget | PC high refresh competitive |
| Deterministic lockstep net | RTS peer-to-peer | Single-player only |
| Manual accumulator custom engine | Non-Bevy | Already on Bevy schedules |
FAQs
What fixed Hz?
60 Hz common default; 30 Hz on mobile saves CPU; fighting games may need 60+ with rollback netcode.
Bevy Time types?
Time variable, Time<Fixed> for fixed schedule, Time<Virtual> for pause/slow-mo.
Pause game?
Pause Time<Virtual> or disable schedules while menu open - UI still animates if desired.
Slow motion?
Scale virtual time factor affecting Update; decide if physics should also slow.
Delta clamp?
Clamp huge delta_secs after loading hitch to avoid teleport - common pattern max 0.25s.
Profiling frame budget?
Tracy spans per schedule; compare FixedUpdate ms vs render ms.
Input sampling?
Poll input once per rendered frame; apply in fixed step via buffered intents.
Cutscenes?
Timeline driven by fixed or real time depending on sync to physics needs.
Bevy config?
TimePlugin sets fixed timestep in App - consult version docs for FixedTime resource API.
Basics link?
Game Dev Basics Time resource example.
Related
- Physics & Audio - fixed physics
- Bevy Engine - schedules
- Performance for Games - frame budget
- Input & Events - input timing
Stack versions: This page was written for Rust 1.97.0 (edition 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, and Polars 0.46+.