acron.fixed_loop

Fixed-rate game loop – Godot’s _physics_process cadence on Acron.

Godot runs _physics_process at a constant tick rate (default 60 Hz). Ported movement code frequently assumes delta == 1/60 (velocity-from-position tricks, per-frame deltas multiplied by 60), so the loop MUST step at a fixed rate rather than Bullet’s variable substeps. A wall-clock accumulator with a spiral-of-death cap drives it.

Ordering per fixed step (avoids one-frame visual “ghosting” – Bullet syncs each body’s NodePath transform only AFTER do_physics):

  1. call each node’s _physics_process(dt) (applies forces / velocity)

  2. world.do_physics(dt, 0, dt) (Bullet integrates + syncs)

  3. call each node’s update_visuals() (place visuals vs FINAL xform)

  4. input_map.end_step() (just-pressed flags live one step)

Do not throw away the accumulator remainder: GameLoop.alpha is the render-interpolation factor. Physics advances at the fixed rate but rendering runs at the display refresh; a camera/model that snaps to the raw body position each render frame advances in irregular full-step jumps (“camera jitters as the player runs”, worse with speed). Consumers record pre-/post-step positions and render at interpolated(prev, curr, loop.alpha).

Per-frame (variable-dt) work is Godot’s _process, and goes through GameLoop.process(), which the app calls once per render frame alongside GameLoop.update_visuals(). Camera smoothing and HUD may equally be driven straight from the app’s own render task.

Headless testing: offscreen, task_mgr.step() barely advances the global clock, so an accumulator loop runs ~0 steps and “nothing moves”. Call GameLoop.step() directly instead – fully deterministic, no clock.

GameLoop([physics, task_mgr, clock, ...])

Drives fixed-step physics over registered nodes, exposing alpha.

Global Functions

interpolated(prev, curr, alpha: float)

Lerp helper for render interpolation: prev + (curr - prev) * alpha.