The simulation clock
Simulation systems run on their own clock, separate from the frame rate. The game can pause, step and fast-forward the sim while rendering and input keep running normally.
#![allow(unused)]
fn main() {
fn controls(ctx: &mut Ctx) {
if ctx.key_pressed(Key::Space) {
if ctx.is_paused() { ctx.resume() } else { ctx.pause() }
}
if ctx.key_pressed(Key::Period) {
ctx.step(); // one tick while paused
}
if ctx.key_pressed(Key::Num4) {
ctx.set_sim_speed(64.0); // fast-forward
}
}
Game::new("Colony", 1280, 720).sim_system(ants).system(controls);
}
static void controls(crabby::ctx& ctx) {
if (ctx.key_pressed(CRABBY_KEY_SPACE)) {
if (ctx.is_paused()) ctx.resume(); else ctx.pause();
}
if (ctx.key_pressed(CRABBY_KEY_PERIOD))
ctx.step(); // one tick while paused
if (ctx.key_pressed(CRABBY_KEY_NUM_4))
ctx.set_sim_speed(64); // fast-forward
}
// crabby::game("Colony", 1280, 720).sim_system(ants).system(controls);
Put controls in a frame system: frame systems keep running while the sim is
paused.
What the clock guarantees
- Ticks are always the same length.
ctx.dt()in a sim system is the fixed step (1 / fixed_hz), at every speed. Speed only changes how many ticks run per frame, so a run at 64× gives the same result as one at 1×. - The game stays responsive. At most 64 ticks run per frame. If the sim can’t keep up, it runs as fast as it can instead of freezing the window.
ctx.sim_tick()counts ticks;ctx.sim_time()is simulated seconds as anf64(no drift after hours of simulated time).
Doing work less often
Not everything needs to run 60 times a second. There are two tools, and they combine.
A whole system at a lower rate:
#![allow(unused)]
fn main() {
Game::new("Colony", 1280, 720)
.sim_system(movement) // every tick
.sim_system_every(10, plan_routes) // 6 times a second
.sim_system_every(10, grow_food) // also 6/s, but on other ticks
}
crabby::game("Colony", 1280, 720)
.sim_system(movement, "movement") // every tick
.sim_system(plan_routes, "plan_routes", 10) // 6 times a second
.sim_system(grow_food, "grow_food", 10); // also 6/s, but on other ticks
Inside plan_routes, ctx.dt() is 10 ticks’ worth of time, so rates stay
right. Systems with the same interval are spread over different ticks, so
their cost doesn’t pile up on one frame.
Entities taking turns: each tick, only 1/n of the entities do the expensive part.
#![allow(unused)]
fn main() {
fn think(ctx: &mut Ctx) {
let turn = ctx.turns(8); // each ant thinks every 8th tick
ctx.world.query::<(Entity, &mut Brain)>().par_for_each(|(e, brain)| {
if turn.includes(e) {
brain.decide(turn.dt()); // time since this ant's last turn
}
});
}
}
struct Brain { std::uint32_t plans; };
static void think(crabby::ctx& ctx) {
const crabby::turns turn = ctx.turns(8); // each ant thinks every 8th tick
ctx.world().each_par([&](crabby::entity e, Brain& brain) {
if (turn.includes(e))
brain.plans += 1; // turn.dt(): time since this ant's last turn
});
}
This keeps the per-tick cost flat instead of spiking every 8th tick, and
works in par_for_each (C++: each_par).
Smooth motion between ticks
If the sim rate is lower than the frame rate, ctx.fixed_alpha() (0..1)
tells a frame system how far it is between the last tick and the next, for
interpolating positions when drawing.