The world
A world holds entities. An entity is nothing but an identity, and what it is comes from the components attached to it: a position, a health value, a tag. Each kind of component is declared once, in a schema.
fn layout() -> schema {
let position = with_field_decl(with_field_decl(component_decl_of("position"),
"x", kind_int()), "y", kind_int());
let health = with_field_decl(component_decl_of("health"), "points", kind_int());
with_component_decl(schema_of(position), health)
}
fn position() -> int { 0 }
fn health() -> int { 1 }
fn main() -> int {
let w = world_new(layout());
let hero = world_spawn(w);
world_set(w, hero, position(), word(value: 2, next: word(value: 3, next: words_end())));
world_set(w, hero, health(), word(value: 10, next: words_end()));
print(world_text(w))
}bootstrap/qdx run std/*.qdx engine/*.qdx hero.qdxtick 0
slots 1
entity @0#1
position x=2 y=3
health points=10Schemas
A schema lists the component kinds a world can hold. Each kind has a name and fields, and each field has a name and one of four kinds:
| Kind | Holds |
|---|---|
kind_int() | A whole number. |
kind_fixed() | A fixed-point number. |
kind_bool() | 0 for false, anything else for true. |
kind_entity() | A handle to an entity. |
A component with no fields is a tag: an entity either has it or does not.
Components are numbered in the order they are declared, starting at 0, and the world’s functions take that number. Give each number a named function, as position() and health() do above. A schema holds at most 63 component kinds.
Handles
world_spawn returns a handle, an int that names one entity. It is written @0#1: slot 0, generation 1.
When an entity is destroyed its slot is freed, and a later spawn may use the slot again with a higher generation. A handle to the old entity then no longer matches, so world_is_alive is false for it and every other function refuses it. A stale handle can never reach the wrong entity.
Freed slots are reused oldest first.
Reading
| Function | Result | Meaning |
|---|---|---|
world_is_alive(w, handle) | bool | Whether the handle names a living entity. |
world_has(w, handle, component) | bool | Whether the entity has that component. |
world_field(w, handle, component, field) | int | The value of one field, by its position in the component. Stops the program if the entity lacks the component. |
world_count(w) | int | The number of living entities. |
world_tick(w) | int | The current tick. |
world_text(w) | bytes | The whole world as text. |
Changing
These functions change the world in place. In a game they are called only by the tick step, never by game rules.
| Function | Result | Meaning |
|---|---|---|
world_spawn(w) | int | A new entity with no components. Returns its handle. |
world_destroy(w, handle) | bool | Removes the entity and all its components. False if it was not alive. |
world_set(w, handle, component, data) | bool | Attaches or replaces a component. False if the entity is not alive, the component does not exist, or data has the wrong number of values. |
world_remove(w, handle, component) | bool | Detaches a component. False if the entity did not have it. |
Component data is a words value, a list of int, with one value per field in declared order: word(value: 2, next: word(value: 3, next: words_end())).
The text form
world_text writes the tick, the number of slots ever used, and every living entity in slot order with its components in declared order. Two worlds with the same content always produce the same text, which makes it the way to compare them.
Snapshots
| Function | Result | Meaning |
|---|---|---|
world_snapshot(w) | int | Keeps the world as it is now. Returns a snapshot. |
world_restore(w, snapshot) | int | Puts the world back to the snapshot. The snapshot stays usable. |
snapshot_release(w, snapshot) | int | Lets go of a snapshot that is no longer needed. |
Taking a snapshot copies nothing. The world’s memory is kept in blocks of one page, and a snapshot shares them with the live world. The first change to a shared block copies that one block. The cost of a snapshot is therefore the size of what changes afterwards, not the size of the world.
Memory
World data lives in memory taken directly from the operating system, outside the heap that ordinary qdx values use. That is what allows the tick loop to throw away everything on the heap after each tick without touching the world.