Everything that changes in a world changes in one of three ways:

MechanismWhat it isWhen it acts
Subscription (this page)A value defined from other values: derive(path, expression).Whenever an input changes.
Eventon(trigger, action): when something happens, do something once.When the trigger fires.
AnimatorA process that takes a value from here to there over time, built and then started.Every tick until it ends or is stopped.

A subscription works like a cell in a spreadsheet. Say once "the light's strength is the bulb's glow times 0.45 candela per nit, times the gain", and whenever the glow or the gain changes, the light follows by itself; no line anywhere "updates" it. The subscription subscribes to everything its expression mentions, the clock included, and the engine keeps it current.

None of the three has to be written beside the field it changes. Each names its target by path, so a statement can sit with the field, with the design, with the event, or in a world-level section, wherever reads best. Where they sit is a matter of arrangement, not meaning.

How you write it

derive(targetPath, expression)

The target is a field, named by path (fields.html, parts.html). The expression may name any value the author can address: a field of this part (glow), another part's (self.parent.lantern[13].glow), a material's (materials.lanternBulb.glow), what another part answers (land.height(here)), the clock (now(world), now(sim)).

// The world's lamp level, and what follows it
field lampLevel: fraction = 0, within: 0 ..= 1, mutable: true
derive(materials.lanternBulb.glow,  lampLevel * bulbBrightness.at(eveningHour(hour(now(world)))))
derive(materials.lanternGlass.glow, materials.lanternBulb.glow * 0.09)
derive(materials.lanternHead.glow,  materials.lanternBulb.glow * 0.0015)
derive(materials.room.glow,         materials.lanternBulb.glow * 0.4)

// In the lantern: two events (in, out) become one value a formula can read
derive(near, occupied(post, by: me))
derive(glass.tint, gain)
derive(bulb.tint,  gain)
derive(light.intensity, materials.lanternBulb.glow * cdPerNit * gain)

// The balloon: riding is never stored, only launched
field launched: moment = never, mutable: true, lives: server
derive(riding, now(sim) - launched < sim(170 s))

In this example the bulb's glow is about 2,800 nits at dusk (17:28), falling to 1.3 nits from 20:00: the lamp level times a brightness that follows the hour, read from the world's curve bulbBrightness, eight evening hours blended on a log scale (a morning hour mirrored about noon). The light gives cdPerNit, 0.45 candela per nit. The balloon is riding for 170 s after its last launch.

A block as the expression

Where a value takes several steps to say, the expression may be a block: local names, then a last line, written without a semicolon, whose value is the block's (blocks.html). In Cone's form, the balloon's burner, which fires in bursts whose rhythm depends on where the ride is:

derive(flame.firing, {
  let t             = now(sim);
  let landed        = launched + ride.length();
  let reachedSummit = launched + ride.timeAt(along: course.pointNamed(summit))   // about 70 s into the ride
  let quiet         = landed - sim(45 s);
  let burst = if launched == never {
    pulse(from: start(sim), every: sim(4 s), for: sim(3 s), at: t)
  } else if t >= landed {
    pulse(from: landed, every: sim(4 s), for: sim(3 s), at: t)
  } else if t < reachedSummit {
    pulse(from: launched, every: sim(5 s), for: sim(3.2 s), at: t)
  } else if t < quiet {
    pulse(from: reachedSummit, every: sim(20 s), for: sim(1.5 s), at: t)
  } else {
    until(pulse(from: reachedSummit, every: sim(20 s), for: sim(1.5 s), at: quiet), quiet)
  };
  burst.contains(t)
})
derive(roar.gain, if flame.firing { 1.5 } else { 0 })

On the ground: 3 s on in every 4; climbing to the summit: 3.2 s in every 5; after it, 1.5 s in every 20; nothing in the last 45 s. pulse, until and span are defined on Blocks. summit is a point of the course and reachedSummit a moment; the notation refuses a local name that hides a name its own definition reads.

What it means

It defines the value outright

A value block may not act

The block inside a derive is a pure calculation. It may not set, start, spawn, send, say or play, because the engine recalculates a value when and as often as it chooses (every frame, only when an input changes, on the server, on each visitor's machine), so an act inside one would happen an unknown number of times in unknown places. Observations that change nothing in the world, such as a line written to a debug log, are allowed. This discipline is what makes recalculation safe, and so makes the spreadsheet model possible at all.

Calculate, then kick something off

To act when a calculated value changes, write two lines: the calculation as a subscription, and a rule that watches it with became. The rule fires once, at the moment the value becomes true, however often the value is recalculated (events.html). An illustration:

derive(home, launched != never && !riding)
on(became(home), play(sound.chime, at: basket))        // illustration

Several effects on one value: the author's formula

Every value is one value. When several things affect one thing, each effect gets its own value, driven by its own animator or subscription, and the value seen is a subscription over them that the author writes. The lantern's light is already three effects multiplied: the lamp level, the hour's brightness and the nearness gain. An illustration with two more:

field flicker: number = 0, mutable: true     // illustration: its own looping animator
field flash   : number = 0, mutable: true    // illustration: up and back in a fraction of a second
derive(light.intensity, clamp(materials.lanternBulb.glow * 0.45 candelaPerNit * gain
                              + flicker + flash, within: 0 ..= maxIntensity))

The author decides whether effects add, multiply or are clamped, and has to: an animated value and a bound value acting together can never combine by a rule nobody wrote. What it costs: an effect added later needs its own named value and a change to the formula. What it buys: one value per name; state that is plain values; no engine policy hidden in combine modes.

Poses blend the same way. A wave over a walk is a formula over two poses, using the pose library's blending, and the wave's fade in and out is an animator on its weight:

derive(body.pose, add(blend(gait.pose, hailPose, weight: wave,
                            mask: bones([rightUpperArm, rightForearm, rightHand, neck, chest])),
                      spellPose))

Build-time and live: one mechanism

A build-time subscription is one whose inputs stop changing once the world is built: a placement at (p.x, land.height(p), p.z), the brook's level from the land. A live one keeps following its inputs: the light from the glow. Both are written with derive. A world states what depends on what; when the engines recompute is their business.

Deriving a kind, a product or an image

The target may be more than a number. A part's kind and its solid can be written as subscriptions, and a live picture can be wired the same way:

derive(pane.kind, if open { portal(link, shows: labyrinth) } else { mirror })     // the looking-glass
derive(house.solid, if open { difference(house.shape, notch(...)) } else { house.shape })
derive(sign.image, gateCamera.picture)                                       // illustration

Whether a field may change a part's kind or product this way, or a part may change type, or two parts are shown one at a time, is open.

When it runs, and where

Each tick, in order: events are handled; animators are stepped and write their values; then the subscriptions whose inputs changed are recomputed, in dependency order; then physics steps. Where a subscription runs follows from what it reads: one that reads a server-only field runs on the server; one whose inputs every machine has costs nothing on the network, since every machine can work it out. To begin with, the server runs the world's logic and sends results.

How it connects

Open questions

← Portals | Contents | Events and triggers →