Skip to content
Ticks

Ticks

Time in the engine moves in ticks. Within a tick the world does not change: game logic reads it and decides what should happen, as a list of commands. Then one step applies the commands in order and moves to the next tick.

fn layout() -> schema {
  schema_of(with_field_decl(component_decl_of("health"), "points", kind_int()))
}

fn health() -> int { 0 }

fn points(value: int) -> words { word(value: value, next: words_end()) }

fn main() -> int {
  let w = world_new(layout());
  let spawn_hero = command_spawn(parts: part_cons(
    head: part(component: health(), data: points(10)), tail: part_nil()));
  step(w, command_cons(head: spawn_hero, tail: command_nil()));
  let hero = handle_of(0, 1);
  let hurt = command_set(target: hero, component: health(), data: points(7));
  let rejected = step(w, command_cons(head: hurt,
    tail: command_cons(head: command_destroy(target: handle_of(9, 1)), tail: command_nil())));
  print(concat3(world_text(w), joined(rejected, "\n"), "\n"))
}
bootstrap/qdx run std/*.qdx engine/*.qdx hurt.qdx
tick 2
slots 1
entity @0#1
  health points=7
tick 1: destroy: there is no entity @9#1

Commands

CommandEffect
command_spawn(parts)Creates an entity with the given components. Each part is a component number and its data.
command_destroy(target)Destroys an entity.
command_set(target, component, data)Attaches or replaces a component.
command_remove(target, component)Detaches a component.

step

step(w, commands) applies each command in order, then advances the tick by one. It returns a bytes_list of rejections.

A command that cannot be applied, such as one aimed at an entity that no longer exists, is skipped. The rest still apply. Each rejection says which tick it happened in and why.

Journals and replay

A journal is the commands of each tick, in order.

union journal { journal_end(), journal_tick(commands: command_list, later: journal) }

replay(w, recorded) steps through a journal from the world’s current state. Because a step depends on nothing but the world and the commands, replaying the same journal from the same start always ends in the same world. A run is saved by saving its journal, not its states.

Memory per tick

Commands, text and anything else a tick makes live on the ordinary heap. The world does not. So the heap can be emptied after every tick:

let mark = raw_heap_mark();
let rejected = step(w, commands_for(w));
raw_heap_reset(mark);

Anything made after the mark is gone once the heap is reset, including the rejections, so use or print them first. A handle is a plain int and survives.

What is not here yet

There is no function that produces commands from the world on its own. Until the game language exists, the caller writes that part in qdx. There is also no source of random numbers and no pacing against a real clock: the loop counts ticks and nothing else.