DriftScript
Documentation

Guide

The entity model

Components hold the data, a system is a rule on a schedule, a query is how it finds what to run on, and uses is how a system is handed something the host owns.

The largest surface in the language is the one that describes a world: components hold the data, a system is a rule that runs on a schedule, and a query is how it finds what to run on. All three are language forms rather than a library, which is why the compiler can check what a system touches.

Components hold data, systems hold rules

A `component` declares fields and nothing else, and it is what a save file holds: every field has a stable id, so a scene saved before a rename loads after one. A `system` declares what it touches and an `update` block, and the block is the only place behaviour lives. Nothing hangs off a component, which is the same decision records made and it buys the same thing.

// Where a walker is, and where it is aiming next.
component Placement {
    x: f32 = 0
    z: f32 = 0
    aimX: f32 = 0
    aimZ: f32 = 0
}

// Where it is going, and how far it has left to go. `left` is written every step so anything
// watching the crowd can read it without asking the navigator.
component Errand {
    toX: f32 = 0
    toZ: f32 = 0
    left: f32 = 0
}
Two components. Every field is a number with a default.

A query is the loop

`for who in query<Placement, Errand>()` walks every entity carrying both, and `who.Placement.x` reads a column rather than a field of an object. Inside a loop that compiles to an array index; outside one it compiles to a call into the host, with both names checked, so a misspelled field is an error where it was written rather than a zero at run time.

        for who in query<Placement, Errand>() {
            let route = navigation.path(who)
The walk over agents, and the route for the one it is on.

uses is how a system is handed a host value

A system takes no arguments, so a handle the host owns reached a script only as a function parameter. `uses graph: NavGraph` binds one for the body. A resource is one per type, so the host is asked for the graph rather than for whatever this file calls it, and two systems naming it differently are handed the same one. Anything that is one per entity arrives the other way, as a value a capability answers for a handle.

system Walk {
    uses graph: NavGraph

    reads Errand
    writes Placement
    writes Errand

    update {
        for who in query<Placement, Errand>() {
            let route = navigation.path(who)
The head of the system, and the two things it is handed.

reads and writes are checked

They are assertions rather than documentation. The compiler works out what a system touches, following the functions it calls as well as its own body, and refuses a declaration that leaves a write out, naming the system and the component. Declaring more than you touch is a warning instead, since it is sometimes deliberate and costs only a scheduler that keeps two systems apart.

// A system that writes a component it did not say it writes.
//
// `reads` and `writes` are assertions rather than documentation: the compiler works out what a
// system touches, following the functions it calls as well as its own body, and refuses a
// declaration that leaves a write out. The name of the system and the name of the component are
// both in the message, so the fix is where the reader is already looking.
//
// A declaration wider than the body is a warning instead, because claiming more than you touch is
// sometimes deliberate and costs only a scheduler that keeps two systems apart.

component Hunger {
    value: f32 = 0
}

system Feeder {
    reads Hunger

    update {
        for who in query<Hunger>() {
            who.Hunger.value = who.Hunger.value + 1
        }
    }
}

The whole thing, compiled

Two components, one system, and a rule a designer can edit while the crowd is walking. The host runs the schedule and owns the graph and the routes; every decision about when to re-path, where to aim and what arriving means is here.

// A crowd that walks somewhere, with the loop over agents inside the script.
//
// Everything here is one `system`. It reads where each walker is and where it is going, asks the
// host for a route, and writes back the direction to head in. Nothing outside this file decides
// when to re-path or when a walker has arrived.
//
// **`uses` is what makes that possible.** A system takes no arguments, so before it existed a
// handle reached a script only as a function parameter, and a host that wanted the rule to stay
// editable had to run the loop itself and call in once per agent per step. The clause hands the
// system the graph, and a resource is one per type, so nothing here has to name which graph.
//
// A route is one per walker rather than one per world, so it arrives the other way: the host keeps
// one for each agent and `navigation.path` answers it.

import { arrived, following, path, remaining, route, steerX, steerZ } from "drift/navigation"

// Where a walker is, and where it is aiming next.
component Placement {
    x: f32 = 0
    z: f32 = 0
    aimX: f32 = 0
    aimZ: f32 = 0
}

// Where it is going, and how far it has left to go. `left` is written every step so anything
// watching the crowd can read it without asking the navigator.
component Errand {
    toX: f32 = 0
    toZ: f32 = 0
    left: f32 = 0
}

system Walk {
    uses graph: NavGraph

    reads Errand
    writes Placement
    writes Errand

    update {
        for who in query<Placement, Errand>() {
            let route = navigation.path(who)

            // Re-path only when there is nothing to follow. A walker keeps the route it has, which
            // is what stops a crowd recomputing the same paths sixty times a second.
            if !navigation.following(route) {
                navigation.route(
                    route,
                    graph,
                    who.Placement.x, 0, who.Placement.z,
                    who.Errand.toX, 0, who.Errand.toZ,
                )
            }

            who.Errand.left = navigation.remaining(route, who.Placement.x, 0, who.Placement.z)

            // Aim at a point ahead on the route rather than at the next node, so a corner is cut
            // smoothly instead of pivoted around.
            who.Placement.aimX = navigation.steerX(route, who.Placement.x, 0, who.Placement.z)
            who.Placement.aimZ = navigation.steerZ(route, who.Placement.x, 0, who.Placement.z)

            if navigation.arrived(route, who.Placement.x, 0, who.Placement.z) {
                who.Placement.aimX = who.Placement.x
                who.Placement.aimZ = who.Placement.z
            }
        }
    }
}
Press link. It compiles and links against this engine as its target.

Why the forms are in the language

A library could offer all of this, and then a system would be a callback and the compiler would have nothing to say about what it touches. Because the forms are the language, a schedule can be built from what the compiler already knows, a query loop can compile to an index instead of a call, and the access a system declares is checked against the access it has.