Skip to content

Ticks per Step

ticks_per_step(cells_per_second, fixed_dt) returns the integer number of ticks between movement steps to achieve the requested cells/sec rate. Designing in cells/sec instead of ticks-per-step decouples timing from grid resolution — change tile size and your movement still feels the same.

import { ticks_per_step } from "@f0rbit/forge/grid";
const every = ticks_per_step(8, ctx.time.fixed_dt);
// 8 cells/sec at fixed_dt = 1/60 → every = 8
// 8 cells/sec at fixed_dt = 1/30 → every = 4
const ticks_per_step: (cells_per_second: number, fixed_dt: number) => number;

Implementation: Math.max(1, Math.round(1 / (cells_per_second * fixed_dt))). Returns at least 1 — never returns a step gate of 0.

cells_per_secondevery (ticks)Apparent speed
160one cell per second — slow puzzle pace
230two cells per second — turn-based feel
415four cells per second — relaxed roguelike
610six cells per second — dungeon-walk default
88snappy action-roguelike
125fast — twin-stick or shooter
302very fast — bullet/projectile cadence
601one cell per tick — shouldn’t generally need this
cells_per_secondevery (ticks)
215
48
65
84
152
301
import { ticks_per_step } from "@f0rbit/forge/grid";
import { schedule } from "@f0rbit/forge";
const sch = schedule();
const every = ticks_per_step(8, time.fixed_dt);
sch.add("update", movement_system, { every, name: "movement" });

When you call g.move_tile inside a periodic system, the gate handles cadence — the system body just performs one step:

const movement_system: System = (w) => {
for (const [id, , d] of w.query([pos_c, dir_c, player_c] as const)) {
if (d.dx === 0 && d.dy === 0) continue;
g.move_tile(w, id, d, { blocked_by });
}
};
sch.add("update", movement_system, { every: ticks_per_step(8, fixed_dt), name: "movement" });