Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 an f64 (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.